view_component 2.5.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +317 -213
- data/app/controllers/{rails/view_components_controller.rb → view_components_controller.rb} +4 -11
- 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 +136 -38
- data/lib/view_component/collection.rb +5 -3
- data/lib/view_component/compile_cache.rb +24 -0
- data/lib/view_component/engine.rb +7 -2
- data/lib/view_component/preview.rb +1 -0
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/test_helpers.rb +5 -3
- data/lib/view_component/version.rb +2 -2
- metadata +37 -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: 66521e84077cac73f9c94df1323b7b7eefc168c47fa635a10ea92ca18ed2791b
|
4
|
+
data.tar.gz: 21add4be61c4e1dddb543704355b4263f5b87a7f8d9060d8c48f309d78703ed2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 532230cb3c84dfd7d492b9f6ee1278dd2706b8c052a667c9be76467cf227616cbc90a69de7c675338ac3a46149b8b241a79668d6d7d399d1d3d7f52cd4251251
|
7
|
+
data.tar.gz: 4a3f0f2a7a4dabb25738e48ddbe92a704e2117e02ab1b38fbccc4c33913cc57115c4bc81e78c57647d5a2ca94843b0f9d79d64b559b0fd1c297fef2f56425194
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,43 @@
|
|
1
1
|
# master
|
2
2
|
|
3
|
+
# 2.10.0
|
4
|
+
|
5
|
+
* Raise an `ArgumentError` with a helpful message when Ruby cannot parse a component class.
|
6
|
+
|
7
|
+
*Max Beizer*
|
8
|
+
|
9
|
+
# 2.9.0
|
10
|
+
|
11
|
+
* Cache components per-request in development, preventing unnecessary recompilation during a single request.
|
12
|
+
|
13
|
+
*Felipe Sateler*
|
14
|
+
|
15
|
+
# 2.8.0
|
16
|
+
|
17
|
+
* Add `before_render`, deprecating `before_render_check`.
|
18
|
+
|
19
|
+
*Joel Hawksley*
|
20
|
+
|
21
|
+
# 2.7.0
|
22
|
+
|
23
|
+
* Add `rendered_component` method to `ViewComponent::TestHelpers` which exposes the raw output of the rendered component.
|
24
|
+
|
25
|
+
*Richard Macklin*
|
26
|
+
|
27
|
+
* Support sidecar directories for views and other assets.
|
28
|
+
|
29
|
+
*Jon Palmer*
|
30
|
+
|
31
|
+
# 2.6.0
|
32
|
+
|
33
|
+
* Add `config.view_component.preview_route` to set the endpoint for component previews. By default `/rails/view_components` is used.
|
34
|
+
|
35
|
+
*Juan Manuel Ramallo*
|
36
|
+
|
37
|
+
* Raise error when initializer omits with_collection_parameter.
|
38
|
+
|
39
|
+
*Joel Hawksley*
|
40
|
+
|
3
41
|
# 2.5.1
|
4
42
|
|
5
43
|
* Compile component before rendering collection.
|
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,7 +351,9 @@ end
|
|
322
351
|
</li>
|
323
352
|
```
|
324
353
|
|
325
|
-
|
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
|
-
###
|
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
|
-
|
377
|
+
Helper methods can be used through the `helpers` proxy:
|
378
378
|
|
379
|
-
`app/components/comment_component.rb`
|
380
379
|
```ruby
|
381
|
-
|
382
|
-
def
|
383
|
-
|
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
|
-
|
399
|
-
|
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
|
-
`
|
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
|
-
|
422
|
-
|
423
|
-
|
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
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
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
|
-
|
405
|
+
Helpers can also be used by including the helper:
|
459
406
|
|
460
|
-
|
407
|
+
```ruby
|
408
|
+
class UserComponent < ViewComponent::Base
|
409
|
+
include IconHelper
|
461
410
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
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
|
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`,
|
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
|
-
|
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`]
|
558
|
-
and [`content_tag`]
|
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
|
-
|
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
|
-
|
587
|
+
### Sidecar assets (experimental)
|
616
588
|
|
617
|
-
|
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.
|
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
|
618
638
|
|
619
|
-
|
639
|
+
def formatted_body
|
640
|
+
simple_format(@comment.body)
|
641
|
+
end
|
620
642
|
|
621
|
-
|
643
|
+
private
|
622
644
|
|
623
|
-
|
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
|
-
|
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,8 @@ 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)
|
734
|
+
- [Rethinking the View Layer with Components - Ruby Rogues Podcast](https://devchat.tv/ruby-rogues/rr-461-rethinking-the-view-layer-with-components-with-joel-hawksley/)
|
636
735
|
- [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
|
637
736
|
- [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
|
638
737
|
- [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
|
@@ -648,7 +747,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
|
|
648
747
|
|
649
748
|
## Contributors
|
650
749
|
|
651
|
-
|
750
|
+
ViewComponent is built by:
|
652
751
|
|
653
752
|
|<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
753
|
|:---:|:---:|:---:|:---:|:---:|
|
@@ -680,11 +779,16 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
|
|
680
779
|
|@blakewilliams|@seanpdoyle|@tclem|@nashby|@jaredcwhite|
|
681
780
|
|Boston, MA|New York, NY|San Francisco, CA|Minsk|Portland, OR|
|
682
781
|
|
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|
|
782
|
+
|<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" />|
|
783
|
+
|:---:|:---:|:---:|:---:|:---:|
|
784
|
+
|@simonrand|@fugufish|@cover|@franks921|@fsateler|
|
785
|
+
|Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|Chile|
|
786
|
+
|
787
|
+
|<img src="https://avatars.githubusercontent.com/maxbeizer?s=256" alt="maxbeizer" width="128" />|
|
788
|
+
|:---:|
|
789
|
+
|@maxbeizer|
|
790
|
+
|Nashville, TN|
|
687
791
|
|
688
792
|
## License
|
689
793
|
|
690
|
-
|
794
|
+
ViewComponent is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|