-
Notifications
You must be signed in to change notification settings - Fork 461
View Components
Approved by: @dmarcoux, @danidoni, @vpereira, @rubhanazeem
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. - Name view components after what they render, not what they accept. (Example:
AvatarComponentinstead ofUserComponentif 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_component, 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_component).to have_text('Everything is fine for an admin')
end
it do
expect(rendered_component).to have_text('Delete this example?')
end
end
endPreviews 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. 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
endIn 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.
# good
class MyComponent < ViewComponent::Base
def initialize(name:)
@name = name
end
end
# bad
class MyComponent < ViewComponent::Base
def initialize
@name = params[:name]
end
endMost 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- 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
- Test in kanku
- 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
- Package Versions
- next_rails
- Ruby Update
- Rails Profiling
- 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
- Brakeman
- How to Introduce Software Design Patterns
- Query Objects
- Services
- View Components
- RFC: Core Components
- RFC: Decorator Pattern
- RFC: Backend models
- RFC: Hotwire Turbo Frames Pattern