amber_component 0.0.5 → 1.1.0

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: f1809ef804fefeb8ed7e05b8d1056895f7068663b92b74b464b99bb611981e7c
4
- data.tar.gz: 7a05630fb2c74850241378c1b2e176373258b0e3a63aa7eae26803fc0a7077d5
3
+ metadata.gz: 3fe50a4aaf1482572196e8ae5fd9fd7e2f04ef1ee07b9b5275b0454623e54a6d
4
+ data.tar.gz: 17c1e2848263efac38a029621d26a8d2eae5b4b3d47d2c9eaf8e040b14fe3eae
5
5
  SHA512:
6
- metadata.gz: b554c4cbaafe3fd6e4cbebe34c4e99ea7d318e50aa38b599b87c35d72bf5a353e0099b0471d643543c6f6227275295f63c0f33432940d7d0de71b8ac29ddd488
7
- data.tar.gz: 69ef4f133acd94753d9108b56ef44f601fd59435a53dadaf194905190f53db8dcc368dcd3c5da68cfcc027a8a27a4b6a33d99f40b3bb3985cf3c80ce9c79e8bc
6
+ metadata.gz: e566cb7199d6d8c890081557ed20ac02410085992b036b6a603e909a62ac6fb6af0e853910ab02ac087fd8e3bcaf398d59496e7172ac1233d843206785b15509
7
+ data.tar.gz: 3b8bb6257ba09f3f6fbf98c68a128ab99adfa289e8ee3d674de744052b4b69f7b06c0e5f43a5e7bb4ae54533a78728fea15ba2534b947b4e730869b5e09b8932
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- amber_component (0.0.5)
4
+ amber_component (1.1.0)
5
5
  actionview (>= 6)
6
6
  activemodel (>= 6)
7
7
  activesupport (>= 6)
@@ -94,21 +94,21 @@ GEM
94
94
  rainbow (3.1.1)
95
95
  rake (13.0.6)
96
96
  rchardet (1.8.0)
97
- regexp_parser (2.5.0)
97
+ regexp_parser (2.6.0)
98
98
  reverse_markdown (2.1.1)
99
99
  nokogiri
100
100
  rexml (3.2.5)
101
- rubocop (1.36.0)
101
+ rubocop (1.38.0)
102
102
  json (~> 2.3)
103
103
  parallel (~> 1.10)
104
104
  parser (>= 3.1.2.1)
105
105
  rainbow (>= 2.2.2, < 4.0)
106
106
  regexp_parser (>= 1.8, < 3.0)
107
107
  rexml (>= 3.2.5, < 4.0)
108
- rubocop-ast (>= 1.20.1, < 2.0)
108
+ rubocop-ast (>= 1.23.0, < 2.0)
109
109
  ruby-progressbar (~> 1.7)
110
110
  unicode-display_width (>= 1.4.0, < 3.0)
111
- rubocop-ast (1.21.0)
111
+ rubocop-ast (1.23.0)
112
112
  parser (>= 3.1.1.0)
113
113
  ruby-progressbar (1.11.0)
114
114
  ruby2_keywords (0.0.5)
@@ -124,7 +124,7 @@ GEM
124
124
  simplecov (~> 0.19)
125
125
  simplecov-html (0.12.3)
126
126
  simplecov_json_formatter (0.1.4)
127
- solargraph (0.46.0)
127
+ solargraph (0.47.2)
128
128
  backport (~> 1.2)
129
129
  benchmark
130
130
  bundler (>= 1.17.2)
data/README.md CHANGED
@@ -9,7 +9,7 @@
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,37 @@ 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
+ ```
58
+ app/components/
59
+ ├─ [name]_component.rb
60
+ └─ [name]_component/
61
+ ├─ style.css # may be .sass or .scss
62
+ ├─ view.html.erb
63
+ └─ controller.js # if stimulus is configured
64
+ test/components/
65
+ └─ [name]_component_test.rb
66
+ ```
46
67
 
47
68
  An individual component which implements a button may look like this.
48
69
 
@@ -57,7 +78,9 @@ end
57
78
  ```html
58
79
  <!-- app/components/button_component/view.html.erb -->
59
80
 
60
- <div class="button_component">
81
+ <div class="button_component"
82
+ data-controller="button-component"
83
+ data-action="click->button-component#greet">
61
84
  <%= label %>
62
85
  </div>
63
86
  ```
