view_component 2.19.0 → 2.22.1

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

Potentially problematic release.


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

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