view_component 2.5.1 → 2.6.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 +10 -0
- data/README.md +238 -210
- 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 +93 -32
- data/lib/view_component/collection.rb +5 -3
- data/lib/view_component/engine.rb +3 -2
- data/lib/view_component/preview.rb +1 -0
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/version.rb +2 -2
- metadata +8 -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: 8bc0f4ab66dfca6a89f9882d0df39f5f4a84f479f26dfd0b3334b43fe4f9b4ed
|
4
|
+
data.tar.gz: 39128211a1338af6c78a54085b59f48a98411dc282e6248f03fc56229a6d7cd0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1aa9fb3d7361efa6a98465c1513e3296abc4495ad30393ed9fbf3febc919a8669184fe2e28fe5afa9666a42b71958787b2bf091b71d4e9889b020d0495769abf
|
7
|
+
data.tar.gz: 2ce5ece866ddc50d8aa01cd139c230c10b26aee9d8bbc49f7421b8d191e5b84dbd57d1247b9973f936932dcde9bd925f96c2d3114ac201d98511157b7643bac0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# master
|
2
2
|
|
3
|
+
# 2.6.0
|
4
|
+
|
5
|
+
* Add `config.view_component.preview_route` to set the endpoint for component previews. By default `/rails/view_components` is used.
|
6
|
+
|
7
|
+
*Juan Manuel Ramallo*
|
8
|
+
|
9
|
+
* Raise error when initializer omits with_collection_parameter.
|
10
|
+
|
11
|
+
*Joel Hawksley*
|
12
|
+
|
3
13
|
# 2.5.1
|
4
14
|
|
5
15
|
* 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
|
84
|
-
|
85
|
-
Component class names end in -`Component`.
|
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`.
|
86
61
|
|
87
|
-
Component
|
62
|
+
Component names end in -`Component`.
|
88
63
|
|
89
|
-
|
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,30 @@ 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
|
-
Using a mixture of templates and inline render methods in a component is supported, however only one should be provided per component (or variant).
|
219
|
-
|
220
194
|
### Conditional Rendering
|
221
195
|
|
222
|
-
Components can implement a `#render?` method to determine if
|
196
|
+
Components can implement a `#render?` method to be called after initialization to determine if the component should render.
|
223
197
|
|
224
|
-
|
198
|
+
Traditionally, the logic for whether to render a view could go in either the component template:
|
225
199
|
|
226
200
|
`app/components/confirm_email_component.html.erb`
|
227
201
|
```
|
228
202
|
<% if user.requires_confirmation? %>
|
229
|
-
<div class="alert">
|
230
|
-
Please confirm your email address.
|
231
|
-
</div>
|
203
|
+
<div class="alert">Please confirm your email address.</div>
|
232
204
|
<% end %>
|
233
205
|
```
|
234
206
|
|
@@ -241,7 +213,7 @@ or the view that renders the component:
|
|
241
213
|
<% end %>
|
242
214
|
```
|
243
215
|
|
244
|
-
|
216
|
+
Using the `#render?` hook simplifies the view:
|
245
217
|
|
246
218
|
`app/components/confirm_email_component.rb`
|
247
219
|
```ruby
|
@@ -268,19 +240,17 @@ end
|
|
268
240
|
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
|
269
241
|
```
|
270
242
|
|
271
|
-
|
243
|
+
_To assert that a component has not been rendered, use `refute_component_rendered` from `ViewComponent::TestHelpers`._
|
272
244
|
|
273
245
|
### Rendering collections
|
274
246
|
|
275
|
-
|
247
|
+
Use `with_collection` to render a ViewComponent with a collection:
|
276
248
|
|
277
249
|
`app/view/products/index.html.erb`
|
278
250
|
``` erb
|
279
251
|
<%= render(ProductComponent.with_collection(@products)) %>
|
280
252
|
```
|
281
253
|
|
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
254
|
`app/components/product_component.rb`
|
285
255
|
``` ruby
|
286
256
|
class ProductComponent < ViewComponent::Base
|
@@ -290,12 +260,26 @@ class ProductComponent < ViewComponent::Base
|
|
290
260
|
end
|
291
261
|
```
|
292
262
|
|
293
|
-
|
294
|
-
|
295
|
-
|
263
|
+
[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.
|
264
|
+
|
265
|
+
#### `with_collection_parameter`
|
266
|
+
|
267
|
+
Use `with_collection_parameter` to change the name of the collection parameter:
|
268
|
+
|
269
|
+
`app/components/product_component.rb`
|
270
|
+
``` ruby
|
271
|
+
class ProductComponent < ViewComponent::Base
|
272
|
+
with_collection_parameter :item
|
273
|
+
|
274
|
+
def initialize(item:)
|
275
|
+
@item = item
|
276
|
+
end
|
277
|
+
end
|
296
278
|
```
|
297
279
|
|
298
|
-
|
280
|
+
#### Additional arguments
|
281
|
+
|
282
|
+
Additional arguments besides the collection are passed to each component instance:
|
299
283
|
|
300
284
|
`app/view/products/index.html.erb`
|
301
285
|
``` erb
|
@@ -322,7 +306,9 @@ end
|
|
322
306
|
</li>
|
323
307
|
```
|
324
308
|
|
325
|
-
|
309
|
+
#### Collection counter
|
310
|
+
|
311
|
+
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
312
|
|
327
313
|
`app/components/product_component.rb`
|
328
314
|
``` ruby
|
@@ -341,140 +327,53 @@ end
|
|
341
327
|
</li>
|
342
328
|
```
|
343
329
|
|
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.
|
330
|
+
### Using helpers
|
376
331
|
|
377
|
-
|
332
|
+
Helper methods can be used through the `helpers` proxy:
|
378
333
|
|
379
|
-
`app/components/comment_component.rb`
|
380
334
|
```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
|
335
|
+
module IconHelper
|
336
|
+
def icon(name)
|
337
|
+
tag.i data: { feather: name.to_s.dasherize }
|
396
338
|
end
|
339
|
+
end
|
397
340
|
|
398
|
-
|
399
|
-
|
341
|
+
class UserComponent < ViewComponent::Base
|
342
|
+
def profile_icon
|
343
|
+
helpers.icon :user
|
400
344
|
end
|
401
|
-
|
402
|
-
private
|
403
|
-
|
404
|
-
attr_reader :comment
|
405
345
|
end
|
406
346
|
```
|
407
347
|
|
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
|
-
```
|
348
|
+
Which can be used with `delegate`:
|
420
349
|
|
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
|
-
}
|
350
|
+
```ruby
|
351
|
+
class UserComponent < ViewComponent::Base
|
352
|
+
delegate :icon, to: :helpers
|
437
353
|
|
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)
|
354
|
+
def profile_icon
|
355
|
+
icon :user
|
356
|
+
end
|
357
|
+
end
|
456
358
|
```
|
457
359
|
|
458
|
-
|
360
|
+
Helpers can also be used by including the helper:
|
459
361
|
|
460
|
-
|
362
|
+
```ruby
|
363
|
+
class UserComponent < ViewComponent::Base
|
364
|
+
include IconHelper
|
461
365
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
application.load(
|
467
|
-
definitionsFromContext(context).concat(
|
468
|
-
definitionsFromContext(context_components)
|
469
|
-
)
|
470
|
-
)
|
366
|
+
def profile_icon
|
367
|
+
icon :user
|
368
|
+
end
|
369
|
+
end
|
471
370
|
```
|
472
371
|
|
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
372
|
### Testing
|
476
373
|
|
477
|
-
Unit test components directly, using the `render_inline` test helper
|
374
|
+
Unit test components directly, using the `render_inline` test helper, asserting against the rendered output.
|
375
|
+
|
376
|
+
Capybara matchers are available if the gem is installed:
|
478
377
|
|
479
378
|
```ruby
|
480
379
|
require "view_component/test_case"
|
@@ -483,12 +382,12 @@ class MyComponentTest < ViewComponent::TestCase
|
|
483
382
|
test "render component" do
|
484
383
|
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
485
384
|
|
486
|
-
assert_selector("span[title='my title']", "Hello, World!")
|
385
|
+
assert_selector("span[title='my title']", text: "Hello, World!")
|
487
386
|
end
|
488
387
|
end
|
489
388
|
```
|
490
389
|
|
491
|
-
In the absence of `capybara`,
|
390
|
+
In the absence of `capybara`, assertion against the return values of `render_inline`, which is an instance of `Nokogiri::HTML::DocumentFragment`:
|
492
391
|
|
493
392
|
```ruby
|
494
393
|
test "render component" do
|
@@ -507,7 +406,7 @@ test "render component for tablet" do
|
|
507
406
|
with_variant :tablet do
|
508
407
|
render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
|
509
408
|
|
510
|
-
assert_selector("span[title='my title']", "Hello, tablets!")
|
409
|
+
assert_selector("span[title='my title']", text: "Hello, tablets!")
|
511
410
|
end
|
512
411
|
end
|
513
412
|
```
|
@@ -551,17 +450,13 @@ class TestComponentPreview < ViewComponent::Preview
|
|
551
450
|
end
|
552
451
|
```
|
553
452
|
|
554
|
-
|
453
|
+
Which enables passing in a value with <http://localhost:3000/rails/components/test_component/with_dynamic_title?title=Custom+title>.
|
555
454
|
|
556
455
|
The `ViewComponent::Preview` base class includes
|
557
|
-
[`ActionView::Helpers::TagHelper`]
|
558
|
-
and [`content_tag`]
|
559
|
-
|
560
|
-
[tag-helper]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html
|
561
|
-
[tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag
|
562
|
-
[content_tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag
|
456
|
+
[`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)
|
457
|
+
and [`content_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag) view helper methods.
|
563
458
|
|
564
|
-
Previews
|
459
|
+
Previews use the application layout by default, but can use a specific layout with the `layout` option:
|
565
460
|
|
566
461
|
`test/components/previews/test_component_preview.rb`
|
567
462
|
```ruby
|
@@ -572,18 +467,25 @@ class TestComponentPreview < ViewComponent::Preview
|
|
572
467
|
end
|
573
468
|
```
|
574
469
|
|
575
|
-
Preview classes live in `test/components/previews`, can be configured using the `preview_path` option
|
576
|
-
|
577
|
-
To use `lib/component_previews`:
|
470
|
+
Preview classes live in `test/components/previews`, which can be configured using the `preview_path` option:
|
578
471
|
|
579
472
|
`config/application.rb`
|
580
473
|
```ruby
|
581
474
|
config.view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
582
475
|
```
|
583
476
|
|
477
|
+
Previews are served from <http://localhost:3000/rails/view_components> by default. To use a different endpoint, set the `preview_route` option:
|
478
|
+
|
479
|
+
`config/application.rb`
|
480
|
+
```ruby
|
481
|
+
config.view_component.preview_route = "/previews"
|
482
|
+
```
|
483
|
+
|
484
|
+
This example will make the previews available from <http://localhost:3000/previews>.
|
485
|
+
|
584
486
|
#### Configuring TestController
|
585
487
|
|
586
|
-
Component tests and previews assume the existence of an `ApplicationController` class, be can be configured using the `test_controller` option:
|
488
|
+
Component tests and previews assume the existence of an `ApplicationController` class, which be can be configured using the `test_controller` option:
|
587
489
|
|
588
490
|
`config/application.rb`
|
589
491
|
```ruby
|
@@ -612,19 +514,144 @@ To use component previews:
|
|
612
514
|
config.view_component.preview_path = "#{Rails.root}/spec/components/previews"
|
613
515
|
```
|
614
516
|
|
615
|
-
|
517
|
+
### Sidecar assets (experimental)
|
616
518
|
|
617
|
-
|
519
|
+
It’s possible to include Javascript and CSS alongside components, sometimes called "sidecar" assets or files.
|
520
|
+
|
521
|
+
To use the Webpacker gem to compile sidecar assets located in `app/components`:
|
522
|
+
|
523
|
+
1. In `config/webpacker.yml`, add `"app/components"` to the `resolved_paths` array (e.g. `resolved_paths: ["app/components"]`).
|
524
|
+
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:
|
525
|
+
|
526
|
+
```js
|
527
|
+
import "../components"
|
528
|
+
```
|
529
|
+
|
530
|
+
Then, in `app/javascript/components.js`, add:
|
531
|
+
|
532
|
+
```js
|
533
|
+
function importAll(r) {
|
534
|
+
r.keys().forEach(r)
|
535
|
+
}
|
536
|
+
|
537
|
+
importAll(require.context("../components", true, /_component.js$/))
|
538
|
+
```
|
539
|
+
|
540
|
+
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.
|
541
|
+
|
542
|
+
#### Encapsulating sidecar assets
|
543
|
+
|
544
|
+
Ideally, sidecar Javascript/CSS should not "leak" out of the context of its associated component.
|
545
|
+
|
546
|
+
One approach is to use Web Components, which contain all Javascript functionality, internal markup, and styles within the shadow root of the Web Component.
|
547
|
+
|
548
|
+
For example:
|
549
|
+
|
550
|
+
`app/components/comment_component.rb`
|
551
|
+
```ruby
|
552
|
+
class CommentComponent < ViewComponent::Base
|
553
|
+
def initialize(comment:)
|
554
|
+
@comment = comment
|
555
|
+
end
|
556
|
+
|
557
|
+
def commenter
|
558
|
+
@comment.user
|
559
|
+
end
|
560
|
+
|
561
|
+
def commenter_name
|
562
|
+
commenter.name
|
563
|
+
end
|
564
|
+
|
565
|
+
def avatar
|
566
|
+
commenter.avatar_image_url
|
567
|
+
end
|
568
|
+
|
569
|
+
def formatted_body
|
570
|
+
simple_format(@comment.body)
|
571
|
+
end
|
572
|
+
|
573
|
+
private
|
618
574
|
|
619
|
-
|
575
|
+
attr_reader :comment
|
576
|
+
end
|
577
|
+
```
|
578
|
+
|
579
|
+
`app/components/comment_component.html.erb`
|
580
|
+
```erb
|
581
|
+
<my-comment comment-id="<%= comment.id %>">
|
582
|
+
<time slot="posted" datetime="<%= comment.created_at.iso8601 %>"><%= comment.created_at.strftime("%b %-d") %></time>
|
583
|
+
|
584
|
+
<div slot="avatar"><img src="<%= avatar %>" /></div>
|
585
|
+
|
586
|
+
<div slot="author"><%= commenter_name %></div>
|
587
|
+
|
588
|
+
<div slot="body"><%= formatted_body %></div>
|
589
|
+
</my-comment>
|
590
|
+
```
|
591
|
+
|
592
|
+
`app/components/comment_component.js`
|
593
|
+
```js
|
594
|
+
class Comment extends HTMLElement {
|
595
|
+
styles() {
|
596
|
+
return `
|
597
|
+
:host {
|
598
|
+
display: block;
|
599
|
+
}
|
600
|
+
::slotted(time) {
|
601
|
+
float: right;
|
602
|
+
font-size: 0.75em;
|
603
|
+
}
|
604
|
+
.commenter { font-weight: bold; }
|
605
|
+
.body { … }
|
606
|
+
`
|
607
|
+
}
|
608
|
+
|
609
|
+
constructor() {
|
610
|
+
super()
|
611
|
+
const shadow = this.attachShadow({mode: 'open'});
|
612
|
+
shadow.innerHTML = `
|
613
|
+
<style>
|
614
|
+
${this.styles()}
|
615
|
+
</style>
|
616
|
+
<slot name="posted"></slot>
|
617
|
+
<div class="commenter">
|
618
|
+
<slot name="avatar"></slot> <slot name="author"></slot>
|
619
|
+
</div>
|
620
|
+
<div class="body">
|
621
|
+
<slot name="body"></slot>
|
622
|
+
</div>
|
623
|
+
`
|
624
|
+
}
|
625
|
+
}
|
626
|
+
customElements.define('my-comment', Comment)
|
627
|
+
```
|
628
|
+
|
629
|
+
##### Stimulus
|
630
|
+
|
631
|
+
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`:
|
632
|
+
|
633
|
+
```js
|
634
|
+
const application = Application.start()
|
635
|
+
const context = require.context("controllers", true, /.js$/)
|
636
|
+
const context_components = require.context("../../components", true, /_controller.js$/)
|
637
|
+
application.load(
|
638
|
+
definitionsFromContext(context).concat(
|
639
|
+
definitionsFromContext(context_components)
|
640
|
+
)
|
641
|
+
)
|
642
|
+
```
|
620
643
|
|
621
|
-
|
644
|
+
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.
|
645
|
+
|
646
|
+
## Frequently Asked Questions
|
647
|
+
|
648
|
+
### Can I use other templating languages besides ERB?
|
622
649
|
|
623
|
-
|
650
|
+
Yes. ViewComponent is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
|
624
651
|
|
625
652
|
### Isn't this just like X library?
|
626
653
|
|
627
|
-
|
654
|
+
ViewComponent is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
|
628
655
|
|
629
656
|
- [trailblazer/cells](https://github.com/trailblazer/cells)
|
630
657
|
- [dry-rb/dry-view](https://github.com/dry-rb/dry-view)
|
@@ -633,6 +660,7 @@ Inline templates have been removed (for now) due to concerns raised by [@soutaro
|
|
633
660
|
|
634
661
|
## Resources
|
635
662
|
|
663
|
+
- [Encapsulating Views, RailsConf 2020](https://youtu.be/YVYRus_2KZM)
|
636
664
|
- [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
|
637
665
|
- [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
|
638
666
|
- [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
|
@@ -648,7 +676,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
|
|
648
676
|
|
649
677
|
## Contributors
|
650
678
|
|
651
|
-
|
679
|
+
ViewComponent is built by:
|
652
680
|
|
653
681
|
|<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
682
|
|:---:|:---:|:---:|:---:|:---:|
|
@@ -687,4 +715,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
|
|
687
715
|
|
688
716
|
## License
|
689
717
|
|
690
|
-
|
718
|
+
ViewComponent is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
require "rails/application_controller"
|
4
4
|
|
5
|
-
class
|
6
|
-
prepend_view_path File.expand_path("
|
5
|
+
class ViewComponentsController < Rails::ApplicationController # :nodoc:
|
6
|
+
prepend_view_path File.expand_path("../views", __dir__)
|
7
7
|
|
8
8
|
around_action :set_locale, only: :previews
|
9
9
|
before_action :find_preview, only: :previews
|
@@ -16,26 +16,19 @@ class Rails::ViewComponentsController < Rails::ApplicationController # :nodoc:
|
|
16
16
|
def index
|
17
17
|
@previews = ViewComponent::Preview.all
|
18
18
|
@page_title = "Component Previews"
|
19
|
-
# rubocop:disable GitHub/RailsControllerRenderPathsExist
|
20
|
-
render "components/index"
|
21
|
-
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
22
19
|
end
|
23
20
|
|
24
21
|
def previews
|
25
22
|
if params[:path] == @preview.preview_name
|
26
23
|
@page_title = "Component Previews for #{@preview.preview_name}"
|
27
|
-
|
28
|
-
render "components/previews"
|
29
|
-
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
24
|
+
render "view_components/previews"
|
30
25
|
else
|
31
26
|
prepend_application_view_paths
|
32
27
|
@example_name = File.basename(params[:path])
|
33
28
|
@render_args = @preview.render_args(@example_name, params: params.permit!)
|
34
29
|
layout = @render_args[:layout]
|
35
30
|
opts = layout.nil? ? {} : { layout: layout }
|
36
|
-
|
37
|
-
render "components/preview", **opts
|
38
|
-
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
31
|
+
render "view_components/preview", opts
|
39
32
|
end
|
40
33
|
end
|
41
34
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<% @previews.each do |preview| %>
|
2
|
+
<h3><%= link_to preview.preview_name.titleize, preview_view_component_path(preview.preview_name) %></h3>
|
3
|
+
<ul>
|
4
|
+
<% preview.examples.each do |preview_example| %>
|
5
|
+
<li><%= link_to preview_example, preview_view_component_path("#{preview.preview_name}/#{preview_example}") %></li>
|
6
|
+
<% end %>
|
7
|
+
</ul>
|
8
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render(@render_args[:component], @render_args[:args], &@render_args[:block])%>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<h3><%= @preview.preview_name.titleize %></h3>
|
2
2
|
<ul>
|
3
3
|
<% @preview.examples.each do |example| %>
|
4
|
-
<li><%= link_to example, "
|
4
|
+
<li><%= link_to example, preview_view_component_path("#{@preview.preview_name}/#{example}") %></li>
|
5
5
|
<% end %>
|
6
6
|
</ul>
|
data/lib/view_component/base.rb
CHANGED
@@ -10,15 +10,11 @@ module ViewComponent
|
|
10
10
|
include ActiveSupport::Configurable
|
11
11
|
include ViewComponent::Previewable
|
12
12
|
|
13
|
+
# For CSRF authenticity tokens in forms
|
13
14
|
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
|
14
15
|
|
15
|
-
class_attribute :content_areas
|
16
|
-
self.content_areas = [] # default doesn't work until Rails 5.2
|
17
|
-
|
18
|
-
# Render a component collection.
|
19
|
-
def self.with_collection(*args)
|
20
|
-
Collection.new(self, *args)
|
21
|
-
end
|
16
|
+
class_attribute :content_areas
|
17
|
+
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
22
18
|
|
23
19
|
# Entrypoint for rendering components.
|
24
20
|
#
|
@@ -45,17 +41,29 @@ module ViewComponent
|
|
45
41
|
# <span title="greeting">Hello, world!</span>
|
46
42
|
#
|
47
43
|
def render_in(view_context, &block)
|
48
|
-
self.class.compile
|
44
|
+
self.class.compile(raise_errors: true)
|
45
|
+
|
49
46
|
@view_context = view_context
|
50
|
-
@view_renderer ||= view_context.view_renderer
|
51
47
|
@lookup_context ||= view_context.lookup_context
|
48
|
+
|
49
|
+
# required for path helpers in older Rails versions
|
50
|
+
@view_renderer ||= view_context.view_renderer
|
51
|
+
|
52
|
+
# For content_for
|
52
53
|
@view_flow ||= view_context.view_flow
|
54
|
+
|
55
|
+
# For i18n
|
53
56
|
@virtual_path ||= virtual_path
|
57
|
+
|
58
|
+
# For template variants (+phone, +desktop, etc.)
|
54
59
|
@variant = @lookup_context.variants.first
|
55
60
|
|
61
|
+
# For caching, such as #cache_if
|
62
|
+
@current_template = nil unless defined?(@current_template)
|
56
63
|
old_current_template = @current_template
|
57
64
|
@current_template = self
|
58
65
|
|
66
|
+
# Assign captured content passed to component as a block to @content
|
59
67
|
@content = view_context.capture(self, &block) if block_given?
|
60
68
|
|
61
69
|
before_render_check
|
@@ -77,12 +85,10 @@ module ViewComponent
|
|
77
85
|
true
|
78
86
|
end
|
79
87
|
|
80
|
-
def self.short_identifier
|
81
|
-
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
82
|
-
end
|
83
|
-
|
84
88
|
def initialize(*); end
|
85
89
|
|
90
|
+
# If trying to render a partial or template inside a component,
|
91
|
+
# pass the render call to the parent view_context.
|
86
92
|
def render(options = {}, args = {}, &block)
|
87
93
|
if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
|
88
94
|
view_context.render(options, args, &block)
|
@@ -95,7 +101,7 @@ module ViewComponent
|
|
95
101
|
@controller ||= view_context.controller
|
96
102
|
end
|
97
103
|
|
98
|
-
# Provides a proxy to access helper methods
|
104
|
+
# Provides a proxy to access helper methods
|
99
105
|
def helpers
|
100
106
|
@helpers ||= view_context
|
101
107
|
end
|
@@ -105,14 +111,17 @@ module ViewComponent
|
|
105
111
|
self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
|
106
112
|
end
|
107
113
|
|
114
|
+
# For caching, such as #cache_if
|
108
115
|
def view_cache_dependencies
|
109
116
|
[]
|
110
117
|
end
|
111
118
|
|
112
|
-
|
119
|
+
# For caching, such as #cache_if
|
120
|
+
def format
|
113
121
|
@variant
|
114
122
|
end
|
115
123
|
|
124
|
+
# Assign the provided content to the content area accessor
|
116
125
|
def with(area, content = nil, &block)
|
117
126
|
unless content_areas.include?(area)
|
118
127
|
raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
|
@@ -128,6 +137,9 @@ module ViewComponent
|
|
128
137
|
|
129
138
|
private
|
130
139
|
|
140
|
+
# Exposes the current request to the component.
|
141
|
+
# Use sparingly as doing so introduces coupling
|
142
|
+
# that inhibits encapsulation & reuse.
|
131
143
|
def request
|
132
144
|
@request ||= controller.request
|
133
145
|
end
|
@@ -143,7 +155,18 @@ module ViewComponent
|
|
143
155
|
class << self
|
144
156
|
attr_accessor :source_location
|
145
157
|
|
158
|
+
# Render a component collection.
|
159
|
+
def with_collection(*args)
|
160
|
+
Collection.new(self, *args)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Provide identifier for ActionView template annotations
|
164
|
+
def short_identifier
|
165
|
+
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
166
|
+
end
|
167
|
+
|
146
168
|
def inherited(child)
|
169
|
+
# If we're in Rails, add application url_helpers to the component context
|
147
170
|
if defined?(Rails)
|
148
171
|
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
|
149
172
|
end
|
@@ -165,36 +188,52 @@ module ViewComponent
|
|
165
188
|
end
|
166
189
|
|
167
190
|
def compiled?
|
168
|
-
@compiled
|
169
|
-
end
|
191
|
+
@compiled ||= false
|
170
192
|
|
171
|
-
|
172
|
-
compile(raise_template_errors: true)
|
193
|
+
@compiled && ActionView::Base.cache_template_loading
|
173
194
|
end
|
174
195
|
|
175
196
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
176
|
-
#
|
177
|
-
#
|
178
|
-
|
197
|
+
#
|
198
|
+
# Do as much work as possible in this step, as doing so reduces the amount
|
199
|
+
# of work done each time a component is rendered.
|
200
|
+
def compile(raise_errors: false)
|
179
201
|
return if compiled?
|
180
202
|
|
181
203
|
if template_errors.present?
|
182
|
-
raise ViewComponent::TemplateError.new(template_errors) if
|
204
|
+
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
183
205
|
return false
|
184
206
|
end
|
185
207
|
|
208
|
+
# Remove any existing singleton methods,
|
209
|
+
# as Ruby warns when redefining a method.
|
210
|
+
remove_possible_singleton_method(:variants)
|
211
|
+
remove_possible_singleton_method(:collection_parameter)
|
212
|
+
remove_possible_singleton_method(:collection_counter_parameter)
|
213
|
+
remove_possible_singleton_method(:counter_argument_present?)
|
214
|
+
|
186
215
|
define_singleton_method(:variants) do
|
187
216
|
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
188
217
|
end
|
189
218
|
|
190
|
-
define_singleton_method(:
|
191
|
-
|
219
|
+
define_singleton_method(:collection_parameter) do
|
220
|
+
if provided_collection_parameter
|
221
|
+
provided_collection_parameter
|
222
|
+
else
|
223
|
+
name.demodulize.underscore.chomp("_component").to_sym
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
define_singleton_method(:collection_counter_parameter) do
|
228
|
+
"#{collection_parameter}_counter".to_sym
|
192
229
|
end
|
193
230
|
|
194
231
|
define_singleton_method(:counter_argument_present?) do
|
195
|
-
instance_method(:initialize).parameters.map(&:second).include?(
|
232
|
+
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
196
233
|
end
|
197
234
|
|
235
|
+
validate_collection_parameter! if raise_errors
|
236
|
+
|
198
237
|
# If template name annotations are turned on, a line is dynamically
|
199
238
|
# added with a comment. In this case, we want to return a different
|
200
239
|
# starting line number so errors that are raised will point to the
|
@@ -208,8 +247,13 @@ module ViewComponent
|
|
208
247
|
end
|
209
248
|
|
210
249
|
templates.each do |template|
|
250
|
+
# Remove existing compiled template methods,
|
251
|
+
# as Ruby warns when redefining a method.
|
252
|
+
method_name = call_method_name(template[:variant])
|
253
|
+
undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
|
254
|
+
|
211
255
|
class_eval <<-RUBY, template[:path], line_number
|
212
|
-
def #{
|
256
|
+
def #{method_name}
|
213
257
|
@output_buffer = ActionView::OutputBuffer.new
|
214
258
|
#{compiled_template(template[:path])}
|
215
259
|
end
|
@@ -236,21 +280,38 @@ module ViewComponent
|
|
236
280
|
if areas.include?(:content)
|
237
281
|
raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
|
238
282
|
end
|
239
|
-
attr_reader
|
283
|
+
attr_reader(*areas)
|
240
284
|
self.content_areas = areas
|
241
285
|
end
|
242
286
|
|
243
|
-
# Support overriding
|
287
|
+
# Support overriding collection parameter name
|
244
288
|
def with_collection_parameter(param)
|
245
|
-
@
|
289
|
+
@provided_collection_parameter = param
|
246
290
|
end
|
247
291
|
|
248
|
-
|
249
|
-
|
292
|
+
# Ensure the component initializer accepts the
|
293
|
+
# collection parameter. By default, we do not
|
294
|
+
# validate that the default parameter name
|
295
|
+
# is accepted, as support for collection
|
296
|
+
# rendering is optional.
|
297
|
+
def validate_collection_parameter!(validate_default: false)
|
298
|
+
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
299
|
+
|
300
|
+
return unless parameter
|
301
|
+
return if instance_method(:initialize).parameters.map(&:last).include?(parameter)
|
302
|
+
|
303
|
+
raise ArgumentError.new(
|
304
|
+
"#{self} initializer must accept " \
|
305
|
+
"`#{parameter}` collection parameter."
|
306
|
+
)
|
250
307
|
end
|
251
308
|
|
252
309
|
private
|
253
310
|
|
311
|
+
def provided_collection_parameter
|
312
|
+
@provided_collection_parameter ||= nil
|
313
|
+
end
|
314
|
+
|
254
315
|
def compiled_template(file_path)
|
255
316
|
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
256
317
|
template = File.read(file_path)
|
@@ -5,7 +5,9 @@ module ViewComponent
|
|
5
5
|
def render_in(view_context, &block)
|
6
6
|
iterator = ActionView::PartialIteration.new(@collection.size)
|
7
7
|
|
8
|
-
@component.compile
|
8
|
+
@component.compile(raise_errors: true)
|
9
|
+
@component.validate_collection_parameter!(validate_default: true)
|
10
|
+
|
9
11
|
@collection.map do |item|
|
10
12
|
content = @component.new(component_options(item, iterator)).render_in(view_context, &block)
|
11
13
|
iterator.iterate!
|
@@ -30,8 +32,8 @@ module ViewComponent
|
|
30
32
|
end
|
31
33
|
|
32
34
|
def component_options(item, iterator)
|
33
|
-
item_options = { @component.
|
34
|
-
item_options[@component.
|
35
|
+
item_options = { @component.collection_parameter => item }
|
36
|
+
item_options[@component.collection_counter_parameter] = iterator.index + 1 if @component.counter_argument_present?
|
35
37
|
|
36
38
|
@options.merge(item_options)
|
37
39
|
end
|
@@ -11,6 +11,7 @@ module ViewComponent
|
|
11
11
|
options = app.config.view_component
|
12
12
|
|
13
13
|
options.show_previews = Rails.env.development? if options.show_previews.nil?
|
14
|
+
options.preview_route ||= ViewComponent::Base.preview_route
|
14
15
|
|
15
16
|
if options.show_previews
|
16
17
|
options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/components/previews" : nil
|
@@ -64,8 +65,8 @@ module ViewComponent
|
|
64
65
|
|
65
66
|
if options.show_previews
|
66
67
|
app.routes.prepend do
|
67
|
-
get
|
68
|
-
get "
|
68
|
+
get options.preview_route, to: "view_components#index", as: :preview_view_components, internal: true
|
69
|
+
get "#{options.preview_route}/*path", to: "view_components#previews", as: :preview_view_component, internal: true
|
69
70
|
end
|
70
71
|
end
|
71
72
|
end
|
@@ -23,6 +23,7 @@ module ViewComponent # :nodoc:
|
|
23
23
|
example_params_names = instance_method(example).parameters.map(&:last)
|
24
24
|
provided_params = params.slice(*example_params_names).to_h.symbolize_keys
|
25
25
|
result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)
|
26
|
+
@layout = nil unless defined?(@layout)
|
26
27
|
result.merge(layout: @layout)
|
27
28
|
end
|
28
29
|
|
@@ -20,6 +20,16 @@ module ViewComponent # :nodoc:
|
|
20
20
|
# Defaults to +true+ for development environment
|
21
21
|
#
|
22
22
|
mattr_accessor :show_previews, instance_writer: false
|
23
|
+
|
24
|
+
# Set the entry route for component previews through app configuration:
|
25
|
+
#
|
26
|
+
# config.view_component.preview_route = "/previews"
|
27
|
+
#
|
28
|
+
# Defaults to +/rails/view_components+ when `show_previews' is enabled
|
29
|
+
#
|
30
|
+
mattr_accessor :preview_route, instance_writer: false do
|
31
|
+
"/rails/view_components"
|
32
|
+
end
|
23
33
|
end
|
24
34
|
end
|
25
35
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: view_component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -64,14 +64,14 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - '='
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: 5.
|
67
|
+
version: 5.6.0
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - '='
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 5.
|
74
|
+
version: 5.6.0
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: haml
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -152,7 +152,10 @@ files:
|
|
152
152
|
- CHANGELOG.md
|
153
153
|
- LICENSE.txt
|
154
154
|
- README.md
|
155
|
-
- app/controllers/
|
155
|
+
- app/controllers/view_components_controller.rb
|
156
|
+
- app/views/view_components/index.html.erb
|
157
|
+
- app/views/view_components/preview.html.erb
|
158
|
+
- app/views/view_components/previews.html.erb
|
156
159
|
- lib/rails/generators/component/USAGE
|
157
160
|
- lib/rails/generators/component/component_generator.rb
|
158
161
|
- lib/rails/generators/component/templates/component.rb.tt
|
@@ -166,10 +169,6 @@ files:
|
|
166
169
|
- lib/rails/generators/slim/templates/component.html.slim.tt
|
167
170
|
- lib/rails/generators/test_unit/component_generator.rb
|
168
171
|
- lib/rails/generators/test_unit/templates/component_test.rb.tt
|
169
|
-
- lib/railties/lib/rails.rb
|
170
|
-
- lib/railties/lib/rails/templates/rails/components/index.html.erb
|
171
|
-
- lib/railties/lib/rails/templates/rails/components/preview.html.erb
|
172
|
-
- lib/railties/lib/rails/templates/rails/components/previews.html.erb
|
173
172
|
- lib/view_component.rb
|
174
173
|
- lib/view_component/base.rb
|
175
174
|
- lib/view_component/collection.rb
|
data/lib/railties/lib/rails.rb
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
<% @previews.each do |preview| %>
|
2
|
-
<h3><%= link_to preview.preview_name.titleize, "/rails/view_components/#{preview.preview_name}" %></h3>
|
3
|
-
<ul>
|
4
|
-
<% preview.examples.each do |preview_example| %>
|
5
|
-
<li><%= link_to preview_example, "/rails/view_components/#{preview.preview_name}/#{preview_example}" %></li>
|
6
|
-
<% end %>
|
7
|
-
</ul>
|
8
|
-
<% end %>
|
@@ -1 +0,0 @@
|
|
1
|
-
<%= render(@render_args[:component], **@render_args[:args], &@render_args[:block])%>
|