@@ -76,6 +99,25 @@ end
76
99
  }
77
100
  ```
78
101
 
102
+ If you used the `--stimulus` option when installing the gem, a JS controller will be generated as well.
103
+ ```js
104
+ // app/components/button_component/controller.js
105
+
106
+ import { Controller } from "@hotwired/stimulus"
107
+
108
+ // Read more about Stimulus here https://stimulus.hotwired.dev/
109
+ export default class extends Controller {
110
+ connect() {
111
+ console.log("Stimulus controller 'button-component' is connected!")
112
+ }
113
+
114
+ greet() {
115
+ alert("Hi there!")
116
+ }
117
+ }
118
+
119
+ ```
120
+
79
121
  You can render this component in other components or in a Rails view.
80
122
 
81
123
  ```html
@@ -97,34 +139,69 @@ ButtonComponent.call label: 'Click me!'
97
139
  #=> '<div class="button_component">Click me!</div>'
98
140
  ```
99
141
 
100
- ### Rails helpers inside component templates
142
+ ### Components with namespaces
101
143
 
102
- Component views/template files can make use
103
- of all ActionView helpers and Rails route helpers.
144
+ Components may be defined inside multiple modules/namespaces.
104
145
 
105
- This makes component views very flexible and convenient.
146
+ ```ruby
147
+ # app/components/sign_up/button_component.rb
106
148
 
107
- ```erb
108
- <!-- app/components/login_form_component/view.html.erb -->
149
+ class SignUp::ButtonComponent < AmberComponent::Base
150
+ prop :label, required: true
151
+ end
152
+ ```
109
153
 
110
- <%= form_with url: sign_up_path, class: "login_form_component" do |f| %>
111
- <%= f.label :first_name %>
112
- <%= f.text_field :first_name %>
154
+ ```html
155
+ <!-- app/components/sign_up/button_component/view.html.erb -->
113
156
 
114
- <%= f.label :last_name %>
115
- <%= f.text_field :last_name %>
157
+ <div class="sign_up_button_component">
158
+ <%= label %>
159
+ </div>
160
+ ```
116
161
 
117
- <%= f.label :email, "Email Address" %>
118
- <%= f.text_field :email %>
162
+ ```scss
163
+ // app/components/sign_up/button_component/style.scss
119
164
 
120
- <%= f.label :password %>
121
- <%= f.password_field :password %>
165
+ .sign_up_button_component {
166
+ background-color: indigo;
167
+ border-radius: 1rem;
168
+ transition-duration: 500ms;
122
169
 
123
- <%= f.label :password_confirmation, "Confirm Password" %>
124
- <%= f.password_field :password_confirmation %>
170
+ &:hover {
171
+ background-color: blue;
172
+ }
173
+ }
174
+ ```
125
175
 
126
- <%= f.submit "Create account" %>
127
- <% end %>
176
+ You can render such a component by calling the `::call` method
177
+ on its class, or by using the helper method defined on its parent module.
178
+
179
+ ```ruby
180
+ SignUp::ButtonComponent.call label: 'Sign up!'
181
+ SignUp.button_component label: 'Sign up!'
182
+ ```
183
+
184
+ ### Generating Components
185
+
186
+ You can generate new components by running
187
+
188
+ ```sh
189
+ $ bin/rails generate component [name]
190
+ ```
191
+
192
+ Name of the component may be PascalCased like `FooBar` or snake_cased `foo_bar`
193
+
194
+ 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).
195
+
196
+ ```
197
+ app/components/
198
+ ├─ [name]_component.rb
199
+ └─ [name]_component/
200
+ ├─ style.css
201
+ ├─ view.html.erb
202
+ └─ controller.js # if stimulus is configured
203
+ test/components/
204
+ └─ [name]_component_test.rb
128
205
  ```
129
206
 
130
207
  ### Component properties
@@ -155,39 +232,6 @@ CommentComponent.call body: 'Foo bar', author: User.first
155
232
  comment_component body: 'Foo bar', author: User.first
156
233
  ```
157
234
 
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
235
  ### Helper methods
192
236
 
193
237
  Defining helper methods which are available
@@ -239,6 +283,39 @@ end
239
283
  </div>
240
284
  ```
