Skip to content

Commit 79c2c25

Browse files
Add a new "general guidelines" page for View Component (#2234)
* Create stub page and increment nav_order * Create a "ViewComponents in general" doc - Some items are pulled from the "ViewComponents at GitHub" page and presented in more generalized language. If it's agreeable to do that, it probably makes sense to remove those from the GitHub-specific page. * Fix smart quotes getting reverted in Changelog * edits * more edits * vale --------- Co-authored-by: Joel Hawksley <[email protected]> Co-authored-by: Joel Hawksley <[email protected]>
1 parent fd6f814 commit 79c2c25

File tree

3 files changed

+178
-194
lines changed

3 files changed

+178
-194
lines changed

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ nav_order: 5
1010

1111
## main
1212

13+
* Rewrite `ViewComponents at GitHub` documentation as more general `Best practices`.
14+
15+
*Phil Schalm*, *Joel Hawksley*
16+
1317
* Add unused mechanism for inheriting config from parent modules to enable future engine-local configuration.
1418

1519
*Simon Fish*

docs/best_practices.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
layout: default
3+
title: Best practices
4+
nav_order: 5
5+
---
6+
7+
# Best practices
8+
9+
_A general guide to building component-driven UI in Rails. Consider it to be more opinion than fact._
10+
11+
## Philosophy
12+
13+
### Why ViewComponent exists
14+
15+
ViewComponent was created to help manage the growing complexity of the GitHub.com view layer, which accumulated thousands of templates over the years, almost entirely through copy-pasting. A lack of abstraction made it challenging to make sweeping design, accessibility, and behavior improvements.
16+
17+
ViewComponent provides a way to isolate common UI patterns for reuse, helping to improve the quality and consistency of Rails applications.
18+
19+
### ViewComponent is to UI what ActiveRecord is to SQL
20+
21+
ViewComponent brings [conceptual compression](https://m.signalvnoise.com/conceptual-compression-means-beginners-dont-need-to-know-sql-hallelujah/) to the practice of building user interfaces.
22+
23+
### ViewComponent exposes existing complexity
24+
25+
Converting an existing view/partial to a ViewComponent often exposes existing complexity. For example, a ViewComponent may need numerous arguments to be rendered, revealing the number of dependencies in the existing view code.
26+
27+
This is good! Refactoring to use ViewComponent improves comprehension and provides a foundation for further improvement.
28+
29+
## Organization
30+
31+
### Two types of ViewComponents
32+
33+
ViewComponents typically come in two forms: general-purpose and application-specific.
34+
35+
#### General-purpose ViewComponents
36+
37+
General-purpose ViewComponents implement common UI patterns, such as a button, form, or modal. GitHub open-sources these components as [Primer ViewComponents](https://github.com/primer/view_components).
38+
39+
#### Application-specific ViewComponents
40+
41+
Application-specific ViewComponents translate a domain object (such as an `ActiveRecord` model or an API response modeled as a Plain Old Ruby Object) into one or more general-purpose components.
42+
43+
For example, `User::AvatarComponent` accepts a `User` ActiveRecord object and renders a `DesignSystem::AvatarComponent`.
44+
45+
### Extract general-purpose ViewComponents
46+
47+
"Good frameworks are extracted, not invented" - [DHH](https://dhh.dk/arc/000416.html)
48+
49+
Just as ViewComponent itself was extracted from GitHub.com, general-purpose components are best extracted once they've proven helpful across more than one area:
50+
51+
1. Single use-case component implemented.
52+
2. Component adapted for general use in multiple locations in the application.
53+
3. Component extracted into a general-purpose ViewComponent in `app/lib` or a separate gem.
54+
55+
### Reduce permutations
56+
57+
When building ViewComponents, look for opportunities to consolidate similar patterns into a single implementation. Consider following standard DRY practices, abstracting once there are three or more similar instances.
58+
59+
### Avoid one-offs
60+
61+
Aim to minimize the amount of single-use view code. Every new component introduced adds to application maintenance burden.
62+
63+
## Implementation
64+
65+
### Avoid inheritance
66+
67+
Having one ViewComponent inherit from another leads to confusion, especially when each component has its own template. Instead, [use composition](https://thoughtbot.com/blog/reusable-oo-composition-vs-inheritance) to wrap one component with another.
68+
69+
### When to use a ViewComponent for an entire route
70+
71+
ViewComponents have less value in single-use cases like replacing a `show` view. However, it can make sense to render an entire route with a ViewComponent when unit testing is valuable, such as for views with many permutations from a state machine.
72+
73+
When migrating an entire route to use ViewComponents, work from the bottom up, extracting portions of the page into ViewComponents first.
74+
75+
### Test against rendered content, not instance methods
76+
77+
ViewComponent tests should use `render_inline` and assert against the rendered output. While it can be useful to test specific component instance methods directly, it's more valuable to write assertions against what's shown to the end user:
78+
79+
```ruby
80+
# good
81+
render_inline(MyComponent.new)
82+
assert_text("Hello, World!")
83+
84+
# bad
85+
assert_equal(MyComponent.new.message, "Hello, World!")
86+
```
87+
88+
### Most ViewComponent instance methods can be private
89+
90+
Most ViewComponent instance methods can be private, as they will still be available in the component template:
91+
92+
```ruby
93+
# good
94+
class MyComponent < ViewComponent::Base
95+
private
96+
97+
def method_used_in_template
98+
end
99+
end
100+
101+
# bad
102+
class MyComponent < ViewComponent::Base
103+
def method_used_in_template
104+
end
105+
end
106+
```
107+
108+
### Prefer ViewComponents over partials
109+
110+
Use ViewComponents in place of partials.
111+
112+
### Prefer ViewComponents over HTML-generating helpers
113+
114+
Use ViewComponents in place of helpers that return HTML.
115+
116+
### Avoid global state
117+
118+
The more a ViewComponent is dependent on global state (such as request parameters or the current URL), the less likely it's to be reusable. Avoid implicit coupling to global state, instead passing it into the component explicitly:
119+
120+
```ruby
121+
# good
122+
class MyComponent < ViewComponent::Base
123+
def initialize(name:)
124+
@name = name
125+
end
126+
end
127+
128+
# bad
129+
class MyComponent < ViewComponent::Base
130+
def initialize
131+
@name = params[:name]
132+
end
133+
end
134+
```
135+
136+
Thorough unit testing is a good way to ensure decoupling from global state.
137+
138+
### Avoid inline Ruby in ViewComponent templates
139+
140+
Avoid writing inline Ruby in ViewComponent templates. Try using an instance method on the ViewComponent instead:
141+
142+
```ruby
143+
# good
144+
class MyComponent < ViewComponent::Base
145+
attr_accessor :name
146+
147+
def message
148+
"Hello, #{name}!"
149+
end
150+
end
151+
```
152+
153+
```erb
154+
<%# bad %>
155+
<% message = "Hello, #{name}" %>
156+
```
157+
158+
### Prefer slots over passing markup as an argument
159+
160+
Prefer using slots for providing markup to components. Passing markup as an argument bypasses the HTML sanitization provided by Rails, creating the potential for security issues:
161+
162+
```erb
163+
# good
164+
<%= render(MyComponent.new) do |component| %>
165+
<% component.with_name do %>
166+
<strong>Hello, world!</strong>
167+
<% end %>
168+
<% end %>
169+
```
170+
171+
```erb
172+
# bad
173+
<%= render MyComponent.new(name: "<strong>Hello, world!</strong>".html_safe) %>
174+
```

docs/viewcomponents-at-github.md

Lines changed: 0 additions & 194 deletions
This file was deleted.

0 commit comments

Comments
 (0)