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 +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +336 -206
- data/app/controllers/{rails/view_components_controller.rb → view_components_controller.rb} +5 -12
- data/app/views/view_components/index.html.erb +8 -0
- data/app/views/view_components/preview.html.erb +1 -0
- data/{lib/railties/lib/rails/templates/rails/components → app/views/view_components}/previews.html.erb +1 -1
- data/lib/view_component/base.rb +127 -33
- data/lib/view_component/collection.rb +14 -3
- data/lib/view_component/engine.rb +3 -2
- data/lib/view_component/preview.rb +6 -2
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/test_helpers.rb +5 -3
- data/lib/view_component/version.rb +1 -1
- metadata +36 -9
- data/lib/railties/lib/rails.rb +0 -5
- data/lib/railties/lib/rails/templates/rails/components/index.html.erb +0 -8
- data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ac6dfbc2cb2fb7bbad0aac55de41cfbdc782dac0eb14f1e4b18784e15db02a9
|
4
|
+
data.tar.gz: 294bd6faac8d95b0db842ab67139e141117058148c24d503647960e31c19ef21
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d2da7f6d4ff599ea9b92408dc632eecb7629d7bf7cda9484eab20b0f02dd1a78ba3669fc5f9107f2b6388d2f5634258a14b806d58327c716a8339b819ef8aa7
|
7
|
+
data.tar.gz: 50b86ff9e697608ae063352c8a55430763b5762016c3512dfa103f5df7a2fbb4875a993e319783f2e1e077c4ca157152a4314d3a106b0fc0a84186926044f917
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
62
|
+
Component names end in -`Component`.
|
86
63
|
|
87
|
-
Component module names are plural, as
|
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
|
68
|
+
Use the component generator to create a new ViewComponent.
|
94
69
|
|
95
|
-
The generator accepts
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
228
|
+
Components can implement a `#render?` method to be called after initialization to determine if the component should render.
|
223
229
|
|
224
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
337
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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
|
-
|
375
|
+
### Using helpers
|
351
376
|
|
352
|
-
|
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
|
-
|
363
|
-
def
|
364
|
-
|
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
|
-
|
380
|
-
|
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
|
-
`
|
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
|
-
|
395
|
-
|
396
|
-
|
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
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
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
|
-
|
405
|
+
Helpers can also be used by including the helper:
|
440
406
|
|
441
|
-
|
407
|
+
```ruby
|
408
|
+
class UserComponent < ViewComponent::Base
|
409
|
+
include IconHelper
|
442
410
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
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`,
|
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
|
-
|
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
|
-
|
529
|
-
|
530
|
-
|
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
|
-
|
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
|
-
|
587
|
+
### Sidecar assets (experimental)
|
584
588
|
|
585
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
788
|
+
ViewComponent is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|