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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09c638fb23b5832cf8ce411d1a07b50d2d56087858d8186ee2d4248c7e0576de'
4
- data.tar.gz: b7ea4f439d382228a7c6911b2c0b959be52c7443cd4885a9d91159b55764d8b7
3
+ metadata.gz: b2282359cc779d83fa6aa65b912fe4e4e70d2dbd6c8939b2bd0f3e21bd2eb9e7
4
+ data.tar.gz: c97fffc33ed1ffb6cd4598136c30c1b5990f1f150e8a7fc04ecfae933ae6c271
5
5
  SHA512:
6
- metadata.gz: 43d6768b641f4fcb4f7a86488312400f6cb1922722f5654a98bebda771dbdc1a544e84d64b719880c822fec94621f1d402f6eaa805a21d383cab227b630b6503
7
- data.tar.gz: 152a9f7ad4a6de0fa37df958d8a1877822bdf96f97ef81cbeebd9afb6224155e37725eb71338dd908061c0f0799e0cbf5dcf7c0c39d805ad5c471335649f4b97
6
+ metadata.gz: ec8397db82e97af474d4d25c8fde9346c73de00fdc37cd108640983a3230c08eae4c95566e8c86a96ad78c7e0c04aabf028f3f9c3e3040111d2b84d43c92bd53
7
+ data.tar.gz: 152e91807eff69a99066d20d977ec9092c71f44754fa2f2fd76cd69be4f221e47cdfd01c413aaac60f7334c4d852f86a2a8514f395c8ed773407203c82d77fe0
@@ -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
- ViewComponent is a framework for building view components that are reusable, testable & encapsulated, in Ruby on Rails.
3
+ A framework for building reusable, testable & encapsulated view components in Ruby on Rails.
4
4
 
5
- ## Design philosophy
5
+ ## Documentation
6
6
 
7
- 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).
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).