Build views with view components
When working on the view layer in the application (app/views) prefer using view components over the alternatives:
- Partials
- ERB files with business logic
- Helpers
- Decorators
Flexible components π
Make sure to generalize your use case so that the component can be used in any part of the application.
In order to make components more reusable the library provides us with concepts like
the content
helper and slots.
Prefer extending component or composing new ones with existing ones over creating additional components. Feature specific components are welcomed, but they should be composed by other reusable components. There are more details on this topic here.
Hereβs an example that illustrates how components can be flexible:
Bad π
<%= render BigButton.new(text: "Big button") %>
<%= render ButtonWithIcon.new(text: "Button with icon", icon: "assets/icons/star.svg") %>
Good π
<%= render Button.new(text: "Big button") %>
<%= render Button.new(text: "Button with icon", with_leading_icon: "assets/icon/star.svg") %>
Tested components π
Make sure that view components are tested. The ViewComponent library introduces a
new type of test (type: :component
) which pulls in a few useful helpers that
make testing components quite easy.
Familiarize yourself with view component testing.
Why is it important to test view components? π
- It reduces the number of necessary system tests.
- View component tests are much faster than system tests.
- These tests allow us to easily check for edge cases.
Hereβs an example that help illustrate how simple a component test can be:
# app/components/table_component.rb
class TableComponent < ApplicationComponent
def initialize(database_records:)
@database_records = database_records
end
def render?
@database_records.any?
end
end
# spec/components/table_component_spec.rb
RSpec.describe TableComponent, type: :component do
it "is not rendered when the collection is empty" do
render_inline(TableComponent.new(collection: []))
expect(page).not_to have_selector("table")
end
end
Styled components π
Avoid using writing CSS when building view components. Weβre moving away from using custom CSS files and using Tailwind instead. If youβre having a hard time styling a component using Tailwind reach out to someone from the design team to discuss making adjustments to the component.
Hereβs an example that illustrates how using Tailwind looks as opposed to writing custom CSS:
Bad π
# app/components/button.scss
.button {
&-content { ... }
&-label { ... }
}
# app/components/button.html.erb
<button type="button" class="button">
<span class="button-content">
<span class="button-label">Button text</span>
</span>
</button>
Good π
# app/components/button.html.erb
<button type="button" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm">
Button text
</button>
Dynamic styling with Tailwind π
Some components may require different styling based on their state or arguments passed to them, and this can be a challenge to handle when working Tailwind because it requires CSS classes to be spelled out. In order to make this work we need to define and spell out all CSS classes a component can use by using methods on the component ruby class.
Bad π
This will not work:
class Button
def initialize(scheme:)
@scheme = scheme
end
def scheme_color
scheme == :primary ? "blue" : "white"
end
end
`<input class="bg-<%= scheme_color %>" />`
Good π
class Button
def initialize(scheme:)
@scheme = scheme
end
def scheme_color_class
scheme == :primary ? "bg-blue" : "bg-white"
end
end
<input class="<%= scheme_color_class %>" />
Shared components π
Always check our custom component library before creating a new one. The goal of our component library is to succinctly provide the minimum building blocks necessary for implementing the view layer of all user facing features.
Keep in mind to:
- Make sure new components are included in the component library by creating a component preview.
- Previews should include annotations and parameter descriptions.
- Annotations be used to describe the default use of the component along with common variations.
- All component previews automatically get included in the Lookbook so make sure to check how it looks there.
Hereβs an example of what a component preview looks like:
# spec/components/previews/button_component_preview.rb
class ButtonComponentPreview < ViewComponent::Preview
# Default
# ---------------
# This is the base button that can be customized as described by the
# parameters.
#
# @param type select { choices: [button, submit, reset] } "Defaults to `:button`"
# @param size select { choices: [small, medium, large] } "Defaults to `:medium`"
# @param scheme select { choices: [primary, secondary] } "Defaults to `:primary`"
# @param expand_on_mobile select { choices: [true, false] } "Defaults to `false`"
def default(type: :button, size: :medium, scheme: :primary, expand_on_mobile: false)
render ButtonComponent.new(type:, size:, scheme:, expand_on_mobile:) do
"Button text"
end
end
# Secondary
# ---------------
# This is the secondary scheme.
def secondary
render ButtonComponent.new(scheme: :secondary) do
"Secondary button"
end
end
end