view_component-contrib 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ class ApplicationViewComponent < ViewComponentContrib::Base
2
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationViewComponentPreview < ViewComponentContrib::Preview::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TemplateBuilder
4
+ attr_reader :root
5
+
6
+ def initialize(root)
7
+ @root = root
8
+ end
9
+
10
+ def get_binding
11
+ binding
12
+ end
13
+
14
+ def embed_code(path)
15
+ contents = File.read(File.join(root, path))
16
+ %Q(<<-CODE
17
+ #{contents}
18
+ CODE)
19
+ end
20
+
21
+ def embed(path)
22
+ File.read(File.join(root, path))
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ def class_for(name, from: identifier)
2
+ "c-\#{from}-\#{name}"
3
+ end
@@ -0,0 +1,275 @@
1
+ if yes?("Would you like to create a custom generator for your setup? (Recommended)")
2
+ template_choice_to_ext = {"1" => ".erb", "2" => ".haml", "3" => ".slim"}
3
+
4
+ template = ask "Which template processor do you use? (1) ERB, (2) Haml, (3) Slim, (0) Other"
5
+
6
+ TEMPLATE_EXT = template_choice_to_ext.fetch(template, "")
7
+ TEST_SUFFIX = USE_RSPEC ? 'spec' : 'test'
8
+
9
+ file "lib/generators/view_component/view_component_generator.rb", <<~CODE
10
+ # frozen_string_literal: true
11
+
12
+ # Based on https://github.com/github/view_component/blob/master/lib/rails/generators/component/component_generator.rb
13
+ class ViewComponentGenerator < Rails::Generators::NamedBase
14
+ source_root File.expand_path("templates", __dir__)
15
+
16
+ class_option :skip_test, type: :boolean, default: false
17
+ class_option :skip_preview, type: :boolean, default: false
18
+
19
+ argument :attributes, type: :array, default: [], banner: "attribute"
20
+
21
+ def create_component_file
22
+ template "component.rb", File.join("#{ROOT_PATH}", class_path, file_name, "component.rb")
23
+ end
24
+
25
+ def create_template_file
26
+ template "component.html#{TEMPLATE_EXT}", File.join("#{ROOT_PATH}", class_path, file_name, "component.html#{TEMPLATE_EXT}")
27
+ end
28
+
29
+ def create_test_file
30
+ return if options[:skip_test]
31
+
32
+ template "component_#{TEST_SUFFIX}.rb", File.join("#{TEST_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb")
33
+ end
34
+
35
+ def create_preview_file
36
+ return if options[:skip_preview]
37
+
38
+ template "preview.rb", File.join("#{ROOT_PATH}", class_path, file_name, "preview.rb")
39
+ end
40
+
41
+ private
42
+
43
+ def parent_class
44
+ "ApplicationViewComponent"
45
+ end
46
+
47
+ def preview_parent_class
48
+ "ApplicationViewComponentPreview"
49
+ end
50
+ end
51
+ CODE
52
+
53
+ if USE_WEBPACK
54
+ inject_into_file "lib/generators/view_component/view_component_generator.rb", after: "class_option :skip_preview, type: :boolean, default: false\n" do
55
+ <<-CODE
56
+ class_option :skip_js, type: :boolean, default: false
57
+ class_option :skip_css, type: :boolean, default: false
58
+ CODE
59
+ end
60
+
61
+ inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\n private" do
62
+ <<-CODE
63
+ def create_css_file
64
+ return if options[:skip_css] || options[:skip_js]
65
+
66
+ template "index.css", File.join("#{ROOT_PATH}", class_path, file_name, "index.css")
67
+ end
68
+
69
+ def create_js_file
70
+ return if options[:skip_js]
71
+
72
+ template "index.js", File.join("#{ROOT_PATH}", class_path, file_name, "index.js")
73
+ end
74
+ CODE
75
+ end
76
+ end
77
+
78
+ if USE_DRY
79
+ inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\nend" do
80
+ <<-CODE
81
+
82
+
83
+ def initialize_signature
84
+ return if attributes.blank?
85
+
86
+ attributes.map { |attr| "option :\#{attr.name}" }.join("\\n ")
87
+ end
88
+ CODE
89
+ end
90
+
91
+ file "lib/generators/view_component/templates/component.rb.tt",
92
+ <<~CODE
93
+ # frozen_string_literal: true
94
+
95
+ class <%= class_name %>::Component < <%= parent_class %>
96
+ <%- if initialize_signature -%>
97
+ <%= initialize_signature %>
98
+ <%- end -%>
99
+ end
100
+ CODE
101
+ else
102
+ inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\nend" do
103
+ <<-CODE
104
+
105
+
106
+ def initialize_signature
107
+ return if attributes.blank?
108
+
109
+ attributes.map { |attr| "\#{attr.name}:" }.join(", ")
110
+ end
111
+
112
+ def initialize_body
113
+ attributes.map { |attr| "@\#{attr.name} = \#{attr.name}" }.join("\\n ")
114
+ end
115
+ CODE
116
+ end
117
+
118
+ file "lib/generators/view_component/templates/component.rb.tt",
119
+ <<~CODE
120
+ # frozen_string_literal: true
121
+
122
+ class <%= class_name %>::Component < <%= parent_class %>
123
+ <%- if initialize_signature -%>
124
+ def initialize(<%= initialize_signature %>)
125
+ <%= initialize_body %>
126
+ end
127
+ <%- end -%>
128
+ end
129
+ CODE
130
+ end
131
+
132
+ if TEMPLATE_EXT == ".slim"
133
+ file "lib/generators/view_component/templates/component.html.slim.tt", <<~CODE
134
+ div Add <%= class_name %> template here
135
+ CODE
136
+ end
137
+
138
+ if TEMPLATE_EXT == ".erb"
139
+ file "lib/generators/view_component/templates/component.html.erb.tt", <<~CODE
140
+ <div>Add <%= class_name %> template here</div>
141
+ CODE
142
+ end
143
+
144
+ if TEMPLATE_EXT == ".haml"
145
+ file "lib/generators/view_component/templates/component.html.tt", <<~CODE
146
+ %div Add <%= class_name %> template here
147
+ CODE
148
+ end
149
+
150
+ if TEMPLATE_EXT == ""
151
+ file "lib/generators/view_component/templates/component.html.tt", <<~CODE
152
+ <div>Add <%= class_name %> template here</div>
153
+ CODE
154
+ end
155
+
156
+ file "lib/generators/view_component/templates/preview.rb.tt", <<~CODE
157
+ # frozen_string_literal: true
158
+
159
+ class <%= class_name %>::Preview < <%= preview_parent_class %>
160
+ # You can specify the container class for the default template
161
+ # self.container_class = "w-1/2 border border-gray-300"
162
+
163
+ def default
164
+ end
165
+ end
166
+ CODE
167
+
168
+ if USE_WEBPACK
169
+ if USE_STIMULUS
170
+ file "lib/generators/view_component/templates/index.js.tt",
171
+ <<-CODE
172
+ import "./index.css"
173
+
174
+ // Add a Stimulus controller for this component.
175
+ // It will automatically registered and its name will be available
176
+ // via #component_name in the component class.
177
+ //
178
+ // import { Controller as BaseController } from "stimulus";
179
+ //
180
+ // export class Controller extends BaseController {
181
+ // connect() {
182
+ // }
183
+ //
184
+ // disconnect() {
185
+ // }
186
+ // }
187
+ CODE
188
+ else
189
+ file "lib/generators/view_component/templates/index.js.tt", <<~CODE
190
+ import "./index.css"
191
+
192
+ CODE
193
+ end
194
+
195
+ if USE_POSTCSS_MODULES
196
+ file "lib/generators/view_component/templates/index.css.tt", <<~CODE
197
+ /* Use component-local class names and add them to HTML via #class_for(name) helper */
198
+
199
+ CODE
200
+ else
201
+ file "lib/generators/view_component/templates/index.css.tt", ""
202
+ end
203
+ end
204
+
205
+ if USE_RSPEC
206
+ file "lib/generators/view_component/templates/component_spec.rb.tt", <<~CODE
207
+ # frozen_string_literal: true
208
+
209
+ require "rails_helper"
210
+
211
+ describe <%= class_name %>::Component do
212
+ let(:options) { {} }
213
+ let(:component) { <%= class_name %>::Component.new(**options) }
214
+
215
+ subject { rendered_component }
216
+
217
+ it "renders" do
218
+ render_inline(component)
219
+
220
+ is_expected.to have_css "div"
221
+ end
222
+ end
223
+ CODE
224
+ else
225
+ file "lib/generators/view_component/templates/component_test.rb.tt", <<~CODE
226
+ # frozen_string_literal: true
227
+
228
+ require "test_helper"
229
+
230
+ class <%= class_name %>::ComponentTest < ActiveSupport::TestCase
231
+ include ViewComponent::TestHelpers
232
+
233
+ def test_renders
234
+ component = build_component
235
+
236
+ render_inline(component)
237
+
238
+ assert_selector "div"
239
+ end
240
+
241
+ private
242
+
243
+ def build_component(**options)
244
+ <%= class_name %>::Component.new(**options)
245
+ end
246
+ end
247
+ CODE
248
+ end
249
+
250
+ file "lib/generators/view_component/USAGE", <<~CODE
251
+ Description:
252
+ ============
253
+ Creates a new view component, test and preview files.
254
+ Pass the component name, either CamelCased or under_scored, and an optional list of attributes as arguments.
255
+
256
+ Example:
257
+ ========
258
+ bin/rails generate view_component Profile name age
259
+
260
+ creates a Profile component and test:
261
+ Component: #{ROOT_PATH}/profile/component.rb
262
+ Template: #{ROOT_PATH}/profile/component.html#{TEMPLATE_EXT}
263
+ Test: #{TEST_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb
264
+ Preview: #{ROOT_PATH}/profile/component_preview.rb
265
+ CODE
266
+
267
+ if USE_WEBPACK
268
+ inject_into_file "lib/generators/view_component/USAGE" do
269
+ <<-CODE
270
+ JS: #{ROOT_PATH}/profile/component.js
271
+ CSS: #{ROOT_PATH}/profile/component.css
272
+ CODE
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,7 @@
1
+
2
+
3
+ private
4
+
5
+ def identifier
6
+ @identifier ||= self.class.name.sub("::Component", "").underscore.split("/").join("--")
7
+ end
@@ -0,0 +1,2 @@
1
+ const context = require.context(".", true, /index.js$/)
2
+ context.keys().forEach(context);
@@ -0,0 +1,20 @@
1
+ // IMPORTANT: Update this import to reflect the location of your Stimulus application
2
+ // See https://github.com/palkan/view_component-contrib#using-with-stimulusjs
3
+ import { application } from "../init/stimulus";
4
+
5
+ const context = require.context(".", true, /index.js$/)
6
+ context.keys().forEach((path) => {
7
+ const mod = context(path);
8
+
9
+ // Check whether a module has the Controller export defined
10
+ if (!mod.Controller) return;
11
+
12
+ // Convert path into a controller identifier:
13
+ // example/index.js -> example
14
+ // nav/user_info/index.js -> nav--user-info
15
+ const identifier = path.replace(/^\\.\\//, '')
16
+ .replace(/\\/index\\.js$/, '')
17
+ .replace(/\\//, '--');
18
+
19
+ application.register(identifier, mod.Controller);
20
+ });
@@ -0,0 +1,16 @@
1
+ ActiveSupport.on_load(:view_component) do
2
+ # Extend your preview controller to support authentication and other
3
+ # application-specific stuff
4
+ #
5
+ # Rails.application.config.to_prepare do
6
+ # ViewComponentsController.class_eval do
7
+ # include Authenticated
8
+ # end
9
+ # end
10
+ #
11
+ # Make it possible to store previews in sidecar folders
12
+ # See https://github.com/palkan/view_component-contrib#organizing-components-or-sidecar-pattern-extended
13
+ ViewComponent::Preview.extend ViewComponentContrib::Preview::Sidecarable
14
+ # Enable `self.abstract_class = true` to exclude previews from the list
15
+ ViewComponent::Preview.extend ViewComponentContrib::Preview::Abstract
16
+ end
@@ -0,0 +1,13 @@
1
+ generateScopedName: (name, filename, _css) => {
2
+ const matches = filename.match(/#{ROOT_PATH.gsub('/', '\/')}\\/?(.*)\\/index.css$/);
3
+ // Do not transform CSS files from outside of the components folder
4
+ if (!matches) return name;
5
+
6
+ // identifier here is the same identifier we used for Stimulus controller (see above)
7
+ const identifier = matches[1].replace("/", "--");
8
+
9
+ // We also add the `c-` prefix to all components classes
10
+ return `c-${identifier}-${name}`;
11
+ },
12
+ // Do not generate *.css.json files (we don't use them)
13
+ getJSON: () => {}
@@ -0,0 +1,133 @@
1
+ say "👋 Welcome to interactive ViewComponent installer and configurator. " \
2
+ "Make sure you've read the view_component-contrib guide: https://github.com/palkan/view_component-contrib"
3
+
4
+ run "bundle add view_component view_component-contrib --skip-install"
5
+
6
+ inject_into_file "config/application.rb", "require \"view_component/engine\"\n", before: "\nBundler.require(*Rails.groups)"
7
+
8
+ say_status :info, "✅ ViewComponent gems added"
9
+
10
+ DEFAULT_ROOT = "app/frontend/components"
11
+
12
+ root = ask("Where do you want to store your view components? (default: #{DEFAULT_ROOT})")
13
+ ROOT_PATH = root.present? && root.downcase != "n" ? root : DEFAULT_ROOT
14
+
15
+ root_paths = ROOT_PATH.split("/").map { |path| "\"#{path}\"" }.join(", ")
16
+
17
+ application "config.view_component.preview_paths << Rails.root.join(#{root_paths})"
18
+ application "config.autoload_paths << Rails.root.join(#{root_paths})"
19
+
20
+ say_status :info, "✅ ViewComponent paths configured"
21
+
22
+ file "#{ROOT_PATH}/application_view_component.rb",
23
+ <%= embed_code("./application_view_component.rb") %>
24
+
25
+ file "#{ROOT_PATH}/application_view_component_preview.rb",
26
+ <%= embed_code("./application_view_component_preview.rb") %>
27
+
28
+ say_status :info, "✅ ApplicationViewComponent and ApplicationViewComponentPreview classes added"
29
+
30
+ USE_RSPEC = File.directory?("spec")
31
+ TEST_ROOT_PATH = USE_RSPEC ? File.join("spec", ROOT_PATH.sub("app/", "")) : File.join("test", ROOT_PATH.sub("app/", ""))
32
+
33
+ USE_DRY = yes? "Would you like to use dry-initializer in your component classes?"
34
+
35
+ if USE_DRY
36
+ run "bundle add dry-initializer --skip-install"
37
+
38
+ inject_into_file "#{ROOT_PATH}/application_view_component.rb", "\n extend Dry::Initializer", after: "class ApplicationViewComponent < ViewComponentContrib::Base"
39
+
40
+ say_status :info, "✅ Extended ApplicationViewComponent with Dry::Initializer"
41
+ end
42
+
43
+ initializer "view_component.rb",
44
+ <%= embed_code("./initializer.rb") %>
45
+
46
+ say_status :info, "✅ Added ViewComponent initializer with required patches"
47
+
48
+ if USE_RSPEC
49
+ inject_into_file "spec/rails_helper.rb", after: "require \"rspec/rails\"\n" do
50
+ "require \"capybara/rspec\"\nrequire \"view_component/test_helpers\"\n"
51
+ end
52
+
53
+ inject_into_file "spec/rails_helper.rb", after: "RSpec.configure do |config|\n" do
54
+ <<-CODE
55
+ config.include ViewComponent::TestHelpers, type: :view_component
56
+ config.include Capybara::RSpecMatchers, type: :view_component
57
+
58
+ config.define_derived_metadata(file_path: %r{/#{TEST_ROOT_PATH}}) do |metadata|
59
+ metadata[:type] = :view_component
60
+ end
61
+
62
+ CODE
63
+ end
64
+ end
65
+
66
+ say_status :info, "✅ RSpec configured"
67
+
68
+ USE_WEBPACK = File.directory?("config/webpack")
69
+
70
+ if USE_WEBPACK
71
+ USE_STIMULUS = yes? "Do you use StimulusJS?"
72
+
73
+ if USE_STIMULUS
74
+ file "#{ROOT_PATH}/index.js",
75
+ <%= embed_code("./index.stimulus.js") %>
76
+
77
+ inject_into_file "#{ROOT_PATH}/application_view_component.rb", before: "\nend" do
78
+ <%= embed_code("./identifier.rb") %>
79
+ end
80
+ else
81
+ file "#{ROOT_PATH}/index.js",
82
+ <%= embed_code("./index.js") %>
83
+ end
84
+
85
+ say_status :info, "✅ Added index.js to load components JS/CSS"
86
+ say "⚠️ Don't forget to import component JS/CSS (#{ROOT_PATH}/index.js) from your application.js entrypoint"
87
+
88
+ USE_POSTCSS_MODULES = yes? "Would you like to use postcss-modules to isolate component styles?"
89
+
90
+ if USE_POSTCSS_MODULES
91
+ run "yarn add postcss-modules"
92
+
93
+ if File.read("postcss.config.js").match(/plugins:\s*\[/)
94
+ inject_into_file "postcss.config.js", after: "plugins: [" do
95
+ <<-CODE
96
+
97
+ require('postcss-modules')({
98
+ <%= embed("./postcss-modules.js") %>
99
+ }),
100
+ CODE
101
+ end
102
+ else
103
+ inject_into_file "postcss.config.js", after: "plugins: {" do
104
+ <<-CODE
105
+
106
+ 'postcss-modules': {
107
+ <%= embed("./postcss-modules.js") %>
108
+ },
109
+ CODE
110
+ end
111
+ end
112
+
113
+ if !USE_STIMULUS
114
+ inject_into_file "#{ROOT_PATH}/application_view_component.rb", before: "\nend" do
115
+ <%= embed_code("./identifier.rb") %>
116
+ end
117
+ end
118
+
119
+ inject_into_file "#{ROOT_PATH}/application_view_component.rb", before: "\nend" do
120
+ <%= embed_code("./class_for.rb") %>
121
+ end
122
+
123
+ say_status :info, "✅ postcss-modules configured"
124
+ end
125
+ end
126
+
127
+ <%= embed("./generator.rb") %>
128
+
129
+ say "Installing gems..."
130
+
131
+ Bundler.with_unbundled_env { run "bundle install" }
132
+
133
+ say_status :info, "✅ You're ready to rock!"