view_component 2.4.0 → 2.8.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: ca0004a178e80b26bc7c47af5ef2dab32cdb4f3c61d09a1b27e593cf8ec4f5ea
4
- data.tar.gz: 16427ce66ee5872627dafd670799fe19c42bfec499163dcbbe4e643a0e3b34fc
3
+ metadata.gz: 7ac6dfbc2cb2fb7bbad0aac55de41cfbdc782dac0eb14f1e4b18784e15db02a9
4
+ data.tar.gz: 294bd6faac8d95b0db842ab67139e141117058148c24d503647960e31c19ef21
5
5
  SHA512:
6
- metadata.gz: 7ea71f48ee58741b49df4818f0717328b715a1c11ebb0ecf0771c15aabb53fc3cdb51428c0b5f85781c04f113afca98fbe152083090f9f0b518f988cd60de3e9
7
- data.tar.gz: 401537d259e436c9cb0ea7e3c4dc40af6c02f69013460583b4827795f9d615d79aa510aa9472840af05edea39d689d6cd785279a82c6e1b48ebb980654c03087
6
+ metadata.gz: 4d2da7f6d4ff599ea9b92408dc632eecb7629d7bf7cda9484eab20b0f02dd1a78ba3669fc5f9107f2b6388d2f5634258a14b806d58327c716a8339b819ef8aa7
7
+ data.tar.gz: 50b86ff9e697608ae063352c8a55430763b5762016c3512dfa103f5df7a2fbb4875a993e319783f2e1e077c4ca157152a4314d3a106b0fc0a84186926044f917
@@ -1,5 +1,47 @@
1
1
  # master
2
2
 
3
+ # 2.8.0
4
+
5
+ * Add `before_render`, deprecating `before_render_check`.
6
+
7
+ *Joel Hawksley*
8
+
9
+ # 2.7.0
10
+
11
+ * Add `rendered_component` method to `ViewComponent::TestHelpers` which exposes the raw output of the rendered component.
12
+
13
+ *Richard Macklin*
14
+
15
+ * Support sidecar directories for views and other assets.
16
+
17
+ *Jon Palmer*
18
+
19
+ # 2.6.0
20
+
21
+ * Add `config.view_component.preview_route` to set the endpoint for component previews. By default `/rails/view_components` is used.
22
+
23
+ *Juan Manuel Ramallo*
24
+
25
+ * Raise error when initializer omits with_collection_parameter.
26
+
27
+ *Joel Hawksley*
28
+
29
+ # 2.5.1
30
+
31
+ * Compile component before rendering collection.
32
+
33
+ *Rainer Borene*
34
+
35
+ # v2.5.0
36
+
37
+ * Add counter variables when rendering collections.
38
+
39
+ *Frank S*
40
+
41
+ * Add the ability to access params from preview examples.
42
+
43
+ *Fabio Cantoni*
44
+
3
45
  # v2.4.0
4
46
 
5
47
  * Add `#render_to_string` support.
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,140 +351,74 @@ end
322
351
  </li>
323
352
  ```
324
353
 
325
- ### Sidecar assets (experimental)
326
-
327
- We're experimenting with including Javascript and CSS alongside components, sometimes called "sidecar" assets or files.
328
-
329
- To use the Webpacker gem to compile sidecar assets located in `app/components`:
330
-
331
- 1. 1. In `config/webpacker.yml`, add `"app/components"` to the `resolved_paths` array (e.g. `resolved_paths: ["app/components"]`).
332
- 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:
354
+ #### Collection counter
333
355
 
334
- Near the top the entry file, add:
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:
335
357
 
336
- ```js
337
- import "../components"
358
+ `app/components/product_component.rb`
359
+ ``` ruby
360
+ class ProductComponent < ViewComponent::Base
361
+ def initialize(product:, product_counter:)
362
+ @product = product
363
+ @counter = product_counter
364
+ end
365
+ end
338
366
  ```
339
367
 
340
- Then add the following to a new file `app/javascript/components.js`:
341
-
342
- ```js
343
- function importAll(r) {
344
- r.keys().forEach(r)
345
- }
346
-
347
- importAll(require.context("../components", true, /_component.js$/))
368
+ `app/components/product_component.html.erb`
369
+ ``` erb
370
+ <li>
371
+ <%= @counter %> <%= @product.name %>
372
+ </li>
348
373
  ```
349
374
 
350
- 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.
375
+ ### Using helpers
351
376
 
352
- #### Encapsulating sidecar assets
377
+ Helper methods can be used through the `helpers` proxy:
353
378
 
354
- Ideally, sidecar Javascript/CSS should not "leak" out of the context of its associated component.
355
-
356
- One approach is to use Web Components, which contain all Javascript functionality, internal markup, and styles within the shadow root of the Web Component.
357
-
358
- For example:
359
-
360
- `app/components/comment_component.rb`
361
379
  ```ruby
