view_component 2.18.2 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

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