view_component 2.5.0 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 026e6dbe1f96cf8fb5d6127b756a5f78733e5918df2f9785972208866e755fd7
4
- data.tar.gz: cbab7c479e82b0d5445e6bdcfa7c2526e409b302283c601cc21c2df8e32f6ed9
3
+ metadata.gz: bc3f38e0c12dafc80bf6c2c9b74d09d3a3f019ee4d4a9dfa59d1a638a1228898
4
+ data.tar.gz: 48f1df9b4f54a0049b96e37b1ca16de5125ef13163838aa94cba4fb5cb6f8a1f
5
5
  SHA512:
6
- metadata.gz: cd6a8e929dc0c9ac04c1b8ac28ccf600a22b74394af75783a901c1054372ef00c1713ac1f82ea7602633f2f9ad92d499c7ea90310a3351f6e1c7e6da3cc52924
7
- data.tar.gz: dc16afe7a53dff39a9c1713d8ecb8878692dc49b5184da1fc5d733d41b46a91e8caeb9c1c0eae7aa4d396dac0269d563556d3a5932f8f31605ef4ee5b98569fa
6
+ metadata.gz: 7efa511f6cc6b76fed913823eb54d3ec709e3d8ca99adbcbc6cf6a667c407be4885701b68fc174fa2af3908d6da708c77c9cffa58de23f3f11307510e7cceb57
7
+ data.tar.gz: 238f0fd897c7125652cf80df6c59dcaa5d8bf5a1eb39d994a48a876be49cf0c43d5274eed3b2fbcf8915461ef1334dec9f92835b886e152e7eb64ab320b7049f
@@ -1,5 +1,43 @@
1
1
  # master
2
2
 
3
+ # 2.9.0
4
+
5
+ * Cache components per-request in development, preventing unnecessary recompilation during a single request.
6
+
7
+ *Felipe Sateler*
8
+
9
+ # 2.8.0
10
+
11
+ * Add `before_render`, deprecating `before_render_check`.
12
+
13
+ *Joel Hawksley*
14
+
15
+ # 2.7.0
16
+
17
+ * Add `rendered_component` method to `ViewComponent::TestHelpers` which exposes the raw output of the rendered component.
18
+
19
+ *Richard Macklin*
20
+
21
+ * Support sidecar directories for views and other assets.
22
+
23
+ *Jon Palmer*
24
+
25
+ # 2.6.0
26
+
27
+ * Add `config.view_component.preview_route` to set the endpoint for component previews. By default `/rails/view_components` is used.
28
+
29
+ *Juan Manuel Ramallo*
30
+
31
+ * Raise error when initializer omits with_collection_parameter.
32
+
33
+ *Joel Hawksley*
34
+
35
+ # 2.5.1
36
+
37
+ * Compile component before rendering collection.
38
+
39
+ *Rainer Borene*
40
+
3
41
  # v2.5.0
4
42
 
5
43
  * Add counter variables when rendering collections.
data/README.md CHANGED
@@ -1,30 +1,13 @@
1
1
  # ViewComponent