241
285
 
286
+ ### Overriding prop getters and setters
287
+
288
+ Getters and setters for properties are
289
+ defined in a module which means that you can override them and call them with `super`.
290
+
291
+ ```ruby
292
+ # app/components/priority_icon_component.rb
293
+
294
+ class PriorityIconComponent < ApplicationComponent
295
+ PriorityStruct = Struct.new :icon, :color
296
+
297
+ PRIORITY_MAP = {
298
+ low: PriorityStruct.new('fa-solid fa-chevrons-down', 'green'),
299
+ medium: PriorityStruct.new('fa-solid fa-chevron-up', 'yellow'),
300
+ high: PriorityStruct.new('fa-solid fa-chevrons-up', 'red')
301
+ }
302
+
303
+ prop :severity, default: -> { :low }
304
+
305
+ def severity=(val)
306
+ # super will call the original
307
+ # implementation of the setter
308
+ super(PRIORITY_MAP[val])
309
+ end
310
+ end
311
+ ```
312
+
313
+ ```html
314
+ <!-- app/components/priority_icon_component/view.html.erb -->
315
+
316
+ <i style="color: <%= severity&.color %>;" class="<%= severity&.icon %>"></i>
317
+ ```
318
+
242
319
  ### Nested components
243
320
 
244
321
  It's possible to nest components or provide
@@ -283,12 +360,14 @@ a block.
283
360
  <!-- app/controller/tasks/show.html.erb -->
284
361
 
285
362
  <%= ModalComponent.call id: 'update-task-modal' title: 'Update the task' do %>
363
+ <!-- You can provide HTML and render other components -->
286
364
  <h2>This is your task!</h2>
287
365
  <%= form_with model: @task do |f| %>
288
366
  <%= f.text_field :name %>
289
367
  <%= f.text_area :description %>
290
368
  <%= f.submit %>
291
369
  <% end %>
370
+ <%= OtherComponent.call some: 'prop' %>
292
371
  <% end %>
293
372
  ```
294
373
 
@@ -299,64 +378,131 @@ only when it is present (will work without nested content)
299
378
  you can use `yield.html_safe if block_given?`
300
379
 
301
380
  In general `block_given?` will return `true` when a block/nested content is present, otherwise `false`.
381
+ You can use it to render content conditionally based on
382
+ whether nested content is present.
302
383
 
303
- ### Components with namespaces
384
+ ### Rails helpers inside component templates
304
385
 
305
- Components may be defined inside multiple modules/namespaces.
386
+ Component views/template files can make use
387
+ of all ActionView helpers and Rails route helpers.
306
388
 
307
- ```ruby
308
- # app/components/sign_up/button_component.rb
389
+ This makes component views very flexible and convenient.
309
390
 
310
- class SignUp::ButtonComponent < AmberComponent::Base
311
- prop :label, required: true
312
- end
313
- ```
391
+ ```erb
392
+ <!-- app/components/login_form_component/view.html.erb -->
314
393
 
315
- ```html
316
- <!-- app/components/sign_up/button_component/view.html.erb -->
394
+ <%= form_with url: sign_up_path, class: "login_form_component" do |f| %>
395
+ <%= f.label :first_name %>
396
+ <%= f.text_field :first_name %>
317
397
 
318
- <div class="sign_up_button_component">
319
- <%= label %>
320
- </div>
321
- ```
398
+ <%= f.label :last_name %>
399
+ <%= f.text_field :last_name %>
322
400
 
323
- ```scss
324
- // app/components/sign_up/button_component/style.scss
401
+ <%= f.label :email, "Email Address" %>
402
+ <%= f.text_field :email %>
325
403
 
326
- .sign_up_button_component {
327
- background-color: indigo;
328
- border-radius: 1rem;
329
- transition-duration: 500ms;
404
+ <%= f.label :password %>
405
+ <%= f.password_field :password %>
330
406
 
331
- &:hover {
332
- background-color: blue;
333
- }
334
- }
407
+ <%= f.label :password_confirmation, "Confirm Password" %>
408
+ <%= f.password_field :password_confirmation %>
409
+
410
+ <%= f.submit "Create account" %>
411
+ <% end %>
335
412
  ```
336
413
 
