amber_component 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d81663eb6f3cd153cb0a277a0114f8cce4e2140f40685f23a688a60c8ce86226
4
- data.tar.gz: 32abb3383e052466435f318e1f8c4010adfa5f3d7e93e588e7e2842ff7cd59e6
3
+ metadata.gz: 46dc65dbef14224da8e0ce373eb07528188c399444eb2b90968c1cf3dc049864
4
+ data.tar.gz: d547837963b2e6148a0724d961f3ba9fa1f540365a711d40d483a24ccdcf8909
5
5
  SHA512:
6
- metadata.gz: 4a3df277a91914b6a148cc72c1b3981f78c420e773b408c8555a8915424d11667c804f113c2a1fd76bce751083607086ab660f1634855fcec686bc433b875c12
7
- data.tar.gz: 330efd82e1589a4804f72f4dcb1df1f9a9b90696ce2dfb542c69b91dd1b0a5b7797736c9fa8ce461f73ff7bb16a3966a8ed00b8340c6d047d289acde976240cc
6
+ metadata.gz: 9f060bd9c1a97475c0678ddc5c0df166bf6f05001cde7f43a41bdc130c068b2ea24c15518d384016aeef2e8ef592b2928c305a41aaaa24dcebf038783a96b57b
7
+ data.tar.gz: 8f2f8029094be9ac3eccbb55b1c46a3c1ed43bb5e7d4c55263bf9bf0a2501862589f7644b8c357ceaa1d8762ea2370125f29c85bce7299925588570ba5118860
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- amber_component (1.0.0)
4
+ amber_component (1.1.1)
5
5
  actionview (>= 6)
6
6
  activemodel (>= 6)
7
7
  activesupport (>= 6)