2
- A view component framework for Rails.
3
-
4
- **Current Status**: Used in production at GitHub. Because of this, all changes will be thoroughly vetted, which could slow down the process of contributing. We will do our best to actively communicate status of pull requests with any contributors. If you have any substantial changes that you would like to make, it would be great to first [open an issue](http://github.com/github/view_component/issues/new) to discuss them with us.
5
-
6
- ## Migration from ActionView::Component
7
-
8
- This gem used to be called `ActionView::Component`.
9
- See [issue #206] for some background on the name change.
10
- Learn more about what changed and how to migrate [here][migration-info].
11
-
12
- [issue #206]: https://github.com/github/view_component/issues/206
13
- [migration-info]: https://github.com/github/view_component/blob/v2.0.0/README.md#migration-in-progress
14
-
15
- ## Roadmap
16
-
17
- Support for third-party component frameworks was merged into Rails `6.1.0.alpha` in https://github.com/rails/rails/pull/36388 and https://github.com/rails/rails/pull/37919. Our goal with this project is to provide a first-class component framework for this new capability in Rails.
18
-
19
- This gem includes a backport of those changes for Rails `5.0.0` through `6.1.0.alpha`.
2
+ ViewComponent is a framework for building view components that are reusable, testable & encapsulated, in Ruby on Rails.
20
3
 
21
4
  ## Design philosophy
22
-
23
- This library is designed to integrate as seamlessly as possible with Rails, with the [least surprise](https://www.artima.com/intv/ruby4.html).
5
+ ViewComponent is designed to integrate as seamlessly as possible [with Rails](https://rubyonrails.org/doctrine/), with the [least surprise](https://www.artima.com/intv/ruby4.html).
24
6
 
25
7
  ## Compatibility
8
+ ViewComponent is [supported natively](https://edgeguides.rubyonrails.org/layouts_and_rendering.html#rendering-objects) in Rails 6.1, and compatible with Rails 5.0+ via an included [monkey patch](https://github.com/github/view_component/blob/master/lib/view_component/render_monkey_patch.rb).
26
9
 
27
- `view_component` is tested for compatibility with combinations of Ruby `2.4`/`2.5`/`2.6`/`2.7` and Rails `5.0.0`/`5.2.3`/`6.0.0`/`master`.
10
+ ViewComponent is tested for compatibility [with combinations of](https://github.com/github/view_component/blob/22e3d4ccce70d8f32c7375e5a5ccc3f70b22a703/.github/workflows/ruby_on_rails.yml#L10-L11) Ruby 2.4+ and Rails 5+.
28
11
 
29
12
  ## Installation
30
13
 
@@ -44,7 +27,7 @@ require "view_component/engine"
44
27
 
45
28
  ### What are components?
46
29
 
47
- `ViewComponent`s are Ruby classes that are used to render views. They take data as input and return output-safe HTML. Think of them as an evolution of the presenter/decorator/view model pattern, inspired by [React Components](https://reactjs.org/docs/react-component.html).
30
+ ViewComponents are Ruby objects that output HTML. Think of them as an evolution of the presenter pattern, inspired by [React](https://reactjs.org/docs/react-component.html).
48
31
 
49
32
  Components are most effective in cases where view code is reused or benefits from being tested directly.
50
33
 
@@ -52,47 +35,39 @@ Components are most effective in cases where view code is reused or benefits fro
52
35
 
53
36
  #### Testing
54
37
 
55
- Rails encourages testing views with integration tests. This discourages us from testing views thoroughly, due to the overhead of exercising the routing and controller layers in addition to the view.
38
+ Unlike traditional Rails views, ViewComponents can be unit-tested. In the GitHub codebase, component unit tests take around 25 milliseconds each, compared to about six seconds for controller tests.
56
39
 
57
- For partials, this means being tested for each view they are included in, reducing the benefit of reusing them.
40
+ Rails views are typically tested with slow integration tests that also exercise the routing and controller layers in addition to the view. This cost often discourages thorough test coverage.
58
41
 
59
- `ViewComponent`s can be unit-tested. In the GitHub codebase, our component unit tests run in around 25 milliseconds, compared to about six seconds for integration tests.
42
+ With ViewComponent, integration tests can be reserved for end-to-end assertions, with permutations and corner cases covered at the unit level.
60
43
 
61
44
  #### Data Flow
62
45
 
63
- Unlike a method declaration on an object, views do not declare the values they are expected to receive, making it hard to figure out what context is necessary to render them. This often leads to subtle bugs when reusing a view in different contexts.
46
+ Traditional Rails views have an implicit interface, making it hard to reason about what information is needed to render, leading to subtle bugs when rendering the same view in different contexts.
64
47
 
65
- By clearly defining the context necessary to render a `ViewComponent`, they're easier to reuse than partials.
48
+ ViewComponents use a standard Ruby initializer that clearly defines what is needed to render, making them easier (and safer) to reuse than partials.
66
49
 
67
50
  #### Standards
68
51
 
69
52
  Views often fail basic Ruby code quality standards: long methods, deep conditional nesting, and mystery guests abound.
70
53
 
71
- `ViewComponent`s are Ruby objects, making it easy to follow code quality standards.
72
-
73
- #### Code Coverage
74
-
75
- Many common Ruby code coverage tools cannot properly handle coverage of views, making it difficult to audit how thorough tests are and leading to missing coverage in test suites.
76
-
77
- `ViewComponent` is at least partially compatible with code coverage tools, such as SimpleCov.
54
+ ViewComponents are Ruby objects, making it easy to follow (and enforce) code quality standards.
78
55
 
79
56
  ### Building components
80
57
 
81
58
  #### Conventions
82
59
 
83
- Components are subclasses of `ViewComponent::Base` and live in `app/components`. It's recommended to create an `ApplicationComponent` that is a subclass of `ViewComponent::Base` and inherit from that instead.
60
+ Components are subclasses of `ViewComponent::Base` and live in `app/components`. It's common practice to create and inherit from an `ApplicationComponent` that is a subclass of `ViewComponent::Base`.
84
61
 
85
- Component class names end in -`Component`.
62
+ Component names end in -`Component`.
86
63
 
87
- Component module names are plural, as they are for controllers. (`Users::AvatarComponent`)
88
-
89
- Content passed to a `ViewComponent` as a block is captured and assigned to the `content` accessor.
64
+ Component module names are plural, as for controllers and jobs: `Users::AvatarComponent`
90
65
 
91
66
  #### Quick start
92
67
 
93
- Use the component generator to create a new `ViewComponent`.
68
+ Use the component generator to create a new ViewComponent.
94
69
 
95
- The generator accepts the component name and the list of accepted properties as arguments:
70
+ The generator accepts a component name and a list of arguments:
96
71
 
97
72
  ```bash
98
73
  bin/rails generate component Example title content
@@ -102,7 +77,7 @@ bin/rails generate component Example title content
102
77
  create app/components/example_component.html.erb
103
78
  ```
104
79
 
105
- `ViewComponent` includes template generators for the `erb`, `haml`, and `slim` template engines and will use the template engine specified in the Rails configuration (`config.generators.template_engine`) by default.
80
+ ViewComponent includes template generators for the `erb`, `haml`, and `slim` template engines and will default to the template engine specified in `config.generators.template_engine`.
106
81
 
107
82
  The template engine can also be passed as an option to the generator:
108
83
 
@@ -112,7 +87,7 @@ bin/rails generate component Example title content --template-engine slim
112
87
 
113
88
  #### Implementation
114
89
 
115
- A `ViewComponent` is a Ruby file and corresponding template file with the same base name:
90
+ A ViewComponent is a Ruby file and corresponding template file with the same base name:
116
91
 
117
92
  `app/components/test_component.rb`:
118
93
  ```ruby
@@ -128,7 +103,7 @@ end
128
103
  <span title="<%= @title %>"><%= content %></span>
129
104
  ```
130
105
 
131
- Which is rendered in a view as:
106
+ Rendered in a view as:
132
107
 
133
108
  ```erb
134
109
  <%= render(TestComponent.new(title: "my title")) do %>
@@ -136,7 +111,7 @@ Which is rendered in a view as:
136
111
  <% end %>
137
112
  ```
138
113
 
139
- Which returns:
114
+ Returning:
140
115
 
141
116
  ```html
142
117
  <span title="my title">Hello, World!</span>
@@ -144,7 +119,9 @@ Which returns:
144
119
 
145
120
  #### Content Areas
146
121
 
147
- A component can declare additional content areas to be rendered in the component. For example:
122
+ Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor.
123
+
124
+ ViewComponents can declare additional content areas. For example:
148
125
 
149
126
  `app/components/modal_component.rb`:
150
127
  ```ruby
@@ -161,7 +138,7 @@ end
161
138
  </div>
162
139
  ```
163
140
 
164
- Which is rendered in a view as:
141
+ Rendered in a view as:
165
142
 
166
143
  ```erb
167
144
  <%= render(ModalComponent.new) do |component| %>
@@ -174,7 +151,7 @@ Which is rendered in a view as:
174
151
  <% end %>
175
152
  ```
176
153
 
177
- Which returns:
154
+ Returning:
178
155
 
179
156
  ```html
180
157
  <div class="modal">
@@ -185,10 +162,9 @@ Which returns:
185
162
 
186
163
  ### Inline Component
187
164
 
188
- A component can be rendered without any template file as well.
165
+ ViewComponents can render without a template file, by defining a `call` method:
189
166
 
190
167
  `app/components/inline_component.rb`:
191
-
192
168
  ```ruby
193
169
  class InlineComponent < ViewComponent::Base
194
170
  def call
@@ -201,34 +177,62 @@ class InlineComponent < ViewComponent::Base
201
177
  end
202
178
  ```
203
179
 
204
- It is also possible to render variants inline by creating additional `call_` methods.
180
+ It is also possible to define methods for variants:
205
181
 
206
182
  ```ruby
207
183
  class InlineVariantComponent < ViewComponent::Base
208
- def call
209
- link_to "Default", default_path
210
- end
211
-
212
184
  def call_phone
213
185
  link_to "Phone", phone_path
214
186
  end
187
+
188
+ def call
189
+ link_to "Default", default_path
190
+ end
215
191
  end
216
192
  ```
217
193
 
218
- Using a mixture of templates and inline render methods in a component is supported, however only one should be provided per component (or variant).
194
+ ### Sidecar Assets
195
+
196
+ ViewComponents supports two options for defining view files.
197
+
198
+ #### Sidecar view
199
+
200
+ The simplest option is to place the view next to the Ruby component:
201
+
202
+ ```
203
+ app/components
204
+ ├── ...
205
+ ├── test_component.rb
206
+ ├── test_component.html.erb
207
+ ├── ...
208
+ ```
209
+
210
+ #### Sidecar directory
211
+
212
+ As an alternative, views and other assets can be placed in a sidecar directory with the same name as the component, which can be useful for organizing views alongside other assets like Javascript and CSS.
213
+
214
+ ```
215
+ app/components
216
+ ├── ...
217
+ ├── test_component.rb
218
+ ├── test_component
219
+ | ├── test_component.css
220
+ | ├── test_component.html.erb
221
+ | └── test_component.js
222
+ ├── ...
223
+
224
+ ```
219
225
 
220
226
  ### Conditional Rendering
221
227
 
222
- Components can implement a `#render?` method to determine if they should be rendered.
228
+ Components can implement a `#render?` method to be called after initialization to determine if the component should render.
223
229
 
224
- For example, given a component that displays a banner to users who haven't confirmed their email address, the logic for whether to render the banner would need to go in either the component template:
230
+ Traditionally, the logic for whether to render a view could go in either the component template:
225
231
 
226
232
  `app/components/confirm_email_component.html.erb`
227
233
  ```
228
234
  <% if user.requires_confirmation? %>
229
- <div class="alert">
230
- Please confirm your email address.
231
- </div>
235
+ <div class="alert">Please confirm your email address.</div>
232
236
  <% end %>
233
237
  ```
234
238
 
@@ -241,7 +245,7 @@ or the view that renders the component:
241
245
  <% end %>
242
246
  ```
243
247
 
244
- Instead, the `#render?` hook expresses this logic in the Ruby class, simplifying the view:
248
+ Using the `#render?` hook simplifies the view:
245
249
 
246
250
  `app/components/confirm_email_component.rb`
247
251
  ```ruby
@@ -268,19 +272,30 @@ end
268
272
  <%= render(ConfirmEmailComponent.new(user: current_user)) %>
269
273
  ```
270
274
 
271
- To assert that a component has not been rendered, use `refute_component_rendered` from `ViewComponent::TestHelpers`.
275
+ _To assert that a component has not been rendered, use `refute_component_rendered` from `ViewComponent::TestHelpers`._
276
+
277
+ ### `before_render`
278
+
279
+ Components can define a `before_render` method to be called before a component is rendered, when `helpers` is able to be used:
280
+
281
+ `app/components/confirm_email_component.rb`
282
+ ```ruby
283
+ class MyComponent < ViewComponent::Base
284
+ def before_render
285
+ @my_icon = helpers.star_icon
286
+ end
287
+ end
288
+ ```
272
289
 
273
290
  ### Rendering collections
274
291
 
275
- It's possible to render collections with components:
292
+ Use `with_collection` to render a ViewComponent with a collection:
276
293
 
277
294
  `app/view/products/index.html.erb`
278
295
  ``` erb
279
296
  <%= render(ProductComponent.with_collection(@products)) %>
280
297
  ```
281
298
 
282
- Where the `ProductComponent` and associated template might look something like the following. Notice that the constructor must take a `product` and the name of that parameter matches the name of the component.
283
-
284
299
  `app/components/product_component.rb`
285
300
  ``` ruby
286
301
  class ProductComponent < ViewComponent::Base
@@ -290,12 +305,26 @@ class ProductComponent < ViewComponent::Base
290
305
  end
291
306
  ```
292
307
 
293
- `app/components/product_component.html.erb`
294
- ``` erb
295
- <li><%= @product.name %></li>
308
+ [By default](https://github.com/github/view_component/blob/89f8fab4609c1ef2467cf434d283864b3c754473/lib/view_component/base.rb#L249), the component name is used to define the parameter passed into the component from the collection.
309
+
310
+ #### `with_collection_parameter`
311
+
312
+ Use `with_collection_parameter` to change the name of the collection parameter:
313
+
314
+ `app/components/product_component.rb`
315
+ ``` ruby
316
+ class ProductComponent < ViewComponent::Base
317
+ with_collection_parameter :item
318
+
319
+ def initialize(item:)
320
+ @item = item
321
+ end
322
+ end
296
323
  ```
297
324
 
298
- Additionally, extra arguments can be passed to the component and the name of the parameter can be changed:
325
+ #### Additional arguments
326
+
327
+ Additional arguments besides the collection are passed to each component instance:
299
328
 
300
329
  `app/view/products/index.html.erb`
301
330
  ``` erb
@@ -322,7 +351,9 @@ end
322
351
  </li>
323
352
  ```
324
353
 
325
- `ViewComponent` defines a counter variable matching the parameter name above, followed by `_counter`. To access this variable, add it to `initialize` as an argument:
354
+ #### Collection counter
355
+
356
+ ViewComponent defines a counter variable matching the parameter name above, followed by `_counter`. To access the variable, add it to `initialize` as an argument:
326
357
 
327
358
  `app/components/product_component.rb`
328
359
  ``` ruby
@@ -341,140 +372,53 @@ end
341
372
  </li>
342
373
  ```
343
374
 
344
- ### Sidecar assets (experimental)
345
-
346
- We're experimenting with including Javascript and CSS alongside components, sometimes called "sidecar" assets or files.
347
-
348
- To use the Webpacker gem to compile sidecar assets located in `app/components`:
349
-
350
- 1. 1. In `config/webpacker.yml`, add `"app/components"` to the `resolved_paths` array (e.g. `resolved_paths: ["app/components"]`).
351
- 2. In the Webpack entry file (often `app/javascript/packs/application.js`), add an import statement to a helper file, and in the helper file, import the components' Javascript:
352
-
353
- Near the top the entry file, add:
354
-
355
- ```js
356
- import "../components"
357
- ```
358
-
359
- Then add the following to a new file `app/javascript/components.js`:
360
-
361
- ```js
362
- function importAll(r) {
363
- r.keys().forEach(r)
364
- }
365
-
366
- importAll(require.context("../components", true, /_component.js$/))
367
- ```
368
-
369
- Any file with the `_component.js` suffix, for example `app/components/widget_component.js`, will get compiled into the Webpack bundle. If that file itself imports another file, for example `app/components/widget_component.css`, that will also get compiled and bundled into Webpack's output stylesheet if Webpack is being used for styles.
370
-
371
- #### Encapsulating sidecar assets
372
-
373
- Ideally, sidecar Javascript/CSS should not "leak" out of the context of its associated component.
374
-
375
- One approach is to use Web Components, which contain all Javascript functionality, internal markup, and styles within the shadow root of the Web Component.
375
+ ### Using helpers
376
376
 
377
- For example:
377
+ Helper methods can be used through the `helpers` proxy:
378
378
 
379
- `app/components/comment_component.rb`
380
379
  ```ruby
381
- class CommentComponent < ViewComponent::Base
382
- def initialize(comment:)
383
- @comment = comment
384
- end
385
-
386
- def commenter
387
- @comment.user
388
- end
389
-
390
- def commenter_name
391
- commenter.name
392
- end
393
-
394
- def avatar
395
- commenter.avatar_image_url
380
+ module IconHelper
381
+ def icon(name)
382
+ tag.i data: { feather: name.to_s.dasherize }
396
383
  end
384
+ end
397
385
 
398
- def formatted_body
399
- simple_format(@comment.body)
386
+ class UserComponent < ViewComponent::Base
387
+ def profile_icon
388
+ helpers.icon :user
400
389
  end
401
-
402
- private
403
-
404
- attr_reader :comment
405
390
  end
406
391
  ```
407
392
 
408
- `app/components/comment_component.html.erb`
409
- ```erb
410
- <my-comment comment-id="<%= comment.id %>">
411
- <time slot="posted" datetime="<%= comment.created_at.iso8601 %>"><%= comment.created_at.strftime("%b %-d") %></time>
412
-
413
- <div slot="avatar"><img src="<%= avatar %>" /></div>
414
-
415
- <div slot="author"><%= commenter_name %></div>
416
-
417
- <div slot="body"><%= formatted_body %></div>
418
- </my-comment>
419
- ```
393
+ Which can be used with `delegate`:
420
394
 
421
- `app/components/comment_component.js`
422
- ```js
423
- class Comment extends HTMLElement {
424
- styles() {
425
- return `
426
- :host {
427
- display: block;
428
- }
429
- ::slotted(time) {
430
- float: right;
431
- font-size: 0.75em;
432
- }
433
- .commenter { font-weight: bold; }
434
- .body { … }
435
- `
436
- }
395
+ ```ruby
396
+ class UserComponent < ViewComponent::Base
397
+ delegate :icon, to: :helpers
437
398
 
438
- constructor() {
439
- super()
440
- const shadow = this.attachShadow({mode: 'open'});
441
- shadow.innerHTML = `
442
- <style>
443
- ${this.styles()}
444
- </style>
445
- <slot name="posted"></slot>
446
- <div class="commenter">
447
- <slot name="avatar"></slot> <slot name="author"></slot>
448
- </div>
449
- <div class="body">
450
- <slot name="body"></slot>
451
- </div>
452
- `
453
- }
454
- }
455
- customElements.define('my-comment', Comment)
399
+ def profile_icon
400
+ icon :user
401
+ end
402
+ end
456
403
  ```
457
404
 
458
- ##### Stimulus
405
+ Helpers can also be used by including the helper:
459
406
 
460
- In Stimulus, create a 1:1 mapping between a Stimulus controller and a component. In order to load in Stimulus controllers from the `app/components` tree, amend the Stimulus boot code in `app/javascript/packs/application.js`:
407
+ ```ruby
408
+ class UserComponent < ViewComponent::Base
409
+ include IconHelper
461
410
 
462
- ```js
463
- const application = Application.start()
464
- const context = require.context("controllers", true, /.js$/)
465
- const context_components = require.context("../../components", true, /_controller.js$/)
466
- application.load(
467
- definitionsFromContext(context).concat(
468
- definitionsFromContext(context_components)
469
- )
470
- )
411
+ def profile_icon
412
+ icon :user
413
+ end
414
+ end
471
415
  ```
472
416
 
473
- This will allow you to create files such as `app/components/widget_controller.js`, where the controller identifier matches the `data-controller` attribute in the component's HTML template.
474
-
475
417
  ### Testing
476
418
 
477
- Unit test components directly, using the `render_inline` test helper. If you have a `capybara` test dependency, Capybara matchers will be available in your tests:
419
+ Unit test components directly, using the `render_inline` test helper, asserting against the rendered output.
420
+
421
+ Capybara matchers are available if the gem is installed:
478
422
 
479
423
  ```ruby
480
424
  require "view_component/test_case"
@@ -483,12 +427,12 @@ class MyComponentTest < ViewComponent::TestCase
483
427
  test "render component" do
484
428
  render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
485
429
 
486
- assert_selector("span[title='my title']", "Hello, World!")
430
+ assert_selector("span[title='my title']", text: "Hello, World!")
487
431
  end
488
432
  end
489
433
  ```
490
434
 
491
- In the absence of `capybara`, you can make assertions on the `render_inline` return value, which is an instance of `Nokogiri::HTML::DocumentFragment`:
435
+ In the absence of `capybara`, assert against the return value of `render_inline`, which is an instance of `Nokogiri::HTML::DocumentFragment`:
492
436
 
493
437
  ```ruby
494
438
  test "render component" do
@@ -498,6 +442,31 @@ test "render component" do
498
442
  end
499
443
  ```
500
444
 
445
+ Alternatively, assert against the raw output of the component, which is exposed as `rendered_component`:
446
+
447
+ ```ruby
448
+ test "render component" do
449
+ render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
450
+
451
+ assert_includes rendered_component, "Hello, World!"
452
+ end
453
+ ```
454
+
455
+ To test components that use `with_content_areas`:
456
+
457
+ ```ruby
458
+ test "renders content_areas template with content " do
459
+ render_inline(ContentAreasComponent.new(footer: "Bye!")) do |component|
460
+ component.with(:title, "Hello!")
461
+ component.with(:body) { "Have a nice day." }
462
+ end
463
+
464
+ assert_selector(".title", text: "Hello!")
465
+ assert_selector(".body", text: "Have a nice day.")
466
+ assert_selector(".footer", text: "Bye!")
467
+ end
468
+ ```
469
+
501
470
  #### Action Pack Variants
502
471
 
503
472
  Use the `with_variant` helper to test specific variants:
@@ -507,7 +476,7 @@ test "render component for tablet" do
507
476
  with_variant :tablet do
508
477
  render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
509
478
 
510
- assert_selector("span[title='my title']", "Hello, tablets!")
479
+ assert_selector("span[title='my title']", text: "Hello, tablets!")
511
480
  end
512
481
  end
513
482
  ```
@@ -551,17 +520,13 @@ class TestComponentPreview < ViewComponent::Preview
551
520
  end
552
521
  ```
553
522
 
554
- You'll then be able to pass down a value with <http://localhost:3000/rails/components/test_component/with_dynamic_title?title=Custom+title>.
523
+ Which enables passing in a value with <http://localhost:3000/rails/components/test_component/with_dynamic_title?title=Custom+title>.
555
524
 
556
525
  The `ViewComponent::Preview` base class includes
557
- [`ActionView::Helpers::TagHelper`][tag-helper], which provides the [`tag`][tag]
558
- and [`content_tag`][content_tag] view helper methods.
526
+ [`ActionView::Helpers::TagHelper`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html), which provides the [`tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag)
527
+ and [`content_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag) view helper methods.
559
528
 
560
- [tag-helper]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html
561
- [tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag
562
- [content_tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag
563
-
564
- Previews default to the application layout, but can be overridden:
529
+ Previews use the application layout by default, but can use a specific layout with the `layout` option:
565
530
 
566
531
  `test/components/previews/test_component_preview.rb`
567
532
  ```ruby
@@ -572,18 +537,25 @@ class TestComponentPreview < ViewComponent::Preview
572
537
  end
573
538
  ```
574
539
 
575
- Preview classes live in `test/components/previews`, can be configured using the `preview_path` option.
576
-
577
- To use `lib/component_previews`:
540
+ Preview classes live in `test/components/previews`, which can be configured using the `preview_path` option:
578
541
 
579
542
  `config/application.rb`
580
543
  ```ruby
581
544
  config.view_component.preview_path = "#{Rails.root}/lib/component_previews"
582
545
  ```
583
546
 
547
+ Previews are served from <http://localhost:3000/rails/view_components> by default. To use a different endpoint, set the `preview_route` option:
548
+
549
+ `config/application.rb`
550
+ ```ruby
551
+ config.view_component.preview_route = "/previews"
552
+ ```
553
+
554
+ This example will make the previews available from <http://localhost:3000/previews>.
555
+
584
556
  #### Configuring TestController
585
557
 
586
- Component tests and previews assume the existence of an `ApplicationController` class, be can be configured using the `test_controller` option:
558
+ Component tests and previews assume the existence of an `ApplicationController` class, which be can be configured using the `test_controller` option:
587
559
 
588
560
  `config/application.rb`
589
561
  ```ruby
@@ -612,19 +584,144 @@ To use component previews:
612
584
  config.view_component.preview_path = "#{Rails.root}/spec/components/previews"
613
585
  ```
614
586
 
615
- ## Frequently Asked Questions
587
+ ### Sidecar assets (experimental)
616
588
 
617
- ### Can I use other templating languages besides ERB?
589
+ It’s possible to include Javascript and CSS alongside components, sometimes called "sidecar" assets or files.
618
590
 
619
- Yes. This gem is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
591
+ To use the Webpacker gem to compile sidecar assets located in `app/components`:
620
592
 
621
- ### What happened to inline templates?
593
+ 1. In `config/webpacker.yml`, add `"app/components"` to the `resolved_paths` array (e.g. `resolved_paths: ["app/components"]`).
594
+ 2. In the Webpack entry file (often `app/javascript/packs/application.js`), add an import statement to a helper file, and in the helper file, import the components' Javascript:
622
595
 
623
- Inline templates have been removed (for now) due to concerns raised by [@soutaro](https://github.com/soutaro) regarding compatibility with the type systems being developed for Ruby 3.
596
+ ```js
597
+ import "../components"
598
+ ```
599
+
600
+ Then, in `app/javascript/components.js`, add:
601
+
602
+ ```js
603
+ function importAll(r) {
604
+ r.keys().forEach(r)
605
+ }
606
+
607
+ importAll(require.context("../components", true, /_component.js$/))
608
+ ```
609
+
610
+ Any file with the `_component.js` suffix (such as `app/components/widget_component.js`) will be compiled into the Webpack bundle. If that file itself imports another file, for example `app/components/widget_component.css`, it will also be compiled and bundled into Webpack's output stylesheet if Webpack is being used for styles.
611
+
612
+ #### Encapsulating sidecar assets
613
+
614
+ Ideally, sidecar Javascript/CSS should not "leak" out of the context of its associated component.
615
+
616
+ One approach is to use Web Components, which contain all Javascript functionality, internal markup, and styles within the shadow root of the Web Component.
617
+
618
+ For example:
619
+
620
+ `app/components/comment_component.rb`
621
+ ```ruby
622
+ class CommentComponent < ViewComponent::Base
623
+ def initialize(comment:)
624
+ @comment = comment
625
+ end
626
+
627
+ def commenter
628
+ @comment.user
629
+ end
630
+
631
+ def commenter_name
632
+ commenter.name
633
+ end
634
+
635
+ def avatar
636
+ commenter.avatar_image_url
637
+ end
638
+
639
+ def formatted_body
640
+ simple_format(@comment.body)
641
+ end
642
+
643
+ private
644
+
645
+ attr_reader :comment
646
+ end
647
+ ```
648
+
649
+ `app/components/comment_component.html.erb`
650
+ ```erb
651
+ <my-comment comment-id="<%= comment.id %>">
652
+ <time slot="posted" datetime="<%= comment.created_at.iso8601 %>"><%= comment.created_at.strftime("%b %-d") %></time>
653
+
654
+ <div slot="avatar"><img src="<%= avatar %>" /></div>
655
+
656
+ <div slot="author"><%= commenter_name %></div>
657
+
658
+ <div slot="body"><%= formatted_body %></div>
659
+ </my-comment>
660
+ ```
661
+
662
+ `app/components/comment_component.js`
663
+ ```js
664
+ class Comment extends HTMLElement {
665
+ styles() {
666
+ return `
667
+ :host {
668
+ display: block;
669
+ }
670
+ ::slotted(time) {
671
+ float: right;
672
+ font-size: 0.75em;
673
+ }
674
+ .commenter { font-weight: bold; }
675
+ .body { … }
676
+ `
677
+ }
678
+
679
+ constructor() {
680
+ super()
681
+ const shadow = this.attachShadow({mode: 'open'});
682
+ shadow.innerHTML = `
683
+ <style>
684
+ ${this.styles()}
685
+ </style>
686
+ <slot name="posted"></slot>
687
+ <div class="commenter">
688
+ <slot name="avatar"></slot> <slot name="author"></slot>
689
+ </div>
690
+ <div class="body">
691
+ <slot name="body"></slot>
692
+ </div>
693
+ `
694
+ }
695
+ }
696
+ customElements.define('my-comment', Comment)
697
+ ```
698
+
699
+ ##### Stimulus
700
+
701
+ In Stimulus, create a 1:1 mapping between a Stimulus controller and a component. In order to load in Stimulus controllers from the `app/components` tree, amend the Stimulus boot code in `app/javascript/packs/application.js`:
702
+
703
+ ```js
704
+ const application = Application.start()
705
+ const context = require.context("controllers", true, /.js$/)
706
+ const context_components = require.context("../../components", true, /_controller.js$/)
707
+ application.load(
708
+ definitionsFromContext(context).concat(
709
+ definitionsFromContext(context_components)
710
+ )
711
+ )
712
+ ```
713
+
714
+ This enables the creation of files such as `app/components/widget_controller.js`, where the controller identifier matches the `data-controller` attribute in the component's HTML template.
715
+
716
+ ## Frequently Asked Questions
717
+
718
+ ### Can I use other templating languages besides ERB?
719
+
720
+ Yes. ViewComponent is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
624
721
 
625
722
  ### Isn't this just like X library?
626
723
 
627
- `ViewComponent` is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
724
+ ViewComponent is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
628
725
 
629
726
  - [trailblazer/cells](https://github.com/trailblazer/cells)
630
727
  - [dry-rb/dry-view](https://github.com/dry-rb/dry-view)
@@ -633,6 +730,7 @@ Inline templates have been removed (for now) due to concerns raised by [@soutaro
633
730
 
634
731
  ## Resources
635
732
 
733
+ - [Encapsulating Views, RailsConf 2020](https://youtu.be/YVYRus_2KZM)
636
734
  - [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
637
735
  - [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
638
736
  - [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
@@ -648,7 +746,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
648
746
 
649
747
  ## Contributors
650
748
 
651
- `view_component` is built by:
749
+ ViewComponent is built by:
652
750
 
653
751
  |<img src="https://avatars.githubusercontent.com/joelhawksley?s=256" alt="joelhawksley" width="128" />|<img src="https://avatars.githubusercontent.com/tenderlove?s=256" alt="tenderlove" width="128" />|<img src="https://avatars.githubusercontent.com/jonspalmer?s=256" alt="jonspalmer" width="128" />|<img src="https://avatars.githubusercontent.com/juanmanuelramallo?s=256" alt="juanmanuelramallo" width="128" />|<img src="https://avatars.githubusercontent.com/vinistock?s=256" alt="vinistock" width="128" />|
654
752
  |:---:|:---:|:---:|:---:|:---:|
@@ -680,11 +778,11 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
680
778
  |@blakewilliams|@seanpdoyle|@tclem|@nashby|@jaredcwhite|
681
779
  |Boston, MA|New York, NY|San Francisco, CA|Minsk|Portland, OR|
682
780
 
683
- |<img src="https://avatars.githubusercontent.com/simonrand?s=256" alt="simonrand" width="128" />|<img src="https://avatars.githubusercontent.com/fugufish?s=256" alt="fugufish" width="128" />|<img src="https://avatars.githubusercontent.com/cover?s=256" alt="cover" width="128" />|<img src="https://avatars.githubusercontent.com/franks921?s=256" alt="franks921" width="128" />|
684
- |:---:|:---:|:---:|:---:|
685
- |@simonrand|@fugufish|@cover|@franks921|
686
- |Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|
781
+ |<img src="https://avatars.githubusercontent.com/simonrand?s=256" alt="simonrand" width="128" />|<img src="https://avatars.githubusercontent.com/fugufish?s=256" alt="fugufish" width="128" />|<img src="https://avatars.githubusercontent.com/cover?s=256" alt="cover" width="128" />|<img src="https://avatars.githubusercontent.com/franks921?s=256" alt="franks921" width="128" />|<img src="https://avatars.githubusercontent.com/fsateler?s=256" alt="fsateler" width="128" />|
782
+ |:---:|:---:|:---:|:---:|:---:|
783
+ |@simonrand|@fugufish|@cover|@franks921|@fsateler|
784
+ |Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|Chile|
687
785
 
688
786
  ## License
689
787
 
690
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
788
+ ViewComponent is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).