-
Notifications
You must be signed in to change notification settings - Fork 441
View Components
- Problem
- Solution
-
Details
- Why We Chose The view_component Gem
- Conventions
- How to Write View Component Specs
- Create Previews for Your View Components
- How to Use Custom Helpers in View Components
- Authorization inside View Components with Pundit
- Avoid Global State
- Most View Component Instance Methods Can Be Private
- Avoid Database Queries
- Conditional Rendering
- OBS Components Details
- Links
- Frequently Asked Questions
Any view code (helpers, partials, etc...) with questionable code quality. It is perhaps complex or huge. It's unclear what data it needs since it doesn't have a clear interface. On top of this, it is probably barely tested and even if it is, it's only through slow integration tests of pages in which it is used.
Replace the view code by a view component located under src/api/app/components. View components are plain old Ruby objects (PORO). It makes them reusable, easy to test through unit tests, encapsulated and they have a clear interface defined by their initialize
method. Replacing pretty much any view code by a view component is beneficial, unless it is super simple without any logic and used in a single view. Using a view component won't be very helpful in this case.
There are many view component implementations in Ruby, but we decided to use the view_component gem since it integrates nicely with Rails and has increasing support throughout the Rails community. Its open-source development is also backed by GitHub.
- All view components must inherit from
ApplicationComponent
. This parent class has methods to prevent potential errors in common scenarios when using view components. - View component names end with
Component
, therefore their file names end with_component.rb
. - Name view components after what they render, not what they accept. (Example:
AvatarComponent
instead ofUserComponent
if the view component is rendering the avatar of a user.)
Specs of view components are located under src/api/app/spec/components. They are typical unit tests with a few extra methods and Capybara matchers like have_text
, have_css
(and much more) are available.
Specs can be split in two common scenarios:
- A single expectation
In the code below, the view component is rendered with render_inline(...)
. We use the have_text
matcher from Capybara to test the rendered component.
require 'rails_helper'
RSpec.describe ExampleComponent, type: :component do
context 'for anonymous user' do
it do
expect(render_inline(described_class.new(title: 'Everything is fine'))).to have_text('Everything is fine')
end
end
end
- Multiple expectations
The view component is rendered with render_inline(...)
once in a before
block to avoid wasting time by rendering it multiple times. The rendered component is stored in rendered_content
, a method provided by the view_component
gem. Use Capybara matchers like have_text
for your expectations. Setting User.session = create(:admin_user)
in a before
block allows you to test how the view component renders for various users based on their permissions.
require 'rails_helper'
RSpec.describe ExampleComponent, type: :component do
context 'for admin user' do
before do
User.session = create(:admin_user)
render_inline(described_class.new(title: 'Everything is fine for an admin'))
end
it do
expect(rendered_content).to have_text('Everything is fine for an admin')
end
it do
expect(rendered_content).to have_text('Delete this example?')
end
end
end
Previews of view components are located under src/api/app/spec/components/previews. They are accessible at https://$HOST:$PORT/rails/view_components/$COMPONENT_NAME/$METHOD_NAME. Their names end with ComponentPreview
and their file names end with _component_preview.rb
. We have a custom RuboCop cop to enforce the presence of previews for every view component.
Previews are enabled by default in development and test environments.
class ExampleComponentPreview < ViewComponent::Preview
# Accessible at https://my_app.com/rails/view_components/example_component/with_a_title
def with_a_title
render(ExampleComponent.new(title: "This is my example"))
end
end
In view components, custom helpers can be used through the helpers
proxy. So if you have a custom helper named user_icon
, use it inside a view component with helpers.user_icon
. Built-in helpers like link_to shouldn't be prefixed by helpers
.
To authorize users inside view components or their Haml template, use the policy
method provided by ApplicationComponent
. It is exactly the same as the policy
method you already know from Pundit. Beside the argument provided to the policy
method, nothing should change when migrating authorization code to a view component.
For example, to check in a view component if the current user can create the package my_package
, write policy(my_package).create?
.
The more a view component 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. Thorough unit testing is a good way to ensure decoupling from global state. We have a custom RuboCop cop to enforce this.
# good
class MyComponent < ViewComponent::Base
def initialize(name:, user:)
@name = name
@user = user
end
end
# bad
class MyComponent < ViewComponent::Base
def initialize
@name = params[:name]
@user = User.session
end
end
Most view component instance methods can be private, as they will still be available in the component template:
# good
class MyComponent < ViewComponent::Base
def initialize; end
private
def method_used_in_template; end
end
# bad
class MyComponent < ViewComponent::Base
def initialize; end
def method_used_in_template; end
end
Avoid executing database queries in view components. Be especially careful for view components which are rendered as lists. Any data needed by view components should instead be passed when they are instantiated, so in their .new
call.
View components can implement a #render?
method which hooks into the view component initialization to determine if it should render. This simplifies the view and avoids code duplication as traditionally, this check is done in the view.
Some components implemented in OBS might need some clarification. Here you have links to the details of those components.
- Gem: https://rubygems.org/gems/view_component
- Official documentation with lots of information
- Talks from a maintainer of the view_component gem
React and Vue are 2 JavaScript frameworks which also implement view components, they call them components. The main difference with the view_component gem is that this is a client-side approach. For our needs, using a JavaScript framework would be pulling a lot of dependencies and imply a lot of changes only to support view components. Most of the JavaScript we write tends to be boilerplate code, so a simpler framework like Stimulus would be a much better choice for OBS. This is why we went with the view_component gem and a server-side approach.
Yes and not only for new features! View components are easy to test through unit tests, encapsulated and they have a clear interface defined by their initialize
method. However, any view code which doesn't need to be tested doesn't benefit much from being a view component.
Yes! However, a super simple code without any logic doesn't benefit much from being migrated to a view component since in this case, tests aren't needed and the clear interface of a view component through its initialize
method isn't helpful if it's empty.
- Development Environment Overview
- Development Environment Tips & Tricks
- Spec-Tips
- Code Style
- Rubocop
- Testing with VCR
- Authentication
- Authorization
- Autocomplete
- BS Requests
- Events
- ProjectLog
- Notifications
- Feature Toggles
- Build Results
- Attrib classes
- Flags
- The BackendPackage Cache
- Maintenance classes
- Cloud uploader
- Delayed Jobs
- Staging Workflow
- StatusHistory
- OBS API
- Owner Search
- Search
- Links
- Distributions
- Repository
- Data Migrations
- next_rails
- Ruby Update
- Rails Profiling
- Installing a local LDAP-server
- Remote Pairing Setup Guide
- Factory Dashboard
- osc
- Setup an OBS Development Environment on macOS
- Run OpenQA smoketest locally
- Responsive Guidelines
- Importing database dumps
- Problem Statement & Solution
- Kickoff New Stuff
- New Swagger API doc
- Documentation and Communication
- GitHub Actions
- How to Introduce Software Design Patterns
- Query Objects
- Services
- View Components
- RFC: Core Components
- RFC: Decorator Pattern
- RFC: Backend models