view_component-contrib 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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!"