view_component 2.20.0 → 2.23.1
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 +40 -0
- data/README.md +3 -1109
- data/lib/rails/generators/component/templates/component.rb.tt +2 -0
- data/lib/view_component/base.rb +20 -16
- data/lib/view_component/compiler.rb +21 -6
- data/lib/view_component/engine.rb +1 -1
- data/lib/view_component/slot_v2.rb +65 -0
- data/lib/view_component/slotable.rb +14 -0
- data/lib/view_component/slotable_v2.rb +249 -0
- data/lib/view_component/version.rb +2 -2
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2282359cc779d83fa6aa65b912fe4e4e70d2dbd6c8939b2bd0f3e21bd2eb9e7
|
4
|
+
data.tar.gz: c97fffc33ed1ffb6cd4598136c30c1b5990f1f150e8a7fc04ecfae933ae6c271
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec8397db82e97af474d4d25c8fde9346c73de00fdc37cd108640983a3230c08eae4c95566e8c86a96ad78c7e0c04aabf028f3f9c3e3040111d2b84d43c92bd53
|
7
|
+
data.tar.gz: 152e91807eff69a99066d20d977ec9092c71f44754fa2f2fd76cd69be4f221e47cdfd01c413aaac60f7334c4d852f86a2a8514f395c8ed773407203c82d77fe0
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,46 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 2.23.1
|
6
|
+
|
7
|
+
* Fixed out-of-order rendering bug in `ActionView::SlotableV2`
|
8
|
+
|
9
|
+
*Blake Williams*
|
10
|
+
|
11
|
+
## 2.23.0
|
12
|
+
|
13
|
+
* Add `ActionView::SlotableV2`
|
14
|
+
* `with_slot` becomes `renders_one`.
|
15
|
+
* `with_slot collection: true` becomes `renders_many`.
|
16
|
+
* Slot definitions now accept either a component class, component class name, or a lambda instead of a `class_name:` keyword argument.
|
17
|
+
* Slots now support positional arguments.
|
18
|
+
* Slots no longer use the `content` attribute to render content, instead relying on `to_s`. e.g. `<%= my_slot %>`.
|
19
|
+
* Slot values are no longer set via the `slot` method, and instead use the name of the slot.
|
20
|
+
|
21
|
+
*Blake Williams*
|
22
|
+
|
23
|
+
* Add `frozen_string_literal: true` to generated component template.
|
24
|
+
|
25
|
+
*Max Beizer*
|
26
|
+
|
27
|
+
## 2.22.1
|
28
|
+
|
29
|
+
* Revert refactor that broke rendering for some users.
|
30
|
+
|
31
|
+
*Joel Hawksley*
|
32
|
+
|
33
|
+
## 2.22.0
|
34
|
+
|
35
|
+
* Add #with_variant to enable inline component variant rendering without template files.
|
36
|
+
|
37
|
+
*Nathan Jones*
|
38
|
+
|
39
|
+
## 2.21.0
|
40
|
+
|
41
|
+
* Only compile components at application initialization if eager loading is enabled.
|
42
|
+
|
43
|
+
*Joel Hawksley*
|
44
|
+
|
5
45
|
## 2.20.0
|
6
46
|
|
7
47
|
* Don't add `/test/components/previews` to preview_paths if directory doesn't exist.
|
data/README.md
CHANGED
@@ -1,16 +1,10 @@
|
|
1
1
|
# ViewComponent
|
2
2
|
|
3
|
-
|
3
|
+
A framework for building reusable, testable & encapsulated view components in Ruby on Rails.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Documentation
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
## Compatibility
|
10
|
-
|
11
|
-
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).
|
12
|
-
|
13
|
-
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+.
|
7
|
+
See [viewcomponent.org](https://viewcomponent.org/) for documentation.
|
14
8
|
|
15
9
|
## Installation
|
16
10
|
|
@@ -20,1110 +14,10 @@ In `Gemfile`, add:
|
|
20
14
|
gem "view_component", require: "view_component/engine"
|
21
15
|
```
|
22
16
|
|
23
|
-
## Guide
|
24
|
-
|
25
|
-
### What are components?
|
26
|
-
|
27
|
-
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).
|
28
|
-
|
29
|
-
Components are most effective in cases where view code is reused or benefits from being tested directly.
|
30
|
-
|
31
|
-
### Why should I use components?
|
32
|
-
|
33
|
-
#### Testing
|
34
|
-
|
35
|
-
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.
|
36
|
-
|
37
|
-
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.
|
38
|
-
|
39
|
-
With ViewComponent, integration tests can be reserved for end-to-end assertions, with permutations and corner cases covered at the unit level.
|
40
|
-
|
41
|
-
#### Data Flow
|
42
|
-
|
43
|
-
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.
|
44
|
-
|
45
|
-
ViewComponents use a standard Ruby initializer that clearly defines what is needed to render, making them easier (and safer) to reuse than partials.
|
46
|
-
|
47
|
-
#### Performance
|
48
|
-
|
49
|
-
Based on our [benchmarks](performance/benchmark.rb), ViewComponents are ~10x faster than partials.
|
50
|
-
|
51
|
-
#### Standards
|
52
|
-
|
53
|
-
Views often fail basic Ruby code quality standards: long methods, deep conditional nesting, and mystery guests abound.
|
54
|
-
|
55
|
-
ViewComponents are Ruby objects, making it easy to follow (and enforce) code quality standards.
|
56
|
-
|
57
|
-
### Building components
|
58
|
-
|
59
|
-
#### Conventions
|
60
|
-
|
61
|
-
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`.
|
62
|
-
|
63
|
-
Component names end in -`Component`.
|
64
|
-
|
65
|
-
Component module names are plural, as for controllers and jobs: `Users::AvatarComponent`
|
66
|
-
|
67
|
-
Name components for what they render, not what they accept. (`AvatarComponent` instead of `UserComponent`)
|
68
|
-
|
69
|
-
#### Quick start
|
70
|
-
|
71
|
-
Use the component generator to create a new ViewComponent.
|
72
|
-
|
73
|
-
The generator accepts a component name and a list of arguments:
|
74
|
-
|
75
|
-
```bash
|
76
|
-
bin/rails generate component Example title content
|
77
|
-
invoke test_unit
|
78
|
-
create test/components/example_component_test.rb
|
79
|
-
create app/components/example_component.rb
|
80
|
-
create app/components/example_component.html.erb
|
81
|
-
```
|
82
|
-
|
83
|
-
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`.
|
84
|
-
|
85
|
-
The template engine can also be passed as an option to the generator:
|
86
|
-
|
87
|
-
```bash
|
88
|
-
bin/rails generate component Example title content --template-engine slim
|
89
|
-
```
|
90
|
-
|
91
|
-
#### Implementation
|
92
|
-
|
93
|
-
A ViewComponent is a Ruby file and corresponding template file with the same base name:
|
94
|
-
|
95
|
-
`app/components/test_component.rb`:
|
96
|
-
|
97
|
-
```ruby
|
98
|
-
class TestComponent < ViewComponent::Base
|
99
|
-
def initialize(title:)
|
100
|
-
@title = title
|
101
|
-
end
|
102
|
-
end
|
103
|
-
```
|
104
|
-
|
105
|
-
`app/components/test_component.html.erb`:
|
106
|
-
|
107
|
-
```erb
|
108
|
-
<span title="<%= @title %>"><%= content %></span>
|
109
|
-
```
|
110
|
-
|
111
|
-
Rendered in a view as:
|
112
|
-
|
113
|
-
```erb
|
114
|
-
<%= render(TestComponent.new(title: "my title")) do %>
|
115
|
-
Hello, World!
|
116
|
-
<% end %>
|
117
|
-
```
|
118
|
-
|
119
|
-
Returning:
|
120
|
-
|
121
|
-
```html
|
122
|
-
<span title="my title">Hello, World!</span>
|
123
|
-
```
|
124
|
-
|
125
|
-
#### Content Areas
|
126
|
-
|
127
|
-
Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor.
|
128
|
-
|
129
|
-
ViewComponents can declare additional content areas. For example:
|
130
|
-
|
131
|
-
`app/components/modal_component.rb`:
|
132
|
-
|
133
|
-
```ruby
|
134
|
-
class ModalComponent < ViewComponent::Base
|
135
|
-
with_content_areas :header, :body
|
136
|
-
end
|
137
|
-
```
|
138
|
-
|
139
|
-
`app/components/modal_component.html.erb`:
|
140
|
-
|
141
|
-
```erb
|
142
|
-
<div class="modal">
|
143
|
-
<div class="header"><%= header %></div>
|
144
|
-
<div class="body"><%= body %></div>
|
145
|
-
</div>
|
146
|
-
```
|
147
|
-
|
148
|
-
Rendered in a view as:
|
149
|
-
|
150
|
-
```erb
|
151
|
-
<%= render(ModalComponent.new) do |component| %>
|
152
|
-
<% component.with(:header) do %>
|
153
|
-
Hello Jane
|
154
|
-
<% end %>
|
155
|
-
<% component.with(:body) do %>
|
156
|
-
<p>Have a great day.</p>
|
157
|
-
<% end %>
|
158
|
-
<% end %>
|
159
|
-
```
|
160
|
-
|
161
|
-
Returning:
|
162
|
-
|
163
|
-
```html
|
164
|
-
<div class="modal">
|
165
|
-
<div class="header">Hello Jane</div>
|
166
|
-
<div class="body"><p>Have a great day.</p></div>
|
167
|
-
</div>
|
168
|
-
```
|
169
|
-
|
170
|
-
#### Slots (experimental)
|
171
|
-
|
172
|
-
_Slots are currently under development as a successor to Content Areas. The Slot APIs should be considered unfinished and subject to breaking changes in non-major releases of ViewComponent._
|
173
|
-
|
174
|
-
Slots enable multiple blocks of content to be passed to a single ViewComponent, reducing the need for sub-components (e.g. ModalHeader, ModalBody).
|
175
|
-
|
176
|
-
By default, slots can be rendered once per component. They provide an accessor with the name of the slot (`#header`) that returns an instance of `ViewComponent::Slot`, etc.
|
177
|
-
|
178
|
-
Slots declared with `collection: true` can be rendered multiple times. They provide an accessor with the pluralized name of the slot (`#rows`), which is an Array of `ViewComponent::Slot` instances.
|
179
|
-
|
180
|
-
To learn more about the design of the Slots API, see [#348](https://github.com/github/view_component/pull/348) and [#325](https://github.com/github/view_component/discussions/325).
|
181
|
-
|
182
|
-
##### Defining Slots
|
183
|
-
|
184
|
-
Slots are defined by `with_slot`:
|
185
|
-
|
186
|
-
`with_slot :header`
|
187
|
-
|
188
|
-
To define a collection slot, add `collection: true`:
|
189
|
-
|
190
|
-
`with_slot :row, collection: true`
|
191
|
-
|
192
|
-
To define a slot with a custom Ruby class, pass `class_name`:
|
193
|
-
|
194
|
-
`with_slot :body, class_name: 'BodySlot'`
|
195
|
-
|
196
|
-
_Note: Slot classes must be subclasses of `ViewComponent::Slot`._
|
197
|
-
|
198
|
-
##### Example ViewComponent with Slots
|
199
|
-
|
200
|
-
`# box_component.rb`
|
201
|
-
|
202
|
-
```ruby
|
203
|
-
class BoxComponent < ViewComponent::Base
|
204
|
-
include ViewComponent::Slotable
|
205
|
-
|
206
|
-
with_slot :body, :footer
|
207
|
-
with_slot :header, class_name: "Header"
|
208
|
-
with_slot :row, collection: true, class_name: "Row"
|
209
|
-
|
210
|
-
class Header < ViewComponent::Slot
|
211
|
-
def initialize(classes: "")
|
212
|
-
@classes = classes
|
213
|
-
end
|
214
|
-
|
215
|
-
def classes
|
216
|
-
"Box-header #{@classes}"
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
class Row < ViewComponent::Slot
|
221
|
-
def initialize(theme: :gray)
|
222
|
-
@theme = theme
|
223
|
-
end
|
224
|
-
|
225
|
-
def theme_class_name
|
226
|
-
case @theme
|
227
|
-
when :gray
|
228
|
-
"Box-row--gray"
|
229
|
-
when :hover_gray
|
230
|
-
"Box-row--hover-gray"
|
231
|
-
when :yellow
|
232
|
-
"Box-row--yellow"
|
233
|
-
when :blue
|
234
|
-
"Box-row--blue"
|
235
|
-
when :hover_blue
|
236
|
-
"Box-row--hover-blue"
|
237
|
-
else
|
238
|
-
"Box-row--gray"
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
end
|
243
|
-
```
|
244
|
-
|
245
|
-
`# box_component.html.erb`
|
246
|
-
|
247
|
-
```erb
|
248
|
-
<div class="Box">
|
249
|
-
<% if header %>
|
250
|
-
<div class="<%= header.classes %>">
|
251
|
-
<%= header.content %>
|
252
|
-
</div>
|
253
|
-
<% end %>
|
254
|
-
<% if body %>
|
255
|
-
<div class="Box-body">
|
256
|
-
<%= body.content %>
|
257
|
-
</div>
|
258
|
-
<% end %>
|
259
|
-
<% if rows.any? %>
|
260
|
-
<ul>
|
261
|
-
<% rows.each do |row| %>
|
262
|
-
<li class="Box-row <%= row.theme_class_name %>">
|
263
|
-
<%= row.content %>
|
264
|
-
</li>
|
265
|
-
<% end %>
|
266
|
-
</ul>
|
267
|
-
<% end %>
|
268
|
-
<% if footer %>
|
269
|
-
<div class="Box-footer">
|
270
|
-
<%= footer.content %>
|
271
|
-
</div>
|
272
|
-
<% end %>
|
273
|
-
</div>
|
274
|
-
```
|
275
|
-
|
276
|
-
`# index.html.erb`
|
277
|
-
|
278
|
-
```erb
|
279
|
-
<%= render(BoxComponent.new) do |component| %>
|
280
|
-
<% component.slot(:header, classes: "my-class-name") do %>
|
281
|
-
This is my header!
|
282
|
-
<% end %>
|
283
|
-
<% component.slot(:body) do %>
|
284
|
-
This is the body.
|
285
|
-
<% end %>
|
286
|
-
<% component.slot(:row) do %>
|
287
|
-
Row one
|
288
|
-
<% end %>
|
289
|
-
<% component.slot(:row, theme: :yellow) do %>
|
290
|
-
Yellow row
|
291
|
-
<% end %>
|
292
|
-
<% component.slot(:footer) do %>
|
293
|
-
This is the footer.
|
294
|
-
<% end %>
|
295
|
-
<% end %>
|
296
|
-
```
|
297
|
-
|
298
|
-
### Inline Component
|
299
|
-
|
300
|
-
ViewComponents can render without a template file, by defining a `call` method:
|
301
|
-
|
302
|
-
`app/components/inline_component.rb`:
|
303
|
-
|
304
|
-
```ruby
|
305
|
-
class InlineComponent < ViewComponent::Base
|
306
|
-
def call
|
307
|
-
if active?
|
308
|
-
link_to "Cancel integration", integration_path, method: :delete
|
309
|
-
else
|
310
|
-
link_to "Integrate now!", integration_path
|
311
|
-
end
|
312
|
-
end
|
313
|
-
end
|
314
|
-
```
|
315
|
-
|
316
|
-
It is also possible to define methods for variants:
|
317
|
-
|
318
|
-
```ruby
|
319
|
-
class InlineVariantComponent < ViewComponent::Base
|
320
|
-
def call_phone
|
321
|
-
link_to "Phone", phone_path
|
322
|
-
end
|
323
|
-
|
324
|
-
def call
|
325
|
-
link_to "Default", default_path
|
326
|
-
end
|
327
|
-
end
|
328
|
-
```
|
329
|
-
|
330
|
-
### Template Inheritance
|
331
|
-
|
332
|
-
Components that subclass another component inherit the parent component's
|
333
|
-
template if they don't define their own template.
|
334
|
-
|
335
|
-
```ruby
|
336
|
-
# If `my_link_component.html.erb` is not defined the component will fall back
|
337
|
-
# to `LinkComponent`s template
|
338
|
-
class MyLinkComponent < LinkComponent
|
339
|
-
end
|
340
|
-
```
|
341
|
-
|
342
|
-
### Sidecar Assets
|
343
|
-
|
344
|
-
ViewComponents supports two options for defining view files.
|
345
|
-
|
346
|
-
#### Sidecar view
|
347
|
-
|
348
|
-
The simplest option is to place the view next to the Ruby component:
|
349
|
-
|
350
|
-
```console
|
351
|
-
app/components
|
352
|
-
├── ...
|
353
|
-
├── test_component.rb
|
354
|
-
├── test_component.html.erb
|
355
|
-
├── ...
|
356
|
-
```
|
357
|
-
|
358
|
-
#### Sidecar directory
|
359
|
-
|
360
|
-
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.
|
361
|
-
|
362
|
-
```console
|
363
|
-
app/components
|
364
|
-
├── ...
|
365
|
-
├── example_component.rb
|
366
|
-
├── example_component
|
367
|
-
| ├── example_component.css
|
368
|
-
| ├── example_component.html.erb
|
369
|
-
| └── example_component.js
|
370
|
-
├── ...
|
371
|
-
```
|
372
|
-
|
373
|
-
To generate a component with a sidecar directory, use the `--sidecar` flag:
|
374
|
-
|
375
|
-
```console
|
376
|
-
bin/rails generate component Example title content --sidecar
|
377
|
-
invoke test_unit
|
378
|
-
create test/components/example_component_test.rb
|
379
|
-
create app/components/example_component.rb
|
380
|
-
create app/components/example_component/example_component.html.erb
|
381
|
-
```
|
382
|
-
|
383
|
-
#### Component file inside Sidecar directory
|
384
|
-
|
385
|
-
It's also possible to place the Ruby component file inside the sidecar directory, grouping all related files in the same folder:
|
386
|
-
|
387
|
-
_Note: Avoid giving your containing folder the same name as your `.rb` file or there will be a conflict between Module and Class definitions_
|
388
|
-
|
389
|
-
```console
|
390
|
-
app/components
|
391
|
-
├── ...
|
392
|
-
├── example
|
393
|
-
| ├── component.rb
|
394
|
-
| ├── component.css
|
395
|
-
| ├── component.html.erb
|
396
|
-
| └── component.js
|
397
|
-
├── ...
|
398
|
-
|
399
|
-
```
|
400
|
-
|
401
|
-
The component can then be rendered using the folder name as a namespace:
|
402
|
-
|
403
|
-
```erb
|
404
|
-
<%= render(Example::Component.new(title: "my title")) do %>
|
405
|
-
Hello, World!
|
406
|
-
<% end %>
|
407
|
-
```
|
408
|
-
|
409
|
-
### Conditional Rendering
|
410
|
-
|
411
|
-
Components can implement a `#render?` method to be called after initialization to determine if the component should render.
|
412
|
-
|
413
|
-
Traditionally, the logic for whether to render a view could go in either the component template:
|
414
|
-
|
415
|
-
`app/components/confirm_email_component.html.erb`
|
416
|
-
|
417
|
-
```erb
|
418
|
-
<% if user.requires_confirmation? %>
|
419
|
-
<div class="alert">Please confirm your email address.</div>
|
420
|
-
<% end %>
|
421
|
-
```
|
422
|
-
|
423
|
-
or the view that renders the component:
|
424
|
-
|
425
|
-
`app/views/_banners.html.erb`
|
426
|
-
|
427
|
-
```erb
|
428
|
-
<% if current_user.requires_confirmation? %>
|
429
|
-
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
|
430
|
-
<% end %>
|
431
|
-
```
|
432
|
-
|
433
|
-
Using the `#render?` hook simplifies the view:
|
434
|
-
|
435
|
-
`app/components/confirm_email_component.rb`
|
436
|
-
|
437
|
-
```ruby
|
438
|
-
class ConfirmEmailComponent < ViewComponent::Base
|
439
|
-
def initialize(user:)
|
440
|
-
@user = user
|
441
|
-
end
|
442
|
-
|
443
|
-
def render?
|
444
|
-
@user.requires_confirmation?
|
445
|
-
end
|
446
|
-
end
|
447
|
-
```
|
448
|
-
|
449
|
-
`app/components/confirm_email_component.html.erb`
|
450
|
-
|
451
|
-
```erb
|
452
|
-
<div class="banner">
|
453
|
-
Please confirm your email address.
|
454
|
-
</div>
|
455
|
-
```
|
456
|
-
|
457
|
-
`app/views/_banners.html.erb`
|
458
|
-
|
459
|
-
```erb
|
460
|
-
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
|
461
|
-
```
|
462
|
-
|
463
|
-
_To assert that a component has not been rendered, use `refute_component_rendered` from `ViewComponent::TestHelpers`._
|
464
|
-
|
465
|
-
### `before_render`
|
466
|
-
|
467
|
-
Components can define a `before_render` method to be called before a component is rendered, when `helpers` is able to be used:
|
468
|
-
|
469
|
-
`app/components/confirm_email_component.rb`
|
470
|
-
|
471
|
-
```ruby
|
472
|
-
class MyComponent < ViewComponent::Base
|
473
|
-
def before_render
|
474
|
-
@my_icon = helpers.star_icon
|
475
|
-
end
|
476
|
-
end
|
477
|
-
```
|
478
|
-
|
479
|
-
### Rendering collections
|
480
|
-
|
481
|
-
Use `with_collection` to render a ViewComponent with a collection:
|
482
|
-
|
483
|
-
`app/view/products/index.html.erb`
|
484
|
-
|
485
|
-
``` erb
|
486
|
-
<%= render(ProductComponent.with_collection(@products)) %>
|
487
|
-
```
|
488
|
-
|
489
|
-
`app/components/product_component.rb`
|
490
|
-
|
491
|
-
``` ruby
|
492
|
-
class ProductComponent < ViewComponent::Base
|
493
|
-
def initialize(product:)
|
494
|
-
@product = product
|
495
|
-
end
|
496
|
-
end
|
497
|
-
```
|
498
|
-
|
499
|
-
[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.
|
500
|
-
|
501
|
-
#### `with_collection_parameter`
|
502
|
-
|
503
|
-
Use `with_collection_parameter` to change the name of the collection parameter:
|
504
|
-
|
505
|
-
`app/components/product_component.rb`
|
506
|
-
|
507
|
-
``` ruby
|
508
|
-
class ProductComponent < ViewComponent::Base
|
509
|
-
with_collection_parameter :item
|
510
|
-
|
511
|
-
def initialize(item:)
|
512
|
-
@item = item
|
513
|
-
end
|
514
|
-
end
|
515
|
-
```
|
516
|
-
|
517
|
-
#### Additional arguments
|
518
|
-
|
519
|
-
Additional arguments besides the collection are passed to each component instance:
|
520
|
-
|
521
|
-
`app/view/products/index.html.erb`
|
522
|
-
|
523
|
-
``` erb
|
524
|
-
<%= render(ProductComponent.with_collection(@products, notice: "hi")) %>
|
525
|
-
```
|
526
|
-
|
527
|
-
`app/components/product_component.rb`
|
528
|
-
|
529
|
-
``` ruby
|
530
|
-
class ProductComponent < ViewComponent::Base
|
531
|
-
with_collection_parameter :item
|
532
|
-
|
533
|
-
def initialize(item:, notice:)
|
534
|
-
@item = item
|
535
|
-
@notice = notice
|
536
|
-
end
|
537
|
-
end
|
538
|
-
```
|
539
|
-
|
540
|
-
`app/components/product_component.html.erb`
|
541
|
-
|
542
|
-
``` erb
|
543
|
-
<li>
|
544
|
-
<h2><%= @item.name %></h2>
|
545
|
-
<span><%= @notice %></span>
|
546
|
-
</li>
|
547
|
-
```
|
548
|
-
|
549
|
-
#### Collection counter
|
550
|
-
|
551
|
-
ViewComponent defines a counter variable matching the parameter name above, followed by `_counter`. To access the variable, add it to `initialize` as an argument:
|
552
|
-
|
553
|
-
`app/components/product_component.rb`
|
554
|
-
|
555
|
-
``` ruby
|
556
|
-
class ProductComponent < ViewComponent::Base
|
557
|
-
def initialize(product:, product_counter:)
|
558
|
-
@product = product
|
559
|
-
@counter = product_counter
|
560
|
-
end
|
561
|
-
end
|
562
|
-
```
|
563
|
-
|
564
|
-
`app/components/product_component.html.erb`
|
565
|
-
|
566
|
-
``` erb
|
567
|
-
<li>
|
568
|
-
<%= @counter %> <%= @product.name %>
|
569
|
-
</li>
|
570
|
-
```
|
571
|
-
|
572
|
-
### Using helpers
|
573
|
-
|
574
|
-
Helper methods can be used through the `helpers` proxy:
|
575
|
-
|
576
|
-
```ruby
|
577
|
-
module IconHelper
|
578
|
-
def icon(name)
|
579
|
-
tag.i data: { feather: name.to_s.dasherize }
|
580
|
-
end
|
581
|
-
end
|
582
|
-
|
583
|
-
class UserComponent < ViewComponent::Base
|
584
|
-
def profile_icon
|
585
|
-
helpers.icon :user
|
586
|
-
end
|
587
|
-
end
|
588
|
-
```
|
589
|
-
|
590
|
-
Which can be used with `delegate`:
|
591
|
-
|
592
|
-
```ruby
|
593
|
-
class UserComponent < ViewComponent::Base
|
594
|
-
delegate :icon, to: :helpers
|
595
|
-
|
596
|
-
def profile_icon
|
597
|
-
icon :user
|
598
|
-
end
|
599
|
-
end
|
600
|
-
```
|
601
|
-
|
602
|
-
Helpers can also be used by including the helper:
|
603
|
-
|
604
|
-
```ruby
|
605
|
-
class UserComponent < ViewComponent::Base
|
606
|
-
include IconHelper
|
607
|
-
|
608
|
-
def profile_icon
|
609
|
-
icon :user
|
610
|
-
end
|
611
|
-
end
|
612
|
-
```
|
613
|
-
|
614
|
-
### Writing tests
|
615
|
-
|
616
|
-
Unit test components directly, using the `render_inline` test helper, asserting against the rendered output.
|
617
|
-
|
618
|
-
Capybara matchers are available if the gem is installed:
|
619
|
-
|
620
|
-
```ruby
|
621
|
-
require "view_component/test_case"
|
622
|
-
|
623
|
-
class MyComponentTest < ViewComponent::TestCase
|
624
|
-
def test_render_component
|
625
|
-
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
626
|
-
|
627
|
-
assert_selector("span[title='my title']", text: "Hello, World!")
|
628
|
-
end
|
629
|
-
end
|
630
|
-
```
|
631
|
-
|
632
|
-
In the absence of `capybara`, assert against the return value of `render_inline`, which is an instance of `Nokogiri::HTML::DocumentFragment`:
|
633
|
-
|
634
|
-
```ruby
|
635
|
-
def test_render_component
|
636
|
-
result = render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
637
|
-
|
638
|
-
assert_includes result.css("span[title='my title']").to_html, "Hello, World!"
|
639
|
-
end
|
640
|
-
```
|
641
|
-
|
642
|
-
Alternatively, assert against the raw output of the component, which is exposed as `rendered_component`:
|
643
|
-
|
644
|
-
```ruby
|
645
|
-
def test_render_component
|
646
|
-
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
647
|
-
|
648
|
-
assert_includes rendered_component, "Hello, World!"
|
649
|
-
end
|
650
|
-
```
|
651
|
-
|
652
|
-
To test components that use `with_content_areas`:
|
653
|
-
|
654
|
-
```ruby
|
655
|
-
def test_renders_content_areas_template_with_content
|
656
|
-
render_inline(ContentAreasComponent.new(footer: "Bye!")) do |component|
|
657
|
-
component.with(:title, "Hello!")
|
658
|
-
component.with(:body) { "Have a nice day." }
|
659
|
-
end
|
660
|
-
|
661
|
-
assert_selector(".title", text: "Hello!")
|
662
|
-
assert_selector(".body", text: "Have a nice day.")
|
663
|
-
assert_selector(".footer", text: "Bye!")
|
664
|
-
end
|
665
|
-
```
|
666
|
-
|
667
|
-
#### Action Pack Variants
|
668
|
-
|
669
|
-
Use the `with_variant` helper to test specific variants:
|
670
|
-
|
671
|
-
```ruby
|
672
|
-
def test_render_component_for_tablet
|
673
|
-
with_variant :tablet do
|
674
|
-
render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
|
675
|
-
|
676
|
-
assert_selector("span[title='my title']", text: "Hello, tablets!")
|
677
|
-
end
|
678
|
-
end
|
679
|
-
```
|
680
|
-
|
681
|
-
### Previewing Components
|
682
|
-
|
683
|
-
`ViewComponent::Preview`, like `ActionMailer::Preview`, provides a way to preview components in isolation:
|
684
|
-
|
685
|
-
`test/components/previews/test_component_preview.rb`
|
686
|
-
|
687
|
-
```ruby
|
688
|
-
class TestComponentPreview < ViewComponent::Preview
|
689
|
-
def with_default_title
|
690
|
-
render(TestComponent.new(title: "Test component default"))
|
691
|
-
end
|
692
|
-
|
693
|
-
def with_long_title
|
694
|
-
render(TestComponent.new(title: "This is a really long title to see how the component renders this"))
|
695
|
-
end
|
696
|
-
|
697
|
-
def with_content_block
|
698
|
-
render(TestComponent.new(title: "This component accepts a block of content")) do
|
699
|
-
tag.div do
|
700
|
-
content_tag(:span, "Hello")
|
701
|
-
end
|
702
|
-
end
|
703
|
-
end
|
704
|
-
end
|
705
|
-
```
|
706
|
-
|
707
|
-
Which generates <http://localhost:3000/rails/view_components/test_component/with_default_title>,
|
708
|
-
<http://localhost:3000/rails/view_components/test_component/with_long_title>,
|
709
|
-
and <http://localhost:3000/rails/view_components/test_component/with_content_block>.
|
710
|
-
|
711
|
-
It's also possible to set dynamic values from the params by setting them as arguments:
|
712
|
-
|
713
|
-
`test/components/previews/test_component_preview.rb`
|
714
|
-
|
715
|
-
```ruby
|
716
|
-
class TestComponentPreview < ViewComponent::Preview
|
717
|
-
def with_dynamic_title(title: "Test component default")
|
718
|
-
render(TestComponent.new(title: title))
|
719
|
-
end
|
720
|
-
end
|
721
|
-
```
|
722
|
-
|
723
|
-
Which enables passing in a value with <http://localhost:3000/rails/components/test_component/with_dynamic_title?title=Custom+title>.
|
724
|
-
|
725
|
-
The `ViewComponent::Preview` base class includes
|
726
|
-
[`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)
|
727
|
-
and [`content_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag) view helper methods.
|
728
|
-
|
729
|
-
Previews use the application layout by default, but can use a specific layout with the `layout` option:
|
730
|
-
|
731
|
-
`test/components/previews/test_component_preview.rb`
|
732
|
-
|
733
|
-
```ruby
|
734
|
-
class TestComponentPreview < ViewComponent::Preview
|
735
|
-
layout "admin"
|
736
|
-
|
737
|
-
...
|
738
|
-
end
|
739
|
-
```
|
740
|
-
|
741
|
-
You can also set a custom layout to be used by default for previews as well as the preview index pages via the `default_preview_layout` configuration option:
|
742
|
-
|
743
|
-
`config/application.rb`
|
744
|
-
|
745
|
-
```ruby
|
746
|
-
# Set the default layout to app/views/layouts/component_preview.html.erb
|
747
|
-
config.view_component.default_preview_layout = "component_preview"
|
748
|
-
```
|
749
|
-
|
750
|
-
Preview classes live in `test/components/previews`, which can be configured using the `preview_paths` option:
|
751
|
-
|
752
|
-
`config/application.rb`
|
753
|
-
|
754
|
-
```ruby
|
755
|
-
config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
|
756
|
-
```
|
757
|
-
|
758
|
-
Previews are served from <http://localhost:3000/rails/view_components> by default. To use a different endpoint, set the `preview_route` option:
|
759
|
-
|
760
|
-
`config/application.rb`
|
761
|
-
|
762
|
-
```ruby
|
763
|
-
config.view_component.preview_route = "/previews"
|
764
|
-
```
|
765
|
-
|
766
|
-
This example will make the previews available from <http://localhost:3000/previews>.
|
767
|
-
|
768
|
-
#### Preview templates
|
769
|
-
|
770
|
-
Given a preview `test/components/previews/cell_component_preview.rb`, template files can be defined at `test/components/previews/cell_component_preview/`:
|
771
|
-
|
772
|
-
`test/components/previews/cell_component_preview.rb`
|
773
|
-
|
774
|
-
```ruby
|
775
|
-
class CellComponentPreview < ViewComponent::Preview
|
776
|
-
def default
|
777
|
-
end
|
778
|
-
end
|
779
|
-
```
|
780
|
-
|
781
|
-
`test/components/previews/cell_component_preview/default.html.erb`
|
782
|
-
|
783
|
-
```erb
|
784
|
-
<table class="table">
|
785
|
-
<tbody>
|
786
|
-
<tr>
|
787
|
-
<%= render CellComponent.new %>
|
788
|
-
</tr>
|
789
|
-
</tbody>
|
790
|
-
</div>
|
791
|
-
```
|
792
|
-
|
793
|
-
To use a different location for preview templates, pass the `template` argument:
|
794
|
-
(the path should be relative to `config.view_component.preview_path`):
|
795
|
-
|
796
|
-
`test/components/previews/cell_component_preview.rb`
|
797
|
-
|
798
|
-
```ruby
|
799
|
-
class CellComponentPreview < ViewComponent::Preview
|
800
|
-
def default
|
801
|
-
render_with_template(template: 'custom_cell_component_preview/my_preview_template')
|
802
|
-
end
|
803
|
-
end
|
804
|
-
```
|
805
|
-
|
806
|
-
Values from `params` can be accessed through `locals`:
|
807
|
-
|
808
|
-
`test/components/previews/cell_component_preview.rb`
|
809
|
-
|
810
|
-
```ruby
|
811
|
-
class CellComponentPreview < ViewComponent::Preview
|
812
|
-
def default(title: "Default title", subtitle: "A subtitle")
|
813
|
-
render_with_template(locals: {
|
814
|
-
title: title,
|
815
|
-
subtitle: subtitle
|
816
|
-
})
|
817
|
-
end
|
818
|
-
end
|
819
|
-
```
|
820
|
-
|
821
|
-
Which enables passing in a value with <http://localhost:3000/rails/components/cell_component/default?title=Custom+title&subtitle=Another+subtitle>.
|
822
|
-
|
823
|
-
#### Configuring preview controller
|
824
|
-
|
825
|
-
Previews can be extended to allow users to add authentication, authorization, before actions, or anything that the end user would need to meet their needs using the `preview_controller` option:
|
826
|
-
|
827
|
-
`config/application.rb`
|
828
|
-
|
829
|
-
```ruby
|
830
|
-
config.view_component.preview_controller = "MyPreviewController"
|
831
|
-
```
|
832
|
-
|
833
|
-
#### Configuring TestController
|
834
|
-
|
835
|
-
Component tests assume the existence of an `ApplicationController` class, which be can be configured using the `test_controller` option:
|
836
|
-
|
837
|
-
`config/application.rb`
|
838
|
-
|
839
|
-
```ruby
|
840
|
-
config.view_component.test_controller = "BaseController"
|
841
|
-
```
|
842
|
-
|
843
|
-
### Setting up RSpec
|
844
|
-
|
845
|
-
To use RSpec, add the following:
|
846
|
-
|
847
|
-
`spec/rails_helper.rb`
|
848
|
-
|
849
|
-
```ruby
|
850
|
-
require "view_component/test_helpers"
|
851
|
-
|
852
|
-
RSpec.configure do |config|
|
853
|
-
config.include ViewComponent::TestHelpers, type: :component
|
854
|
-
end
|
855
|
-
```
|
856
|
-
|
857
|
-
Specs created by the generator have access to test helpers like `render_inline`.
|
858
|
-
|
859
|
-
To use component previews:
|
860
|
-
|
861
|
-
`config/application.rb`
|
862
|
-
|
863
|
-
```ruby
|
864
|
-
config.view_component.preview_paths << "#{Rails.root}/spec/components/previews"
|
865
|
-
```
|
866
|
-
|
867
|
-
### Disabling the render monkey patch (Rails < 6.1)
|
868
|
-
|
869
|
-
In order to [avoid conflicts](https://github.com/github/view_component/issues/288) between ViewComponent and other gems that also monkey patch the `render` method, it is possible to configure ViewComponent to not include the render monkey patch:
|
870
|
-
|
871
|
-
`config.view_component.render_monkey_patch_enabled = false # defaults to true`
|
872
|
-
|
873
|
-
With the monkey patch disabled, use `render_component` (or `render_component_to_string`) instead:
|
874
|
-
|
875
|
-
```erb
|
876
|
-
<%= render_component Component.new(message: "bar") %>
|
877
|
-
```
|
878
|
-
|
879
|
-
### Sidecar assets (experimental)
|
880
|
-
|
881
|
-
It’s possible to include Javascript and CSS alongside components, sometimes called "sidecar" assets or files.
|
882
|
-
|
883
|
-
To use the Webpacker gem to compile sidecar assets located in `app/components`:
|
884
|
-
|
885
|
-
1. In `config/webpacker.yml`, add `"app/components"` to the `resolved_paths` array (e.g. `resolved_paths: ["app/components"]`).
|
886
|
-
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:
|
887
|
-
|
888
|
-
```js
|
889
|
-
import "../components"
|
890
|
-
```
|
891
|
-
|
892
|
-
Then, in `app/javascript/components.js`, add:
|
893
|
-
|
894
|
-
```js
|
895
|
-
function importAll(r) {
|
896
|
-
r.keys().forEach(r)
|
897
|
-
}
|
898
|
-
|
899
|
-
importAll(require.context("../components", true, /_component.js$/))
|
900
|
-
```
|
901
|
-
|
902
|
-
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.
|
903
|
-
|
904
|
-
#### Encapsulating sidecar assets
|
905
|
-
|
906
|
-
Ideally, sidecar Javascript/CSS should not "leak" out of the context of its associated component.
|
907
|
-
|
908
|
-
One approach is to use Web Components, which contain all Javascript functionality, internal markup, and styles within the shadow root of the Web Component.
|
909
|
-
|
910
|
-
For example:
|
911
|
-
|
912
|
-
`app/components/comment_component.rb`
|
913
|
-
|
914
|
-
```ruby
|
915
|
-
class CommentComponent < ViewComponent::Base
|
916
|
-
def initialize(comment:)
|
917
|
-
@comment = comment
|
918
|
-
end
|
919
|
-
|
920
|
-
def commenter
|
921
|
-
@comment.user
|
922
|
-
end
|
923
|
-
|
924
|
-
def commenter_name
|
925
|
-
commenter.name
|
926
|
-
end
|
927
|
-
|
928
|
-
def avatar
|
929
|
-
commenter.avatar_image_url
|
930
|
-
end
|
931
|
-
|
932
|
-
def formatted_body
|
933
|
-
simple_format(@comment.body)
|
934
|
-
end
|
935
|
-
|
936
|
-
private
|
937
|
-
|
938
|
-
attr_reader :comment
|
939
|
-
end
|
940
|
-
```
|
941
|
-
|
942
|
-
`app/components/comment_component.html.erb`
|
943
|
-
|
944
|
-
```erb
|
945
|
-
<my-comment comment-id="<%= comment.id %>">
|
946
|
-
<time slot="posted" datetime="<%= comment.created_at.iso8601 %>"><%= comment.created_at.strftime("%b %-d") %></time>
|
947
|
-
|
948
|
-
<div slot="avatar"><img src="<%= avatar %>" /></div>
|
949
|
-
|
950
|
-
<div slot="author"><%= commenter_name %></div>
|
951
|
-
|
952
|
-
<div slot="body"><%= formatted_body %></div>
|
953
|
-
</my-comment>
|
954
|
-
```
|
955
|
-
|
956
|
-
`app/components/comment_component.js`
|
957
|
-
|
958
|
-
```js
|
959
|
-
class Comment extends HTMLElement {
|
960
|
-
styles() {
|
961
|
-
return `
|
962
|
-
:host {
|
963
|
-
display: block;
|
964
|
-
}
|
965
|
-
::slotted(time) {
|
966
|
-
float: right;
|
967
|
-
font-size: 0.75em;
|
968
|
-
}
|
969
|
-
.commenter { font-weight: bold; }
|
970
|
-
.body { … }
|
971
|
-
`
|
972
|
-
}
|
973
|
-
|
974
|
-
constructor() {
|
975
|
-
super()
|
976
|
-
const shadow = this.attachShadow({mode: 'open'});
|
977
|
-
shadow.innerHTML = `
|
978
|
-
<style>
|
979
|
-
${this.styles()}
|
980
|
-
</style>
|
981
|
-
<slot name="posted"></slot>
|
982
|
-
<div class="commenter">
|
983
|
-
<slot name="avatar"></slot> <slot name="author"></slot>
|
984
|
-
</div>
|
985
|
-
<div class="body">
|
986
|
-
<slot name="body"></slot>
|
987
|
-
</div>
|
988
|
-
`
|
989
|
-
}
|
990
|
-
}
|
991
|
-
customElements.define('my-comment', Comment)
|
992
|
-
```
|
993
|
-
|
994
|
-
##### Stimulus
|
995
|
-
|
996
|
-
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`:
|
997
|
-
|
998
|
-
```js
|
999
|
-
const application = Application.start()
|
1000
|
-
const context = require.context("controllers", true, /.js$/)
|
1001
|
-
const context_components = require.context("../../components", true, /_controller.js$/)
|
1002
|
-
application.load(
|
1003
|
-
definitionsFromContext(context).concat(
|
1004
|
-
definitionsFromContext(context_components)
|
1005
|
-
)
|
1006
|
-
)
|
1007
|
-
```
|
1008
|
-
|
1009
|
-
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.
|
1010
|
-
|
1011
|
-
After configuring Webpack to load Stimulus controller files from the `components` directory, add the path to `resolved_paths` in `config/webpacker.yml`:
|
1012
|
-
|
1013
|
-
```yml
|
1014
|
-
resolved_paths: ["app/components"]
|
1015
|
-
```
|
1016
|
-
|
1017
|
-
When placing a Stimulus controller inside a sidecar directory, be aware that when referencing the controller [each forward slash in a namespaced controller file’s path becomes two dashes in its identifier](
|
1018
|
-
https://stimulusjs.org/handbook/installing#controller-filenames-map-to-identifiers):
|
1019
|
-
|
1020
|
-
```console
|
1021
|
-
app/components
|
1022
|
-
├── ...
|
1023
|
-
├── example
|
1024
|
-
| ├── component.rb
|
1025
|
-
| ├── component.css
|
1026
|
-
| ├── component.html.erb
|
1027
|
-
| └── component_controller.js
|
1028
|
-
├── ...
|
1029
|
-
```
|
1030
|
-
|
1031
|
-
`component_controller.js`'s Stimulus identifier becomes: `example--component`:
|
1032
|
-
|
1033
|
-
```erb
|
1034
|
-
<div data-controller="example--component">
|
1035
|
-
<input type="text">
|
1036
|
-
<button data-action="click->example--component#greet">Greet</button>
|
1037
|
-
</div>
|
1038
|
-
```
|
1039
|
-
|
1040
|
-
## Frequently Asked Questions
|
1041
|
-
|
1042
|
-
### Can I use other templating languages besides ERB?
|
1043
|
-
|
1044
|
-
Yes. ViewComponent is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
|
1045
|
-
|
1046
|
-
### Isn't this just like X library?
|
1047
|
-
|
1048
|
-
ViewComponent is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
|
1049
|
-
|
1050
|
-
- [trailblazer/cells](https://github.com/trailblazer/cells)
|
1051
|
-
- [dry-rb/dry-view](https://github.com/dry-rb/dry-view)
|
1052
|
-
- [komposable/komponent](https://github.com/komposable/komponent)
|
1053
|
-
- [activeadmin/arbre](https://github.com/activeadmin/arbre)
|
1054
|
-
|
1055
|
-
## Resources
|
1056
|
-
|
1057
|
-
- [Encapsulating Views, RailsConf 2020](https://youtu.be/YVYRus_2KZM)
|
1058
|
-
- [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/)
|
1059
|
-
- [ViewComponents in Action with Andrew Mason, Ruby on Rails Podcast](https://5by5.tv/rubyonrails/320)
|
1060
|
-
- [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
|
1061
|
-
- [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
|
1062
|
-
- [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
|
1063
|
-
- [Rethinking the View Layer with Components, RailsConf 2019](https://www.youtube.com/watch?v=y5Z5a6QdA-M)
|
1064
|
-
- [Introducing ActionView::Component with Joel Hawksley, Ruby on Rails Podcast](http://5by5.tv/rubyonrails/276)
|
1065
|
-
- [Rails to Introduce View Components, Dev.to](https://dev.to/andy/rails-to-introduce-view-components-3ome)
|
1066
|
-
- [ActionView::Components in Rails 6.1, Drifting Ruby](https://www.driftingruby.com/episodes/actionview-components-in-rails-6-1)
|
1067
|
-
- [Demo repository, view-component-demo](https://github.com/joelhawksley/view-component-demo)
|
1068
|
-
|
1069
17
|
## Contributing
|
1070
18
|
|
1071
19
|
This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. We recommend reading the [contributing guide](./CONTRIBUTING.md) as well.
|
1072
20
|
|
1073
|
-
## Contributors
|
1074
|
-
|
1075
|
-
ViewComponent is built by:
|
1076
|
-
|
1077
|
-
|<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" />|
|
1078
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1079
|
-
|@joelhawksley|@tenderlove|@jonspalmer|@juanmanuelramallo|@vinistock|
|
1080
|
-
|Denver|Seattle|Boston||Toronto|
|
1081
|
-
|
1082
|
-
|<img src="https://avatars.githubusercontent.com/metade?s=256" alt="metade" width="128" />|<img src="https://avatars.githubusercontent.com/asgerb?s=256" alt="asgerb" width="128" />|<img src="https://avatars.githubusercontent.com/xronos-i-am?s=256" alt="xronos-i-am" width="128" />|<img src="https://avatars.githubusercontent.com/dylnclrk?s=256" alt="dylnclrk" width="128" />|<img src="https://avatars.githubusercontent.com/kaspermeyer?s=256" alt="kaspermeyer" width="128" />|
|
1083
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1084
|
-
|@metade|@asgerb|@xronos-i-am|@dylnclrk|@kaspermeyer|
|
1085
|
-
|London|Copenhagen|Russia, Kirov|Berkeley, CA|Denmark|
|
1086
|
-
|
1087
|
-
|<img src="https://avatars.githubusercontent.com/rdavid1099?s=256" alt="rdavid1099" width="128" />|<img src="https://avatars.githubusercontent.com/kylefox?s=256" alt="kylefox" width="128" />|<img src="https://avatars.githubusercontent.com/traels?s=256" alt="traels" width="128" />|<img src="https://avatars.githubusercontent.com/rainerborene?s=256" alt="rainerborene" width="128" />|<img src="https://avatars.githubusercontent.com/jcoyne?s=256" alt="jcoyne" width="128" />|
|
1088
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1089
|
-
|@rdavid1099|@kylefox|@traels|@rainerborene|@jcoyne|
|
1090
|
-
|Los Angeles|Edmonton|Odense, Denmark|Brazil|Minneapolis|
|
1091
|
-
|
1092
|
-
|<img src="https://avatars.githubusercontent.com/elia?s=256" alt="elia" width="128" />|<img src="https://avatars.githubusercontent.com/cesariouy?s=256" alt="cesariouy" width="128" />|<img src="https://avatars.githubusercontent.com/spdawson?s=256" alt="spdawson" width="128" />|<img src="https://avatars.githubusercontent.com/rmacklin?s=256" alt="rmacklin" width="128" />|<img src="https://avatars.githubusercontent.com/michaelem?s=256" alt="michaelem" width="128" />|
|
1093
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1094
|
-
|@elia|@cesariouy|@spdawson|@rmacklin|@michaelem|
|
1095
|
-
|Milan||United Kingdom||Berlin|
|
1096
|
-
|
1097
|
-
|<img src="https://avatars.githubusercontent.com/mellowfish?s=256" alt="mellowfish" width="128" />|<img src="https://avatars.githubusercontent.com/horacio?s=256" alt="horacio" width="128" />|<img src="https://avatars.githubusercontent.com/dukex?s=256" alt="dukex" width="128" />|<img src="https://avatars.githubusercontent.com/dark-panda?s=256" alt="dark-panda" width="128" />|<img src="https://avatars.githubusercontent.com/smashwilson?s=256" alt="smashwilson" width="128" />|
|
1098
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1099
|
-
|@mellowfish|@horacio|@dukex|@dark-panda|@smashwilson|
|
1100
|
-
|Spring Hill, TN|Buenos Aires|São Paulo||Gambrills, MD|
|
1101
|
-
|
1102
|
-
|<img src="https://avatars.githubusercontent.com/blakewilliams?s=256" alt="blakewilliams" width="128" />|<img src="https://avatars.githubusercontent.com/seanpdoyle?s=256" alt="seanpdoyle" width="128" />|<img src="https://avatars.githubusercontent.com/tclem?s=256" alt="tclem" width="128" />|<img src="https://avatars.githubusercontent.com/nashby?s=256" alt="nashby" width="128" />|<img src="https://avatars.githubusercontent.com/jaredcwhite?s=256" alt="jaredcwhite" width="128" />|
|
1103
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1104
|
-
|@blakewilliams|@seanpdoyle|@tclem|@nashby|@jaredcwhite|
|
1105
|
-
|Boston, MA|New York, NY|San Francisco, CA|Minsk|Portland, OR|
|
1106
|
-
|
1107
|
-
|<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" />|
|
1108
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1109
|
-
|@simonrand|@fugufish|@cover|@franks921|@fsateler|
|
1110
|
-
|Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|Chile|
|
1111
|
-
|
1112
|
-
|<img src="https://avatars.githubusercontent.com/maxbeizer?s=256" alt="maxbeizer" width="128" />|<img src="https://avatars.githubusercontent.com/franco?s=256" alt="franco" width="128" />|<img src="https://avatars.githubusercontent.com/tbroad-ramsey?s=256" alt="tbroad-ramsey" width="128" />|<img src="https://avatars.githubusercontent.com/jensljungblad?s=256" alt="jensljungblad" width="128" />|<img src="https://avatars.githubusercontent.com/bbugh?s=256" alt="bbugh" width="128" />|
|
1113
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1114
|
-
|@maxbeizer|@franco|@tbroad-ramsey|@jensljungblad|@bbugh|
|
1115
|
-
|Nashville, TN|Switzerland|Spring Hill, TN|New York, NY|Austin, TX|
|
1116
|
-
|
1117
|
-
|<img src="https://avatars.githubusercontent.com/johannesengl?s=256" alt="johannesengl" width="128" />|<img src="https://avatars.githubusercontent.com/czj?s=256" alt="czj" width="128" />|<img src="https://avatars.githubusercontent.com/mrrooijen?s=256" alt="mrrooijen" width="128" />|<img src="https://avatars.githubusercontent.com/bradparker?s=256" alt="bradparker" width="128" />|<img src="https://avatars.githubusercontent.com/mattbrictson?s=256" alt="mattbrictson" width="128" />|
|
1118
|
-
|:---:|:---:|:---:|:---:|:---:|
|
1119
|
-
|@johannesengl|@czj|@mrrooijen|@bradparker|@mattbrictson|
|
1120
|
-
|Berlin, Germany|Paris, France|The Netherlands|Brisbane, Australia|San Francisco|
|
1121
|
-
|
1122
|
-
|<img src="https://avatars.githubusercontent.com/mixergtz?s=256" alt="mixergtz" width="128" />|<img src="https://avatars.githubusercontent.com/jules2689?s=256" alt="jules2689" width="128" />|<img src="https://avatars.githubusercontent.com/g13ydson?s=256" alt="g13ydson" width="128" />|<img src="https://avatars.githubusercontent.com/swanson?s=256" alt="swanson" width="128" />|
|
1123
|
-
|:---:|:---:|:---:|:---:|
|
1124
|
-
|@mixergtz|@jules2689|@g13ydson|@swanson|
|
1125
|
-
|Medellin, Colombia|Toronto, Canada|João Pessoa, Brazil|Indianapolis, IN|
|
1126
|
-
|
1127
21
|
## License
|
1128
22
|
|
1129
23
|
ViewComponent is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|