337
- You can render such a component by calling the `::call` method
338
- on its class, or by using the helper method defined on its parent module.
414
+ ### Testing Components
415
+
416
+ ### Rails
417
+
418
+ After setting up this gem with the rails generator
419
+ `rails generate amber_component:install` a new abstract
420
+ test class will be available called `ApplicationComponentTestCase`.
421
+
422
+ It provides a handful of helper methods to make it
423
+ easier to inspect the rendered HTML.
424
+
425
+ A simple test file may look like this:
339
426
 
340
427
  ```ruby
341
- SignUp::ButtonComponent.call label: 'Sign up!'
342
- SignUp.button_component label: 'Sign up!'
428
+ # test/components/foo_component_test.rb
429
+
430
+ require 'test_helper'
431
+
432
+ class FooComponentTest < ApplicationComponentTestCase
433
+ test 'render correct HTML' do
434
+ # Specify what the assertions are supposed to
435
+ # check against.
436
+ #
437
+ # There can be multiple renders in one test
438
+ # but they override the previous one.
439
+ # So there is only one rendered component
440
+ # at any given time.
441
+ render do
442
+ FooComponent.call some: 'prop'
443
+ end
444
+
445
+ # Assertions on the rendered HTML
446
+
447
+ # Use a CSS selector
448
+ assert_selector ".foo_component span.my_class", text: 'Some Text'
449
+ # Check text
450
+ assert_text 'Amber Component is awesome!'
451
+ end
452
+ end
343
453
  ```
344
454
 
345
- ### Generating Components
455
+ A full list of available assertions can be found [here](https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Matchers).
346
456
 
347
- You an generate new components by running
457
+ ### Non Rails
348
458
 
349
- ```sh
350
- $ bin/rails generate component foo_bar
459
+ There is a test case class for minitest. You can
460
+ access it by requiring `'amber_component/minitest_test_case'`.
461
+
462
+ It has the same assertion methods as the Rails test case class.
463
+ It requires [capybara](https://github.com/teamcapybara/capybara) to be installed and present in the Gemfile.
464
+
465
+ A full list of available assertions can be found [here](https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Matchers).
466
+
467
+ ```ruby
468
+ require 'amber_component/minitest_test_case'
469
+
470
+ class FooComponentTest < AmberComponent::MinitestTestCase
471
+ def test_render_correct_html
472
+ # Specify what the assertions are supposed to
473
+ # check against.
474
+ #
475
+ # There can be multiple renders in one test
476
+ # but they override the previous one.
477
+ # So there is only one rendered component
478
+ # at any given time.
479
+ render do
480
+ FooComponent.call some: 'prop'
481
+ end
482
+
483
+ # Assertions on the rendered HTML
484
+
485
+ # Use a CSS selector
486
+ assert_selector ".foo_component span.my_class", text: 'Some Text'
487
+ # Check text
488
+ assert_text 'Amber Component is awesome!'
489
+ end
490
+ end
351
491
  ```
352
492
 
353
- or
493
+ There is also a helper module which provides all of these assertions
494
+ under `'amber_component/test_helper'`.
354
495
 
355
- ```sh
356
- $ bin/rails generate component FooBar
496
+ ```ruby
497
+ require 'amber_component/test_helper'
498
+
499
+ class MyAbstractTestCase < ::Minitest::Test
500
+ include ::AmberComponent::TestHelper
501
+ end
357
502
  ```
358
503
 
359
- This will generate a new component in `app/components/foo_bar_component.rb` along with a view, stylesheet and test file.
504
+ Note that this module has only been tested with minitest and rails test suites,
505
+ so it may require overriding or implementing a few methods to work with other test suites.
360
506
 
361
507
  ## Contribute
362
508
 