362
- class CommentComponent < ViewComponent::Base
363
- def initialize(comment:)
364
- @comment = comment
365
- end
366
-
367
- def commenter
368
- @comment.user
369
- end
370
-
371
- def commenter_name
372
- commenter.name
373
- end
374
-
375
- def avatar
376
- commenter.avatar_image_url
380
+ module IconHelper
381
+ def icon(name)
382
+ tag.i data: { feather: name.to_s.dasherize }
377
383
  end
384
+ end
378
385
 
379
- def formatted_body
380
- simple_format(@comment.body)
386
+ class UserComponent < ViewComponent::Base
387
+ def profile_icon
388
+ helpers.icon :user
381
389
  end
382
-
383
- private
384
-
385
- attr_reader :comment
386
390
  end
387
391
  ```
388
392
 
389
- `app/components/comment_component.html.erb`
390
- ```erb
391
- <my-comment comment-id="<%= comment.id %>">
392
- <time slot="posted" datetime="<%= comment.created_at.iso8601 %>"><%= comment.created_at.strftime("%b %-d") %></time>
393
+ Which can be used with `delegate`:
393
394
 
394
- <div slot="avatar"><img src="<%= avatar %>" /></div>
395
-
396
- <div slot="author"><%= commenter_name %></div>
397
-
398
- <div slot="body"><%= formatted_body %></div>
399
- </my-comment>
400
- ```
401
-
402
- `app/components/comment_component.js`
403
- ```js
404
- class Comment extends HTMLElement {
405
- styles() {
406
- return `
407
- :host {
408
- display: block;
409
- }
410
- ::slotted(time) {
411
- float: right;
412
- font-size: 0.75em;
413
- }
414
- .commenter { font-weight: bold; }
415
- .body { … }
416
- `
417
- }
395
+ ```ruby
396
+ class UserComponent < ViewComponent::Base
397
+ delegate :icon, to: :helpers
418
398
 
419
- constructor() {
420
- super()
421
- const shadow = this.attachShadow({mode: 'open'});
422
- shadow.innerHTML = `
423
- <style>
424
- ${this.styles()}
425
- </style>
426
- <slot name="posted"></slot>
427
- <div class="commenter">
428
- <slot name="avatar"></slot> <slot name="author"></slot>
429
- </div>
430
- <div class="body">
431
- <slot name="body"></slot>
432
- </div>
433
- `
434
- }
435
- }
436
- customElements.define('my-comment', Comment)
399
+ def profile_icon
400
+ icon :user
401
+ end
402
+ end
437
403
  ```
438
404
 
439
- ##### Stimulus
405
+ Helpers can also be used by including the helper:
440
406
 
441
- 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
442
410
 
443
- ```js
444
- const application = Application.start()
445
- const context = require.context("controllers", true, /.js$/)
446
- const context_components = require.context("../../components", true, /_controller.js$/)
447
- application.load(
448
- definitionsFromContext(context).concat(
449
- definitionsFromContext(context_components)
450
- )
451
- )
411
+ def profile_icon
412
+ icon :user
413
+ end
414
+ end
452
415
  ```
453
416
 
454
- 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.
455
-
456
417
  ### Testing
457
418
 
458
- 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:
459
422
 
460
423
  ```ruby
461
424
  require "view_component/test_case"
@@ -464,12 +427,12 @@ class MyComponentTest < ViewComponent::TestCase
464
427
  test "render component" do
465
428
  render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
466
429
 
467
- assert_selector("span[title='my title']", "Hello, World!")
430
+ assert_selector("span[title='my title']", text: "Hello, World!")
468
431
  end
469
432
  end
470
433
  ```
471
434
 
472
- 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`:
473
436
 
474
437
  ```ruby
475
438
  test "render component" do
@@ -479,6 +442,31 @@ test "render component" do
479
442
  end
480
443
  ```
481
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
+
482
470
  #### Action Pack Variants
483
471
 
484
472
  Use the `with_variant` helper to test specific variants:
@@ -488,7 +476,7 @@ test "render component for tablet" do
488
476
  with_variant :tablet do
489
477
  render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
490
478
 
491
- assert_selector("span[title='my title']", "Hello, tablets!")
479
+ assert_selector("span[title='my title']", text: "Hello, tablets!")
492
480
  end
493
481
  end
494
482
  ```
@@ -508,7 +496,7 @@ class TestComponentPreview < ViewComponent::Preview
508
496
  end
509
497
 
510
498
  def with_content_block
511
- render(TestComponent.new(title: "This component accepts a block of content") do
499
+ render(TestComponent.new(title: "This component accepts a block of content")) do
512
500
  tag.div do
513
501
  content_tag(:span, "Hello")
514
502
  end
@@ -521,15 +509,24 @@ Which generates
521
509
  <http://localhost:3000/rails/view_components/test_component/with_long_title>,
522
510
  and <http://localhost:3000/rails/view_components/test_component/with_content_block>.
523
511
 
524
- The `ViewComponent::Preview` base class includes
525
- [`ActionView::Helpers::TagHelper`][tag-helper], which provides the [`tag`][tag]
526
- and [`content_tag`][content_tag] view helper methods.
512
+ It's also possible to set dynamic values from the params by setting them as arguments:
527
513
 
528
- [tag-helper]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html
529
- [tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag
530
- [content_tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag
514
+ `test/components/previews/test_component_preview.rb`
515
+ ```ruby
516
+ class TestComponentPreview < ViewComponent::Preview
517
+ def with_dynamic_title(title: "Test component default")
518
+ render(TestComponent.new(title: title))
519
+ end
520
+ end
521
+ ```
531
522
 
532
- Previews default to the application layout, but can be overridden:
523
+ Which enables passing in a value with <http://localhost:3000/rails/components/test_component/with_dynamic_title?title=Custom+title>.
524
+
525
+ The `ViewComponent::Preview` base class includes
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.
528
+
529
+ Previews use the application layout by default, but can use a specific layout with the `layout` option:
533
530
 
534
531
  `test/components/previews/test_component_preview.rb`
535
532
  ```ruby
@@ -540,18 +537,25 @@ class TestComponentPreview < ViewComponent::Preview
540
537
  end
541
538
  ```
542
539
 
543
- Preview classes live in `test/components/previews`, can be configured using the `preview_path` option.
544
-
545
- To use `lib/component_previews`:
540
+ Preview classes live in `test/components/previews`, which can be configured using the `preview_path` option:
546
541
 
547
542
  `config/application.rb`
548
543
  ```ruby
549
544
  config.view_component.preview_path = "#{Rails.root}/lib/component_previews"
550
545
  ```
551
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
+
552
556
  #### Configuring TestController
553
557
 
554
- 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:
555
559
 
556
560
  `config/application.rb`
557
561
  ```ruby
@@ -580,19 +584,144 @@ To use component previews:
580
584
  config.view_component.preview_path = "#{Rails.root}/spec/components/previews"
581
585
  ```
582
586
 
583
- ## Frequently Asked Questions
587
+ ### Sidecar assets (experimental)
584
588
 
585
- ### 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.
590
+
591
+ To use the Webpacker gem to compile sidecar assets located in `app/components`:
592
+
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:
595
+
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.
586
615
 
587
- Yes. This gem is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
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
588
634
 
589
- ### What happened to inline templates?
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?
590
719
 
591
- 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.
720
+ Yes. ViewComponent is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
592
721
 
593
722
  ### Isn't this just like X library?
594
723
 
595
- `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:
596
725
 
597
726
  - [trailblazer/cells](https://github.com/trailblazer/cells)
598
727
  - [dry-rb/dry-view](https://github.com/dry-rb/dry-view)
@@ -601,6 +730,7 @@ Inline templates have been removed (for now) due to concerns raised by [@soutaro
601
730
 
602
731
  ## Resources
603
732
 
733
+ - [Encapsulating Views, RailsConf 2020](https://youtu.be/YVYRus_2KZM)
604
734
  - [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
605
735
  - [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
606
736
  - [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
@@ -616,7 +746,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
616
746
 
617
747
  ## Contributors
618
748
 
619
- `view_component` is built by:
749
+ ViewComponent is built by:
620
750
 
621
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" />|
622
752
  |:---:|:---:|:---:|:---:|:---:|
@@ -648,11 +778,11 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
648
778
  |@blakewilliams|@seanpdoyle|@tclem|@nashby|@jaredcwhite|
649
779
  |Boston, MA|New York, NY|San Francisco, CA|Minsk|Portland, OR|
650
780
 
651
- |<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" />|
652
- |:---:|:---:|
653
- |@simonrand|@fugufish|
654
- |Dublin, Ireland|Salt Lake City, Utah|
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" />|
782
+ |:---:|:---:|:---:|:---:|
783
+ |@simonrand|@fugufish|@cover|@franks921|
784
+ |Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|
655
785
 
656
786
  ## License
657
787
 
658
- 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).