A view component framework for Rails.
Current Status: Used in production at GitHub. Because of this, all changes will be thoroughly vetted, which could slow down the process of contributing. We will do our best to actively communicate status of pull requests with any contributors. If you have any substantial changes that you would like to make, it would be great to first open an issue to discuss them with us.
This gem is in the process of a name / API change from ActionView::Component
to ViewComponent
, see ViewComponent#206.
ActionView::Component::Base
is nowViewComponent::Base
.- Components can only be rendered with
render(MyComponent.new)
syntax. - Validations are no longer supported by default.
- Update
Gemfile
to useview_component
. - In
application.rb
, requireview_component/engine
. - Update components to inherit from
ViewComponent::Base
. - Update component tests to inherit from
ViewComponent::TestCase
. - Update component previews to inherit from
ViewComponent::Preview
. - Include
ViewComponent::TestHelpers
in the appropriate test helper file.
Support for third-party component frameworks was merged into Rails 6.1.0.alpha
in rails/rails#36388 and rails/rails#37919. Our goal with this project is to provide a first-class component framework for this new capability in Rails.
This gem includes a backport of those changes for Rails 5.0.0
through 6.1.0.alpha
.
This library is designed to integrate as seamlessly as possible with Rails, with the least surprise.
view_component
is tested for compatibility with combinations of Ruby 2.4
/2.5
/2.6
/2.7
and Rails 5.0.0
/5.2.3
/6.0.0
/master
.
In Gemfile
, add:
gem "view_component"
In config/application.rb
, add:
require "view_component/engine"
ViewComponent
s are Ruby classes that are used to render views. They take data as input and return output-safe HTML. Think of them as an evolution of the presenter/decorator/view model pattern, inspired by React Components.
Components are most effective in cases where view code is reused or benefits from being tested directly.
Rails encourages testing views with integration tests. This discourages us from testing views thoroughly, due to the overhead of exercising the routing and controller layers in addition to the view.
For partials, this means being tested for each view they are included in, reducing the benefit of reusing them.
ViewComponent
s can be unit-tested. In the GitHub codebase, our component unit tests run in around 25 milliseconds, compared to about six seconds for integration tests.
Unlike a method declaration on an object, views do not declare the values they are expected to receive, making it hard to figure out what context is necessary to render them. This often leads to subtle bugs when reusing a view in different contexts.
By clearly defining the context necessary to render a ViewComponent
, they're easier to reuse than partials.
Views often fail basic Ruby code quality standards: long methods, deep conditional nesting, and mystery guests abound.
ViewComponent
s are Ruby objects, making it easy to follow code quality standards.
Many common Ruby code coverage tools cannot properly handle coverage of views, making it difficult to audit how thorough tests are and leading to missing coverage in test suites.
ViewComponent
is at least partially compatible with code coverage tools, such as SimpleCov.
Components are subclasses of ViewComponent::Base
and live in app/components
. It's recommended to create an ApplicationComponent
that is a subclass of ViewComponent::Base
and inherit from that instead.
Component class names end in -Component
.
Component module names are plural, as they are for controllers. (Users::AvatarComponent
)
Content passed to a ViewComponent
as a block is captured and assigned to the content
accessor.
Use the component generator to create a new ViewComponent
.
The generator accepts the component name and the list of accepted properties as arguments:
bin/rails generate component Example title content
invoke test_unit
create test/components/example_component_test.rb
create app/components/example_component.rb
create app/components/example_component.html.erb
ViewComponent
includes template generators for the erb
, haml
, and slim
template engines and will use the template engine specified in the Rails configuration (config.generators.template_engine
) by default.
The template engine can also be passed as an option to the generator:
bin/rails generate component Example title content --template-engine slim
A ViewComponent
is a Ruby file and corresponding template file with the same base name:
app/components/test_component.rb
:
class TestComponent < ViewComponent::Base
def initialize(title:)
@title = title
end
end
app/components/test_component.html.erb
:
<span title="<%= @title %>"><%= content %></span>
Which is rendered in a view as:
<%= render(TestComponent.new(title: "my title")) do %>
Hello, World!
<% end %>
Which returns:
<span title="my title">Hello, World!</span>
ViewComponent
requires the presence of an initialize
method in each component.
A component can declare additional content areas to be rendered in the component. For example:
app/components/modal_component.rb
:
class ModalComponent < ViewComponent::Base
with_content_areas :header, :body
def initialize(*)
end
end
app/components/modal_component.html.erb
:
<div class="modal">
<div class="header"><%= header %></div>
<div class="body"><%= body %></div>
</div>
Which is rendered in a view as:
<%= render(ModalComponent.new) do |component| %>
<% component.with(:header) do %>
Hello Jane
<% end %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
Which returns:
<div class="modal">
<div class="header">Hello Jane</div>
<div class="body"><p>Have a great day.</p></div>
</div>
A component can be rendered without any template file as well.
app/components/inline_component.rb
:
class InlineComponent < ViewComponent::Base
def call
if active?
link_to "Cancel integration", integration_path, method: :delete
else
link_to "Integrate now!", integration_path
end
end
end
Components can implement a #render?
method to determine if they should be rendered.
For example, given a component that displays a banner to users who haven't confirmed their email address, the logic for whether to render the banner would need to go in either the component template:
app/components/confirm_email_component.html.erb
<% if user.requires_confirmation? %>
<div class="alert">
Please confirm your email address.
</div>
<% end %>
or the view that renders the component:
app/views/_banners.html.erb
<% if current_user.requires_confirmation? %>
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
<% end %>
Instead, the #render?
hook expresses this logic in the Ruby class, simplifying the view:
app/components/confirm_email_component.rb
class ConfirmEmailComponent < ViewComponent::Base
def initialize(user:)
@user = user
end
def render?
@user.requires_confirmation?
end
end
app/components/confirm_email_component.html.erb
<div class="banner">
Please confirm your email address.
</div>
app/views/_banners.html.erb
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
To assert that a component has not been rendered, use refute_component_rendered
from ViewComponent::TestHelpers
.
It's possible to render collections with components:
app/view/products/index.html.erb
<%= render(ProductComponent.with_collection(@products)) %>
Where the ProductComponent
and associated template might look something like the following. Notice that the constructor must take a product
and the name of that parameter matches the name of the component.
app/components/product_component.rb
class ProductComponent < ViewComponent::Base
def initialize(product:)
@product = product
end
end
app/components/product_component.html.erb
<li><%= @product.name %></li>
Additionally, extra arguments can be passed to the component and the name of the parameter can be changed:
app/view/products/index.html.erb
<%= render(ProductComponent.with_collection(@products, notice: "hi")) %>
app/components/product_component.rb
class ProductComponent < ViewComponent::Base
with_collection_parameter :item
def initialize(item:, notice:)
@item = item
@notice = notice
end
end
app/components/product_component.html.erb
<li>
<h2><%= @item.name %></h2>
<span><%= @notice %></span>
</li>
Unit test components directly, using the render_inline
test helper and Capybara matchers:
require "view_component/test_case"
class MyComponentTest < ViewComponent::TestCase
test "render component" do
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
assert_selector("span[title='my title']", "Hello, World!")
end
end
Use the with_variant
helper to test specific variants:
test "render component for tablet" do
with_variant :tablet do
render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
assert_selector("span[title='my title']", "Hello, tablets!")
end
end
ViewComponent::Preview
, like ActionMailer::Preview
, provides a way to preview components in isolation:
test/components/previews/test_component_preview.rb
class TestComponentPreview < ViewComponent::Preview
def with_default_title
render(TestComponent.new(title: "Test component default"))
end
def with_long_title
render(TestComponent.new(title: "This is a really long title to see how the component renders this"))
end
def with_content_block
render(TestComponent.new(title: "This component accepts a block of content") do
tag.div do
content_tag(:span, "Hello")
end
end
end
end
Which generates http://localhost:3000/rails/view_components/test_component/with_default_title, http://localhost:3000/rails/view_components/test_component/with_long_title, and http://localhost:3000/rails/view_components/test_component/with_content_block.
The ViewComponent::Preview
base class includes
ActionView::Helpers::TagHelper
, which provides the tag
and content_tag
view helper methods.
Previews default to the application layout, but can be overridden:
test/components/previews/test_component_preview.rb
class TestComponentPreview < ViewComponent::Preview
layout "admin"
...
end
Preview classes live in test/components/previews
, can be configured using the preview_path
option.
To use lib/component_previews
:
config/application.rb
config.view_component.preview_path = "#{Rails.root}/lib/component_previews"
Component tests and previews assume the existence of an ApplicationController
class, be can beconfigured using the test_controller
option:
config/application.rb
config.view_component.test_controller = "BaseController"
To use RSpec, add the following:
spec/rails_helper.rb
require "view_component/test_helpers"
RSpec.configure do |config|
config.include ViewComponent::TestHelpers, type: :component
end
Specs created by the generator have access to test helpers like render_inline
.
To use component previews:
config/application.rb
config.view_component.preview_path = "#{Rails.root}/spec/components/previews"
Yes. This gem is tested against ERB, Haml, and Slim, but it should support most Rails template handlers.
Inline templates have been removed (for now) due to concerns raised by @soutaro regarding compatibility with the type systems being developed for Ruby 3.
ViewComponent
is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
- Rethinking the View Layer with Components, RailsConf 2019
- Introducing ActionView::Component with Joel Hawksley, Ruby on Rails Podcast
- Rails to Introduce View Components, Dev.to
- ActionView::Components in Rails 6.1, Drifting Ruby
- Demo repository, view-component-demo
Bug reports and pull requests are welcome on GitHub at https://github.com/github/view_component. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct. We recommend reading the contributing guide as well.
view_component
is built by:
@joelhawksley | @tenderlove | @jonspalmer | @juanmanuelramallo | @vinistock |
Denver | Seattle | Boston | Toronto |
@metade | @asgerb | @xronos-i-am | @dylnclrk | @kaspermeyer |
London | Copenhagen | Russia, Kirov | Berkeley, CA | Denmark |
@rdavid1099 | @kylefox | @traels | @rainerborene | @jcoyne |
Los Angeles | Edmonton | Odense, Denmark | Brazil | Minneapolis |
@elia | @cesariouy | @spdawson | @rmacklin | @michaelem |
Milan | United Kingdom | Berlin |
@mellowfish | @horacio | @dukex | @dark-panda | @smashwilson |
Spring Hill, TN | Buenos Aires | São Paulo | Gambrills, MD |
@blakewilliams | @seanpdoyle | @tclem |
Boston, MA | New York, NY | San Francisco, CA |
The gem is available as open source under the terms of the MIT License.