data/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  [![Maintainability](https://api.codeclimate.com/v1/badges/ad84af499e9791933a87/maintainability)](https://codeclimate.com/github/amber-ruby/amber_component/maintainability)
4
4
  [![CI badge](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml/badge.svg)](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml)
5
5
  [![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Verseth/6a095c79278b074d79feaa4f8ceeb2a8/raw/amber_component__heads_main.json)](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml)
6
- [![Downloads](https://ruby-gem-downloads-badge.herokuapp.com/amber_component)]((https://rubygems.org/gems/amber_component))
6
+ <!-- [![Downloads](https://ruby-gem-downloads-badge.herokuapp.com/amber_component)]((https://rubygems.org/gems/amber_component)) -->
7
7
 
8
8
  <img src="banner.png" width="500px" style="margin-bottom: 2rem;"/>
9
9
 
10
10
  # AmberComponent
11
11
 
12
- AmberComponent is a simple component library which seamlessly hooks into your Rails project and allows you to create simple backend components. They work like mini controllers which are bound with their view and stylesheet.
12
+ AmberComponent is a simple component library which seamlessly hooks into your Rails project and allows you to create simple backend components which consist of a Ruby controller, view, stylesheet and even a JavaScript controller (using [Stimulus](https://stimulus.hotwired.dev/)).
13
13
 
14
14
  Created by [Garbus Beach](https://github.com/garbusbeach) and [Mateusz Drewniak](https://github.com/Verseth).
15
15
 
@@ -33,16 +33,44 @@ If you're using a Rails application there's an installation generator that you s
33
33
  $ bin/rails generate amber_component:install
34
34
  ```
35
35
 
36
+ Amber component supports [Stimulus](https://stimulus.hotwired.dev/) to make your components
37
+ reactive using JavaScript.
38
+
39
+ If you want to use stimulus you should install this gem with the `--stimulus` flag
40
+
41
+ ```sh
42
+ $ bin/rails generate amber_component:install --stimulus
43
+ ```
44
+
36
45
  ## Usage
37
46
 
38
- ## Components
47
+ ### Components
39
48
 
40
- Components are located under `app/components`.
49
+ Components are located under `app/components`. And their tests under `test/components`.
41
50
 
42
51
  Every component consists of:
43
52
  - a Ruby file which defines its properties, encapsulates logic and may implement helper methods (like a controller)
44
53
  - a view/template file (html.erb, haml, slim etc.)
45
54
  - a style file (css, scss, sass etc.)
55
+ - [optional] a JavaScript file with a Stimulus controller (if you installed the gem with `--stimulus`)
56
+
57
+ `amber_component` automatically detects what kind of view and stylesheet formats your app is configured to use.
58
+
59
+ So if you've got `haml-rails`, components will be generated with `haml`. When your app uses `slim-rails`, components will be generated with `slim`. When your `Gemfile` contains `sassc-rails`, components will use `scss` etc.
60
+
61
+ All of these formats can be overridden in
62
+ an initializer or by adding arguments to the component generator.
63
+
64
+ ```
65
+ app/components/
66
+ ├─ [name]_component.rb
67
+ └─ [name]_component/
68
+ ├─ style.css # may be .sass or .scss
69
+ ├─ view.html.erb
70
+ └─ controller.js # if stimulus is configured
71
+ test/components/
72
+ └─ [name]_component_test.rb
73
+ ```
46
74
 
47
75
  An individual component which implements a button may look like this.
48
76
 
@@ -57,7 +85,9 @@ end
57
85
  ```html
58
86
  <!-- app/components/button_component/view.html.erb -->
59
87
 
60
- <div class="button_component">
88
+ <div class="button_component"
89
+ data-controller="button-component"
90
+ data-action="click->button-component#greet">
61
91
  <%= label %>
62
92
  </div>
63
93
  ```
@@ -76,6 +106,25 @@ end
76
106
  }
77
107
  ```
78
108
 
109
+ If you used the `--stimulus` option when installing the gem, a JS controller will be generated as well.
110
+ ```js
111
+ // app/components/button_component/controller.js
112
+
113
+ import { Controller } from "@hotwired/stimulus"
114
+
115
+ // Read more about Stimulus here https://stimulus.hotwired.dev/
116
+ export default class extends Controller {
117
+ connect() {
118
+ console.log("Stimulus controller 'button-component' is connected!")
119
+ }
120
+
121
+ greet() {
122
+ alert("Hi there!")
123
+ }
124
+ }
125
+
126
+ ```
127
+
79
128
  You can render this component in other components or in a Rails view.
80
129
 
81
130
  ```html
@@ -97,34 +146,76 @@ ButtonComponent.call label: 'Click me!'
97
146
  #=> '<div class="button_component">Click me!</div>'
98
147
  ```
99
148
 
100
- ### Rails helpers inside component templates
149
+ ### Components with namespaces
101
150
 
102
- Component views/template files can make use
103
- of all ActionView helpers and Rails route helpers.
151
+ Components may be defined inside multiple modules/namespaces.
104
152
 
105
- This makes component views very flexible and convenient.
153
+ ```ruby
154
+ # app/components/sign_up/button_component.rb
106
155
 
107
- ```erb
108
- <!-- app/components/login_form_component/view.html.erb -->
156
+ class SignUp::ButtonComponent < AmberComponent::Base
157
+ prop :label, required: true
158
+ end
159
+ ```
109
160
 
110
- <%= form_with url: sign_up_path, class: "login_form_component" do |f| %>
111
- <%= f.label :first_name %>
112
- <%= f.text_field :first_name %>
161
+ ```html
162
+ <!-- app/components/sign_up/button_component/view.html.erb -->
113
163
 
114
- <%= f.label :last_name %>
115
- <%= f.text_field :last_name %>
164
+ <div class="sign_up_button_component">
165
+ <%= label %>
166
+ </div>
167
+ ```
116
168
 
117
- <%= f.label :email, "Email Address" %>
118
- <%= f.text_field :email %>
169
+ ```scss
170
+ // app/components/sign_up/button_component/style.scss
119
171
 
120
- <%= f.label :password %>
121
- <%= f.password_field :password %>
172
+ .sign_up_button_component {
173
+ background-color: indigo;
174
+ border-radius: 1rem;
175
+ transition-duration: 500ms;
122
176
 
123
- <%= f.label :password_confirmation, "Confirm Password" %>
124
- <%= f.password_field :password_confirmation %>
177
+ &:hover {
178
+ background-color: blue;
179
+ }
180
+ }
181
+ ```
182
+
183
+ You can render such a component by calling the `::call` method
184
+ on its class, or by using the helper method defined on its parent module.
185
+
186
+ ```ruby
187
+ SignUp::ButtonComponent.call label: 'Sign up!'
188
+ SignUp.button_component label: 'Sign up!'
189
+ ```
190
+
191
+ ### Generating Components
192
+
193
+ You can generate new components by running
194
+
195
+ ```sh
196
+ $ bin/rails generate component [name]
197
+ ```
198
+
199
+ Name of the component may be PascalCased like `FooBar` or snake_cased `foo_bar`
200
+
201
+ This will generate a new component in `app/components/[name]_component.rb` along with a view, stylesheet, test file and a stimulus controller (if configured).
125
202
 
126
- <%= f.submit "Create account" %>
127
- <% end %>
203
+ ```
204
+ app/components/
205
+ ├─ [name]_component.rb
206
+ └─ [name]_component/
207
+ ├─ style.css # may be `.scss` or `.sass`
208
+ ├─ view.html.erb # may be `.haml` or `.slim`
209
+ └─ controller.js # if stimulus is configured
210
+ test/components/
211
+ └─ [name]_component_test.rb
212
+ ```
213
+
214
+ View and stylesheet formats can be overridden by providing options.
215
+
216
+ ```
217
+ -v, [--view=VIEW] # Indicate what type of view should be generated eg. [:erb, :haml, :slim]
218
+ --styles, -c, [--css=CSS] # Indicate what type of styles should be generated eg. [:css, :scss, :sass]
128
219
  ```
129
220
 
130
221
  ### Component properties
@@ -155,39 +246,6 @@ CommentComponent.call body: 'Foo bar', author: User.first
155
246
  comment_component body: 'Foo bar', author: User.first
156
247
  ```
157
248
 
158
- ### Overriding prop getters and setters
159
-
160
- Getters and setters for properties are
161
- defined in a module which means that you can override them and call them with `super`.
162
-
163
- ```ruby
164
- # app/components/priority_icon_component.rb
165
-
166
- class PriorityIconComponent < ApplicationComponent
167
- PriorityStruct = Struct.new :icon, :color
168
-
169
- PRIORITY_MAP = {
170
- low: PriorityStruct.new('fa-solid fa-chevrons-down', 'green'),
171
- medium: PriorityStruct.new('fa-solid fa-chevron-up', 'yellow'),
172
- high: PriorityStruct.new('fa-solid fa-chevrons-up', 'red')
173
- }
174
-
175
- prop :severity, default: -> { :low }
176
-
177
- def severity=(val)
178
- # super will call the original
179
- # implementation of the setter
180
- super(PRIORITY_MAP[val])
181
- end
182
- end
183
- ```
184
-
185
- ```html
186
- <!-- app/components/priority_icon_component/view.html.erb -->
187
-
188
- <i style="color: <%= severity&.color %>;" class="<%= severity&.icon %>"></i>
189
- ```
190
-
191
249
  ### Helper methods
192
250
 
193
251
  Defining helper methods which are available
@@ -239,6 +297,39 @@ end
239
297
  </div>
240
298
  ```
241
299
 
300
+ ### Overriding prop getters and setters
301
+
302
+ Getters and setters for properties are
303
+ defined in a module which means that you can override them and call them with `super`.
304
+
305
+ ```ruby
306
+ # app/components/priority_icon_component.rb
307
+
308
+ class PriorityIconComponent < ApplicationComponent
309
+ PriorityStruct = Struct.new :icon, :color
310
+
311
+ PRIORITY_MAP = {
312
+ low: PriorityStruct.new('fa-solid fa-chevrons-down', 'green'),
313
+ medium: PriorityStruct.new('fa-solid fa-chevron-up', 'yellow'),
314
+ high: PriorityStruct.new('fa-solid fa-chevrons-up', 'red')
315
+ }
316
+
317
+ prop :severity, default: -> { :low }
318
+
319
+ def severity=(val)
320
+ # super will call the original
321
+ # implementation of the setter
322
+ super(PRIORITY_MAP[val])
323
+ end
324
+ end
325
+ ```
326
+
327
+ ```html
328
+ <!-- app/components/priority_icon_component/view.html.erb -->
329
+
330
+ <i style="color: <%= severity&.color %>;" class="<%= severity&.icon %>"></i>
331
+ ```
332
+
242
333
  ### Nested components
243
334
 
244
335
  It's possible to nest components or provide
@@ -304,64 +395,51 @@ In general `block_given?` will return `true` when a block/nested content is pres
304
395
  You can use it to render content conditionally based on
305
396
  whether nested content is present.
306
397
 
307
- ### Components with namespaces
308
-
309
- Components may be defined inside multiple modules/namespaces.
398
+ ### Rails helpers inside component templates
310
399
 
311
- ```ruby
312
- # app/components/sign_up/button_component.rb
400
+ Component views/template files can make use
401
+ of all ActionView helpers and Rails route helpers.
313
402
 
314
- class SignUp::ButtonComponent < AmberComponent::Base
315
- prop :label, required: true
316
- end
317
- ```
403
+ This makes component views very flexible and convenient.
318
404
 
319
- ```html
320
- <!-- app/components/sign_up/button_component/view.html.erb -->
405
+ ```erb
406
+ <!-- app/components/login_form_component/view.html.erb -->
321
407
 
322
- <div class="sign_up_button_component">
323
- <%= label %>
324
- </div>
325
- ```
408
+ <%= form_with url: sign_up_path, class: "login_form_component" do |f| %>
409
+ <%= f.label :first_name %>
410
+ <%= f.text_field :first_name %>
326
411
 
327
- ```scss
328
- // app/components/sign_up/button_component/style.scss
412
+ <%= f.label :last_name %>
413
+ <%= f.text_field :last_name %>
329
414
 
330
- .sign_up_button_component {
331
- background-color: indigo;
332
- border-radius: 1rem;
333
- transition-duration: 500ms;
415
+ <%= f.label :email, "Email Address" %>
416
+ <%= f.text_field :email %>
334
417
 
335
- &:hover {
336
- background-color: blue;
337
- }
338
- }
339
- ```
418
+ <%= f.label :password %>
419
+ <%= f.password_field :password %>
340
420
 
341
- You can render such a component by calling the `::call` method
342
- on its class, or by using the helper method defined on its parent module.
421
+ <%= f.label :password_confirmation, "Confirm Password" %>
422
+ <%= f.password_field :password_confirmation %>
343
423
 
344
- ```ruby
345
- SignUp::ButtonComponent.call label: 'Sign up!'
346
- SignUp.button_component label: 'Sign up!'
424
+ <%= f.submit "Create account" %>
425
+ <% end %>
347
426
  ```
348
427
 
349
- ### Generating Components
350
-
351
- You an generate new components by running
428
+ ### Configuration
352
429
 
353
- ```sh
354
- $ bin/rails generate component foo_bar
355
- ```
430
+ This gem can be configured in an initializer.
431
+ If you used the installer generator it should already be present.
356
432
 
357
- or
433
+ ```ruby
434
+ # config/initializers/amber_component.rb
358
435
 
359
- ```sh
360
- $ bin/rails generate component FooBar
436
+ ::AmberComponent.configure do |c|
437
+ c.stimulus = nil # [nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]
438
+ c.stylesheet_format = :css # [:css, :scss, :sass]
439
+ c.view_format = :erb # [:erb, :haml, :slim]
440
+ end
361
441
  ```
362
442
 
363
- This will generate a new component in `app/components/foo_bar_component.rb` along with a view, stylesheet and test file.
364
-
365
443
  ### Testing Components
366
444
 
367
445
  ### Rails
@@ -43,7 +43,4 @@ require_relative "lib/amber_component/version"
43
43
  spec.add_dependency "activesupport", ">= 6"
44
44
  spec.add_dependency "memery", ">= 1.4.1"
45
45
  spec.add_dependency "tilt", ">= 2.0.10"
46
-
47
- # For more information and examples about making a new gem, check out our
48
- # guide at: https://bundler.io/guides/creating_gem.html
49
46
  end
@@ -0,0 +1,30 @@
1
+ const registeredControllers = {}
2
+
3
+ export function eagerLoadAmberComponentControllers(application) {
4
+ const paths = Object.keys(parseImportmapJson()).filter(path => path.match(new RegExp(`/controller$`)))
5
+ paths.forEach(path => registerControllerFromPath(path, application))
6
+ }
7
+
8
+ function parseImportmapJson() {
9
+ return JSON.parse(document.querySelector("script[type=importmap]").text).imports
10
+ }
11
+
12
+ function registerControllerFromPath(path, application) {
13
+ const name = path
14
+ .replace("/controller", "")
15
+ .replace(/\//g, "--")
16
+ .replace(/_/g, "-")
17
+
18
+ if (!(name in registeredControllers)) {
19
+ import(path)
20
+ .then(module => registerController(name, module, application))
21
+ .catch(error => console.error(`Failed to register controller: ${name} (${path})`, error))
22
+ }
23
+ }
24
+
25
+ function registerController(name, module, application) {
26
+ if (!(name in registeredControllers)) {
27
+ application.register(name, module.default)
28
+ registeredControllers[name] = true
29
+ }
30
+ }
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Object which stores configuration options
5
+ # for this gem.
6
+ class Configuration
7
+ # @return [Set<Symbol>]
8
+ STIMULUS_INTEGRATIONS = ::Set[nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]
9
+ # @return [Set<Symbol>]
10
+ ALLOWED_STYLES = ::Set.new(%i[css scss sass])
11
+ # @return [Set<Symbol>]
12
+ ALLOWED_VIEWS = ::Set.new(%i[erb haml slim])
13
+
14
+ # How Stimulus.js is bundled in this app.
15
+ # Possible values: `[nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]`
16
+ # `nil` indicates that stimulus should not be used (default behaviour).
17
+ #
18
+ # @return [Symbol, nil]
19
+ attr_reader :stimulus
20
+
21
+ # The default format that the generators will use
22
+ # for the view/template file of a component.
23
+ # Possible values: `[nil, :erb, :haml, :slim]`
24
+ #
25
+ # @return [Symbol, nil]
26
+ attr_reader :view_format
27
+
28
+ # The default format that the generators will use
29
+ # for the stylesheets of a component.
30
+ # Possible values: `[nil, :css, :scss, :sass]`
31
+ #
32
+ # @return [Symbol, nil]
33
+ attr_reader :stylesheet_format
34
+
35
+ # How Stimulus.js is bundled in this app.
36
+ # Possible values: `[nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]`
37
+ # `nil` indicates that stimulus should not be used (default behaviour).
38
+ #
39
+ # @param val [Symbol, String, nil]
40
+ def stimulus=(val)
41
+ val = val&.to_sym
42
+ unless val.nil? || STIMULUS_INTEGRATIONS.include?(val)
43
+ raise(::ArgumentError,
44
+ "Invalid value for `#{__method__}` bundling. " \
45
+ "Received #{val.inspect}, expected one of #{STIMULUS_INTEGRATIONS.inspect}")
46
+ end
47
+
48
+ @stimulus = val
49
+ end
50
+
51
+ # @param val [Symbol, String, nil]
52
+ def stylesheet_format=(val)
53
+ val = val&.to_sym
54
+ unless val.nil? || ALLOWED_STYLES.include?(val)
55
+ raise(::ArgumentError,
56
+ "Invalid value for `#{__method__}`. " \
57
+ "Received #{val.inspect}, expected one of #{ALLOWED_STYLES.inspect}")
58
+ end
59
+
60
+ @stylesheet_format = val
61
+ end
62
+
63
+ # @param val [Symbol, String, nil]
64
+ def view_format=(val)
65
+ val = val&.to_sym
66
+ unless val.nil? || ALLOWED_VIEWS.include?(val)
67
+ raise(::ArgumentError,
68
+ "Invalid value for `#{__method__}`. " \
69
+ "Received #{val.inspect}, expected one of #{ALLOWED_VIEWS.inspect}")
70
+ end
71
+
72
+ @view_format = val
73
+ end
74
+
75
+ # @return [Boolean]
76
+ def stimulus?
77
+ !@stimulus.nil?
78
+ end
79
+
80
+ # @return [Boolean]
81
+ def stimulus_importmap?
82
+ @stimulus == :importmap
83
+ end
84
+ end
85
+ end
@@ -4,8 +4,10 @@ module ::AmberComponent
4
4
  # Class which hooks into Rails
5
5
  # and configures the application.
6
6
  class Railtie < ::Rails::Railtie
7
- initializer 'amber_component.initialization' do |app|
7
+ initializer 'amber_component.assets' do |app|
8
8
  app.config.assets.paths << (app.root / 'app' / 'components')
9
+ app.config.assets.paths << (ROOT_GEM_PATH / 'assets' / 'javascripts')
10
+ app.config.assets.precompile += %w[amber_component/stimulus_loading.js]
9
11
 
10
12
  next if ::Rails.env.production?
11
13
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ::AmberComponent
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.1'
5
5
  end
@@ -8,7 +8,7 @@ module ::AmberComponent
8
8
  # @return [Set<Symbol>]
9
9
  VIEW_TYPES_WITH_RUBY = ::Set[:erb, :haml, :slim].freeze
10
10
  # @return [Set<Symbol>]
11
- ALLOWED_VIEW_TYPES = ::Set[:erb, :haml, :slim, :html, :md, :markdown].freeze
11
+ ALLOWED_VIEW_TYPES = ::Set[:erb, :haml, :slim, :html].freeze
12
12
  # @return [Regexp]
13
13
  VIEW_FILE_REGEXP = /^view\./.freeze
14
14
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext'
5
+ require 'pathname'
6
+ require 'set'
5
7
 
8
+ require_relative 'amber_component/configuration'
9
+
10
+ # Root module of the `amber_component` gem.
6
11
  module ::AmberComponent
7
12
  class Error < ::StandardError; end
8
13
  class MissingPropsError < Error; end
@@ -13,6 +18,22 @@ module ::AmberComponent
13
18
  class EmptyViewError < Error; end
14
19
  class UnknownViewTypeError < Error; end
15
20
  class MultipleViewsError < Error; end
21
+
22
+ # @return [Pathname]
23
+ ROOT_GEM_PATH = ::Pathname.new ::File.expand_path('..', __dir__)
24
+
25
+ class << self
26
+ # @return [Configuration]
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ # @yieldparam [Configuration]
32
+ # @return [void]
33
+ def configure
34
+ yield configuration
35
+ end
36
+ end
16
37
  end
17
38
 
18
39
  require_relative 'amber_component/version'
@@ -10,8 +10,22 @@ module ::AmberComponent
10
10
  desc 'Install the AmberComponent gem'
11
11
  source_root ::File.expand_path('templates', __dir__)
12
12
 
13
- # copy rake tasks
14
- def copy_tasks
13
+ class_option :stimulus,
14
+ desc: "Configure the app to use Stimulus.js wih components to make them interactive " \
15
+ "[options: importmap (default), webpacker (legacy), jsbundling, webpack, esbuild, rollup]"
16
+
17
+ class_option :styles,
18
+ desc: "Configure the app to generate components with a particular stylesheet format " \
19
+ "[options: css (default), scss, sass]"
20
+
21
+ class_option :views,
22
+ desc: "Configure the app to generate components with a particular view format " \
23
+ "[options: erb (default), haml, slim]"
24
+
25
+ def setup
26
+ detect_stimulus
27
+ detect_styles
28
+ detect_views
15
29
  copy_file 'application_component.rb', 'app/components/application_component.rb'
16
30
  copy_file 'application_component_test_case.rb', 'test/application_component_test_case.rb'
17
31
  append_file 'test/test_helper.rb', "require_relative 'application_component_test_case'"
@@ -23,10 +37,165 @@ module ::AmberComponent
23
37
  require_components_css_in 'app/assets/stylesheets/application.css.sass'
24
38
  require_components_css_in 'app/assets/stylesheets/application.scss.sass'
25
39
  require_components_css_in 'app/assets/stylesheets/application.sass.scss'
40
+ configure_stimulus
41
+ create_initializer
26
42
  end
27
43
 
28
44
  private
29
45
 
46
+ def detect_styles
47
+ styles_option = options[:styles]&.to_sym
48
+ if !styles_option.nil? && !Configuration::ALLOWED_STYLES.include?(styles_option)
49
+ raise ::ArgumentError, "no such `stylesheet_format` as #{styles_option.inspect}"
50
+ end
51
+
52
+ @styles =
53
+ if styles_option
54
+ styles_option
55
+ elsif defined?(::SassC)
56
+ :scss
57
+ else
58
+ :css
59
+ end
60
+ end
61
+
62
+ def detect_views
63
+ views_option = options[:views]&.to_sym
64
+ if !views_option.nil? && !Configuration::ALLOWED_VIEWS.include?(views_option)
65
+ raise ::ArgumentError, "no such `view_format` as #{views_option.inspect}"
66
+ end
67
+
68
+ @views =
69
+ if views_option
70
+ views_option
71
+ elsif defined?(::Haml)
72
+ :haml
73
+ elsif defined?(::Slim)
74
+ :slim
75
+ else
76
+ :erb
77
+ end
78
+ end
79
+
80
+ def detect_stimulus
81
+ stimulus_option = options[:stimulus]&.to_sym
82
+ return unless stimulus_option
83
+
84
+ case stimulus_option
85
+ when :stimulus
86
+ if defined?(::Jsbundling)
87
+ stimulus_jsbundling!
88
+ elsif defined?(::Webpacker)
89
+ stimulus_webpacker!
90
+ else
91
+ stimulus_importmap!
92
+ end
93
+ when :importmap
94
+ stimulus_importmap!
95
+ when :jsbundling, :webpack, :esbuild, :rollup
96
+ stimulus_jsbundling!
97
+ when :webpacker
98
+ stimulus_webpacker!
99
+ else
100
+ raise ::ArgumentError,
101
+ "no such stimulus integration as `#{options[:stimulus].inspect}`"
102
+ end
103
+ end
104
+
105
+ def assert_styles
106
+ return if options[:styles].nil?
107
+ return if options[:styles].nil?
108
+ end
109
+
110
+ def configure_stimulus
111
+ case @stimulus
112
+ when :importmap then configure_stimulus_importmap
113
+ when :jsbundling then configure_stimulus_jsbundling
114
+ when :webpacker then configure_stimulus_webpacker
115
+ end
116
+ end
117
+
118
+ def create_initializer
119
+ create_file 'config/initializers/amber_component.rb', <<~RUBY
120
+ # frozen_string_literal: true
121
+
122
+ ::AmberComponent.configure do |c|
123
+ c.stimulus = #{@stimulus.inspect} # #{Configuration::STIMULUS_INTEGRATIONS.to_a}
124
+ c.stylesheet_format = #{@styles.inspect} # #{Configuration::ALLOWED_STYLES.to_a}
125
+ c.view_format = #{@views.inspect} # #{Configuration::ALLOWED_VIEWS.to_a}
126
+ end
127
+ RUBY
128
+ end
129
+
130
+ def stimulus_jsbundling!
131
+ @stimulus = :jsbundling
132
+ end
133
+
134
+ def stimulus_importmap!
135
+ @stimulus = :importmap
136
+ end
137
+
138
+ def stimulus_webpacker!
139
+ @stimulus = :webpacker
140
+ end
141
+
142
+ def configure_stimulus_importmap
143
+ install_importmap
144
+ install_stimulus
145
+ append_file 'config/importmap.rb', <<~RUBY
146
+ pin "@amber_component/stimulus_loading", to: "amber_component/stimulus_loading.js", preload: true
147
+ pin_all_from "app/components"
148
+ RUBY
149
+ append_file 'app/javascript/controllers/index.js', <<~JS
150
+ import { eagerLoadAmberComponentControllers } from "@amber_component/stimulus_loading"
151
+ eagerLoadAmberComponentControllers(application)
152
+ JS
153
+ append_file 'app/assets/config/manifest.js', %(//= link_tree ../../components .js\n)
154
+ end
155
+
156
+ def configure_stimulus_jsbundling
157
+ install_stimulus
158
+ append_file 'app/javascript/application.js', %(import "./controllers/components"\n)
159
+ create_file 'app/javascript/controllers/components.js', <<~JS
160
+ // This file has been created by `amber_component` and will
161
+ // register all stimulus controllers from your components
162
+ import { application } from "./application"
163
+ JS
164
+ end
165
+
166
+ def configure_stimulus_webpacker
167
+ install_stimulus
168
+ append_file 'app/javascript/packs/application.js', %(import "controllers"\n)
169
+ append_file 'app/javascript/controllers/index.js', %(import "./components"\n)
170
+ create_file 'app/javascript/controllers/components.js', <<~JS
171
+ // This file has been created by `amber_component` and will
172
+ // register all stimulus controllers from your components
173
+ import { application } from "./application"
174
+ JS
175
+ end
176
+
177
+ # @return [void]
178
+ def install_importmap
179
+ return if ::File.exist?('config/importmap.rb') && defined?(::Importmap)
180
+
181
+ unless defined?(::Importmap)
182
+ system 'gem install importmap-rails'
183
+ gem 'importmap-rails'
184
+ system 'bundle install'
185
+ end
186
+ rake 'importmap:install'
187
+ end
188
+
189
+ # @return [void]
190
+ def install_stimulus
191
+ return if defined?(::Stimulus)
192
+
193
+ system 'gem install stimulus-rails'
194
+ gem 'stimulus-rails'
195
+ system 'bundle install'
196
+ rake 'stimulus:install'
197
+ end
198
+
30
199
  # @param file_name [String]
31
200
  # @return [void]
32
201
  def require_components_css_in(file_name)
@@ -7,41 +7,36 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
7
7
  desc 'Generate a new component'
8
8
  source_root ::File.expand_path('templates', __dir__)
9
9
 
10
- # @return [Array<Symbol>]
11
- VIEW_FORMATS = %i[html erb haml slim].freeze
12
- # @return [Array<Symbol>]
13
- STYLE_FORMATS = %i[css scss sass].freeze
14
-
15
10
  class_option :view,
16
11
  aliases: ['-v'],
17
- desc: "Indicate what type of view should be generated eg. #{VIEW_FORMATS}"
12
+ desc: "Indicate what type of view should be generated " \
13
+ "eg. #{::AmberComponent::Configuration::ALLOWED_VIEWS}"
18
14
 
19
15
  class_option :css,
20
- aliases: ['--style', '-c'],
21
- desc: "Indicate what type of styles should be generated eg. #{STYLE_FORMATS}"
16
+ aliases: ['--styles', '-c'],
17
+ desc: "Indicate what type of styles should be generated " \
18
+ "eg. #{::AmberComponent::Configuration::ALLOWED_STYLES}"
22
19
 
23
20
  def generate_component
24
- @view_format = (options[:view] || :html).to_sym
25
- @view_format = :html if @view_format == :erb
26
-
27
- @style_format = options[:css]&.to_sym
21
+ set_view_format
22
+ set_stylesheet_format
28
23
 
29
- unless VIEW_FORMATS.include? @view_format
30
- puts "No such view format as `#{@view_format}`"
31
- return
24
+ unless ::AmberComponent::Configuration::ALLOWED_VIEWS.include? @view_format
25
+ raise ::ArgumentError, "No such view format as `#{@view_format}`"
32
26
  end
33
27
 
34
- if !@style_format.nil? && STYLE_FORMATS.include?(@style_format)
35
- puts "No such css/style format as `#{@style_format}`"
36
- return
28
+ unless ::AmberComponent::Configuration::ALLOWED_STYLES.include?(@stylesheet_format)
29
+ raise ::ArgumentError, "No such css/style format as `#{@stylesheet_format}`"
37
30
  end
38
31
 
39
32
  template 'component.rb.erb', "app/components/#{file_path}.rb"
40
33
  template 'component_test.rb.erb', "test/components/#{file_path}_test.rb"
41
34
  create_stylesheet
42
35
  create_view
36
+ create_stimulus_controller
43
37
  end
44
38
 
39
+ # @return [String]
45
40
  def file_name
46
41
  name = super
47
42
  return name if name.end_with? '_component'
@@ -51,6 +46,37 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
51
46
 
52
47
  private
53
48
 
49
+ def set_view_format
50
+ @view_format = options[:view]&.to_sym || ::AmberComponent.configuration.view_format || :erb
51
+ end
52
+
53
+ def set_stylesheet_format
54
+ @stylesheet_format = options[:style]&.to_sym || ::AmberComponent.configuration.stylesheet_format || :css
55
+ end
56
+
57
+ # @return [Boolean]
58
+ def stimulus?
59
+ ::AmberComponent.configuration.stimulus?
60
+ end
61
+
62
+ # @return [Boolean]
63
+ def stimulus_importmap?
64
+ ::AmberComponent.configuration.stimulus_importmap?
65
+ end
66
+
67
+ # @return [void]
68
+ def create_stimulus_controller
69
+ return unless stimulus?
70
+
71
+ template 'controller.js.erb', "app/components/#{file_path}/controller.js"
72
+ return if stimulus_importmap?
73
+
74
+ append_file 'app/javascript/controllers/components.js', <<~JS
75
+ import #{stimulus_controller_class_name} from "../../components/#{file_path}/controller"
76
+ application.register("#{stimulus_controller_id}", #{stimulus_controller_class_name})
77
+ JS
78
+ end
79
+
54
80
  # @return [void]
55
81
  def create_view
56
82
  case @view_format
@@ -65,12 +91,23 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
65
91
 
66
92
  # @return [void]
67
93
  def create_stylesheet
68
- if (@style_format.nil? && defined?(::SassC)) || @style_format == :scss
94
+ case @stylesheet_format
95
+ when :scss
69
96
  template 'style.scss.erb', "app/components/#{file_path}/style.scss"
70
- elsif @style_format == :sass
97
+ when :sass
71
98
  template 'style.sass.erb', "app/components/#{file_path}/style.sass"
72
99
  else
73
100
  template 'style.css.erb', "app/components/#{file_path}/style.css"
74
101
  end
75
102
  end
103
+
104
+ # @return [String]
105
+ def stimulus_controller_id
106
+ file_path.gsub('_', '-').gsub('/', '--')
107
+ end
108
+
109
+ # @return [String]
110
+ def stimulus_controller_class_name
111
+ file_path.gsub('/', '_').camelize
112
+ end
76
113
  end
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Read more about Stimulus here https://stimulus.hotwired.dev/
4
+ export default class extends Controller {
5
+ connect() {
6
+ console.log("Stimulus controller '<%= stimulus_controller_id %>' is connected!")
7
+ }
8
+
9
+ greet() {
10
+ alert("Hi there!")
11
+ }
12
+ }
@@ -1,9 +1,13 @@
1
- .<%= singular_table_name %>
1
+ .<%= singular_table_name %><%= %({ data: { controller: "#{stimulus_controller_id}" } }) if stimulus? %>
2
2
  %h1
3
3
  Hello from
4
4
  %b
5
5
  <%= class_name %>
6
6
  , initialized at:
7
7
  = @time
8
+ <%- if stimulus? %>
9
+ %button{ data: { action: "click-><%= stimulus_controller_id %>#greet" }
10
+ Greet me
11
+ <% end %>
8
12
  %p
9
13
  = description
@@ -1,7 +1,10 @@
1
- <div class='<%= singular_table_name %>'>
1
+ <div class="<%= singular_table_name %>"<%= %( data-controller="#{stimulus_controller_id}") if stimulus? %>>
2
2
  <h1>
3
3
  Hello from <b><%= class_name %></b>, initialized at: <%%= @time %>
4
4
  </h1>
5
+ <%- if stimulus? %>
6
+ <button data-action="click-><%= stimulus_controller_id %>#greet">Greet me</button>
7
+ <% end %>
5
8
  <p>
6
9
  <%%= description %>
7
10
  </p>
@@ -1,6 +1,10 @@
1
- div.<%= singular_table_name %>
1
+ div.<%= singular_table_name %><%= %( data-controller="#{stimulus_controller_id}") if stimulus? %>
2
2
  h1
3
3
  | Hello from
4
4
  b <%= class_name %>
5
5
  | , initialized at: #{@time}
6
+ <%- if stimulus? %>
7
+ button data-action="click-><%= stimulus_controller_id %>#greet"
8
+ | Greet me
9
+ <% end %>
6
10
  p = description
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amber_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruby-Amber
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2022-11-07 00:00:00.000000000 Z
13
+ date: 2022-11-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: actionview
@@ -106,11 +106,13 @@ files:
106
106
  - README.md
107
107
  - Rakefile
108
108
  - amber_component.gemspec
109
+ - assets/javascripts/amber_component/stimulus_loading.js
109
110
  - banner.png
110
111
  - icon.png
111
112
  - lib/amber_component.rb
112
113
  - lib/amber_component/assets.rb
113
114
  - lib/amber_component/base.rb
115
+ - lib/amber_component/configuration.rb
114
116
  - lib/amber_component/helpers.rb
115
117
  - lib/amber_component/helpers/class_helper.rb
116
118
  - lib/amber_component/helpers/component_helper.rb
@@ -133,6 +135,7 @@ files:
133
135
  - lib/generators/component_generator.rb
134
136
  - lib/generators/templates/component.rb.erb
135
137
  - lib/generators/templates/component_test.rb.erb
138
+ - lib/generators/templates/controller.js.erb
136
139
  - lib/generators/templates/style.css.erb
137
140
  - lib/generators/templates/style.sass.erb
138
141
  - lib/generators/templates/style.scss.erb