@@ -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
+ }
data/banner.png CHANGED
Binary file
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Object which stores configuration options
5
+ # for this gem.
6
+ class Configuration
7
+ # @return [Array<Symbol>]
8
+ STIMULUS_INTEGRATIONS = %i[importmap jsbundling webpack esbuild rollup].freeze
9
+
10
+ # How Stimulus.js is bundled in this app.
11
+ # Possible values: `[nil, :importmap, :jsbundling, :webpack, :esbuild, :rollup]`
12
+ # `nil` indicates that stimulus should not be used (default behaviour).
13
+ #
14
+ # @return [Symbol, nil]
15
+ attr_reader :stimulus
16
+
17
+ # How Stimulus.js is bundled in this app.
18
+ # Possible values: `[nil, :importmap, :jsbundling, :webpack, :esbuild, :rollup]`
19
+ # `nil` indicates that stimulus should not be used (default behaviour).
20
+ #
21
+ # @param val [Symbol, String, nil]
22
+ def stimulus=(val)
23
+ val = val&.to_sym
24
+ unless val.nil? || STIMULUS_INTEGRATIONS.include?(val)
25
+ raise(::ArgumentError,
26
+ "Invalid value for `stimulus` bundling. " \
27
+ "Received #{val.inspect}, expected one of #{STIMULUS_INTEGRATIONS.inspect}")
28
+ end
29
+
30
+ @stimulus = val
31
+ end
32
+
33
+ # @return [Boolean]
34
+ def stimulus?
35
+ !@stimulus.nil?
36
+ end
37
+
38
+ # @return [Boolean]
39
+ def stimulus_importmap?
40
+ @stimulus == :importmap
41
+ end
42
+ end
43
+ end
@@ -75,7 +75,7 @@ module ::AmberComponent
75
75
  module InstanceMethods
76
76
  private
77
77
 
78
- # @param props [Hash{Symbol => Object}]
78
+ # @param kwargs [Hash{Symbol => Object}]
79
79
  def initialize(**kwargs)
80
80
  bind_props(kwargs)
81
81
  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 = '0.0.5'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -2,7 +2,11 @@
2
2
 
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext'
5
+ require 'pathname'
5
6
 
7
+ require_relative 'amber_component/configuration'
8
+
9
+ # Root module of the `amber_component` gem.
6
10
  module ::AmberComponent
7
11
  class Error < ::StandardError; end
8
12
  class MissingPropsError < Error; end
@@ -13,6 +17,22 @@ module ::AmberComponent
13
17
  class EmptyViewError < Error; end
14
18
  class UnknownViewTypeError < Error; end
15
19
  class MultipleViewsError < Error; end
20
+
21
+ # @return [Pathname]
22
+ ROOT_GEM_PATH = ::Pathname.new ::File.expand_path('..', __dir__)
23
+
24
+ class << self
25
+ # @return [Configuration]
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ # @yieldparam [Configuration]
31
+ # @return [void]
32
+ def configure
33
+ yield configuration
34
+ end
35
+ end
16
36
  end
17
37
 
18
38
  require_relative 'amber_component/version'
@@ -10,8 +10,14 @@ 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
+ # @return [Array<Symbol>]
14
+ STIMULUS_INTEGRATIONS = %i[stimulus importmap jsbundling webpack esbuild rollup].freeze
15
+
16
+ class_option :stimulus,
17
+ desc: "Configure the app to use Stimulus.js wih components to make them interactive " \
18
+ "[options: importmap (default), jsbundling, webpack, esbuild, rollup]"
19
+
20
+ def setup
15
21
  copy_file 'application_component.rb', 'app/components/application_component.rb'
16
22
  copy_file 'application_component_test_case.rb', 'test/application_component_test_case.rb'
17
23
  append_file 'test/test_helper.rb', "require_relative 'application_component_test_case'"
@@ -23,10 +29,87 @@ module ::AmberComponent
23
29
  require_components_css_in 'app/assets/stylesheets/application.css.sass'
24
30
  require_components_css_in 'app/assets/stylesheets/application.scss.sass'
25
31
  require_components_css_in 'app/assets/stylesheets/application.sass.scss'
32
+ configure_stimulus
26
33
  end
27
34
 
28
35
  private
29
36
 
37
+ def configure_stimulus
38
+ stimulus = options[:stimulus]&.to_sym
39
+ return unless stimulus
40
+
41
+ case stimulus
42
+ when :stimulus
43
+ if defined?(::Jsbundling)
44
+ stimulus_integration = :jsbundling
45
+ configure_stimulus_jsbundling
46
+ else
47
+ stimulus_integration = :importmap
48
+ configure_stimulus_importmap
49
+ end
50
+ when :importmap
51
+ stimulus_integration = :importmap
52
+ configure_stimulus_importmap
53
+ when :jsbundling, :webpack, :esbuild, :rollup
54
+ stimulus_integration = :jsbundling
55
+ configure_stimulus_jsbundling
56
+ end
57
+
58
+ create_file 'config/initializers/amber_component.rb', <<~RUBY
59
+ # frozen_string_literal: true
60
+
61
+ ::AmberComponent.configure do |c|
62
+ c.stimulus = :#{stimulus_integration}
63
+ end
64
+ RUBY
65
+ end
66
+
67
+ def configure_stimulus_importmap
68
+ install_importmap
69
+ install_stimulus
70
+ append_file 'config/importmap.rb', <<~RUBY
71
+ pin "@amber_component/stimulus_loading", to: "amber_component/stimulus_loading.js", preload: true
72
+ pin_all_from "app/components"
73
+ RUBY
74
+ append_file 'app/javascript/controllers/index.js', <<~JS
75
+ import { eagerLoadAmberComponentControllers } from "@amber_component/stimulus_loading"
76
+ eagerLoadAmberComponentControllers(application)
77
+ JS
78
+ append_file 'app/assets/config/manifest.js', %(//= link_tree ../../components .js\n)
79
+ end
80
+
81
+ def configure_stimulus_jsbundling
82
+ install_stimulus
83
+ append_file 'app/javascript/application.js', %(import "./controllers/components"\n)
84
+ create_file 'app/javascript/controllers/components.js', <<~JS
85
+ // This file has been created by `amber_component` and will
86
+ // register all stimulus controllers from your components
87
+ import { application } from "./application"
88
+ JS
89
+ end
90
+
91
+ # @return [void]
92
+ def install_importmap
93
+ return if ::File.exist?('config/importmap.rb') && defined?(::Importmap)
94
+
95
+ unless defined?(::Importmap)
96
+ system 'gem install importmap-rails'
97
+ gem 'importmap-rails'
98
+ system 'bundle install'
99
+ end
100
+ rake 'importmap:install'
101
+ end
102
+
103
+ # @return [void]
104
+ def install_stimulus
105
+ return if defined?(::Stimulus)
106
+
107
+ system 'gem install stimulus-rails'
108
+ gem 'stimulus-rails'
109
+ system 'bundle install'
110
+ rake 'stimulus:install'
111
+ end
112
+
30
113
  # @param file_name [String]
31
114
  # @return [void]
32
115
  def require_components_css_in(file_name)
@@ -40,8 +40,10 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
40
40
  template 'component_test.rb.erb', "test/components/#{file_path}_test.rb"
41
41
  create_stylesheet
42
42
  create_view
43
+ create_stimulus_controller
43
44
  end
44
45
 
46
+ # @return [String]
45
47
  def file_name
46
48
  name = super
47
49
  return name if name.end_with? '_component'
@@ -51,6 +53,29 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
51
53
 
52
54
  private
53
55
 
56
+ # @return [Boolean]
57
+ def stimulus?
58
+ ::AmberComponent.configuration.stimulus?
59
+ end
60
+
61
+ # @return [Boolean]
62
+ def stimulus_importmap?
63
+ ::AmberComponent.configuration.stimulus_importmap?
64
+ end
65
+
66
+ # @return [void]
67
+ def create_stimulus_controller
68
+ return unless stimulus?
69
+
70
+ template 'controller.js.erb', "app/components/#{file_path}/controller.js"
71
+ return if stimulus_importmap?
72
+
73
+ append_file 'app/javascript/controllers/components.js', <<~JS
74
+ import #{stimulus_controller_class_name} from "../../components/#{file_path}/controller"
75
+ application.register("#{stimulus_controller_id}", #{stimulus_controller_class_name})
76
+ JS
77
+ end
78
+
54
79
  # @return [void]
55
80
  def create_view
56
81
  case @view_format
@@ -73,4 +98,14 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
73
98
  template 'style.css.erb', "app/components/#{file_path}/style.css"
74
99
  end
75
100
  end
101
+
102
+ # @return [String]
103
+ def stimulus_controller_id
104
+ file_path.gsub('_', '-').gsub('/', '--')
105
+ end
106
+
107
+ # @return [String]
108
+ def stimulus_controller_class_name
109
+ file_path.gsub('/', '_').camelize
110
+ end
76
111
  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: 0.0.5
4
+ version: 1.1.0
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-13 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