Note: This gem is in the process of a name / API change, see ViewComponent#206
You are viewing the README for the development version of ActionView::Component. If you are using the current release version you can find the README at https://github.com/github/actionview-component/blob/v1.11.1/README.md
ActionView::Component
is a framework for building view components in 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.
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 patch that enables that support 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.
actionview-component
is tested for compatibility with combinations of Ruby 2.5
/2.6
/2.7
and Rails 5.0.0
/5.2.3
/6.0.0
/6.1.0.alpha
.
Add this line to your application's Gemfile:
gem "actionview-component"
And then execute:
$ bundle
In config/application.rb
, add:
require "action_view/component/railtie"
ActionView::Component
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.
In working on views in the Rails monolith at GitHub (which has over 3700 templates), we have run into several key pain points:
Currently, Rails encourages testing views via integration or system tests. This discourages us from testing our views thoroughly, due to the costly overhead of exercising the routing/controller layer, instead of just the view. It also often leads to partials being tested for each view they are included in, cheapening the benefit of DRYing up our views.
Many common Ruby code coverage tools cannot properly handle coverage of views, making it difficult to audit how thorough our tests are and leading to gaps in our test suite.
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 we reuse a view across different contexts.
Our views often fail even the most basic standards of code quality we expect out of our Ruby classes: long methods, deep conditional nesting, and mystery guests abound.
ActionView::Component
allows views to be unit-tested. In the main GitHub codebase, our unit tests run in around 25ms/test, vs. ~6s/test for integration tests.
ActionView::Component
is at least partially compatible with code coverage tools. We’ve seen some success with SimpleCov.
By clearly defining the context necessary to render a component, we’ve found them to be easier to reuse than partials.
Components are most effective in cases where view code is reused or needs to be tested directly.
Components are subclasses of ActionView::Component::Base
and live in app/components
. You may wish to create an ApplicationComponent
that is a subclass of ActionView::Component::Base
and inherit from that instead.
Component class names end in -Component
.
Component module names are plural, as they are for controllers. (Users::AvatarComponent
)
Components support ActiveModel validations. Components are validated after initialization, but before rendering.
Content passed to an ActionView::Component
as a block is captured and assigned to the content
accessor.
Use the component generator to create a new ActionView::Component
.
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
ActionView::Component
includes template generators for the erb
, haml
, and slim
template engines and will use the template engine specified in your Rails config (config.generators.template_engine
) by default.
If you want to override this behavior, you can pass the template engine as an option to the generator:
bin/rails generate component Example title content --template-engine slim
invoke test_unit
create test/components/example_component_test.rb
create app/components/example_component.rb
create app/components/example_component.html.slim
An ActionView::Component
is a Ruby file and corresponding template file (in any format supported by Rails) with the same base name:
app/components/test_component.rb
:
class TestComponent < ActionView::Component::Base
validates :content, :title, presence: true
def initialize(title:)
@title = title
end
private
attr_reader :title
end
app/components/test_component.html.erb
:
<span title="<%= title %>"><%= content %></span>
We can render it in a view as:
<%= render(TestComponent.new(title: "my title")) do %>
Hello, World!
<% end %>
Which returns:
<span title="my title">Hello, World!</span>
If the component is rendered with a blank title:
<%= render(TestComponent.new(title: "")) do %>
Hello, World!
<% end %>
An error will be raised:
ActiveModel::ValidationError: Validation failed: Title can't be blank
A component can declare additional content areas to be rendered in the component. For example:
app/components/modal_component.rb
:
class ModalComponent < ActionView::Component::Base
validates :user, :header, :body, presence: true
with_content_areas :header, :body
def initialize(user:)
@user = user
end
attr_reader :user
end
app/components/modal_component.html.erb
:
<div class="modal">
<div class="header"><%= header %></div>
<div class="body"><%= body %></div>
</div>
We can render it in a view as:
<%= render(ModalComponent.new(user: {name: 'Jane'})) do |component| %>
<% component.with(:header) do %>
Hello <%= component.user[:name] %>
<% 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>
Content for content areas can be passed as arguments to the render method or as named blocks passed to the with
method.
This allows a few different combinations of ways to render the component:
app/components/modal_component.rb
:
class ModalComponent < ActionView::Component::Base
validates :header, :body, presence: true
with_content_areas :header, :body
def initialize(header:)
@header = header
end
end
<%= render(ModalComponent.new(header: "Hi!")) do |component| %>
<% component.with(:header) do %>
<span class="help_icon"><%= component.header %></span>
<% end %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
app/components/modal_component.rb
:
class ModalComponent < ActionView::Component::Base
validates :header, :body, presence: true
with_content_areas :header, :body
def initialize(header: nil)
@header = header
end
end
app/views/render_arg.html.erb
:
<%= render(ModalComponent.new(header: "Hi!")) do |component| %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
app/views/with_block.html.erb
:
<%= render(ModalComponent) do |component| %>
<% component.with(:header) do %>
<span class="help_icon">Hello</span>
<% end %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
app/components/modal_component.rb
:
class ModalComponent < ActionView::Component::Base
validates :body, presence: true
with_content_areas :header, :body
def initialize(header: nil)
@header = header
end
end
app/components/modal_component.html.erb
:
<div class="modal">
<% if header %>
<div class="header"><%= header %></div>
<% end %>
<div class="body"><%= body %></div>
</div>
app/views/render_arg.html.erb
:
<%= render(ModalComponent.new(header: "Hi!")) do |component| %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
app/views/with_block.html.erb
:
<%= render(ModalComponent.new) do |component| %>
<% component.with(:header) do %>
<span class="help_icon">Hello</span>
<% end %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
app/views/no_header.html.erb
:
<%= render(ModalComponent.new) do |component| %>
<% component.with(:body) do %>
<p>Have a great day.</p>
<% end %>
<% end %>
Components can implement a #render?
method which indicates if they should be rendered, or not at all.
For example, you might have a component that displays a "Please confirm your email address" banner to users who haven't confirmed their email address. The logic for rendering 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 %>
The #render?
hook allows you to move this logic into the Ruby class, leaving your views more readable and declarative in style:
# app/components/confirm_email_component.rb
class ConfirmEmailComponent < ActionView::Component::Base
def initialize(user:)
@user = user
end
def render?
@user.requires_confirmation?
end
attr_reader :user
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)) %>
Components are unit tested directly. The render_inline
test helper is compatible with Capybara matchers, allowing us to test the component above as:
require "action_view/component/test_case"
class MyComponentTest < ActionView::Component::TestCase
test "render component" do
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
assert_selector("span[title='my title']", "Hello, World!")
end
end
In general, we’ve found it makes the most sense to test components based on their rendered HTML.
To test a specific variant you can wrap your test with the with_variant
helper method as:
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
ActionView::Component::Preview
provides a way to see how components look by visiting a special URL that renders them.
In the previous example, the preview class for TestComponent
would be called TestComponentPreview
and located in test/components/previews/test_component_preview.rb
.
To see the preview of the component with a given title, implement a method that renders the component.
You can define as many examples as you want:
# test/components/previews/test_component_preview.rb
class TestComponentPreview < ActionView::Component::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
end
The previews will be available in http://localhost:3000/rails/components/test_component/with_default_title and http://localhost:3000/rails/components/test_component/with_long_title.
Previews use the application layout by default, but you can also use other layouts from your app:
# test/components/previews/test_component_preview.rb
class TestComponentPreview < ActionView::Component::Preview
layout "admin"
...
end
By default, the preview classes live in test/components/previews
.
This can be configured using the preview_path
option.
For example, if you want to use lib/component_previews
, set the following in config/application.rb
:
config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
By default components tests and previews expect your Rails project to contain an ApplicationController
class from which Controller classes inherit.
This can be configured using the test_controller
option.
For example, if your controllers inherit from BaseController
, set the following in config/application.rb
:
config.action_view_component.test_controller = "BaseController"
If you're using RSpec, you can configure component specs to have access to test helpers. Add the following to
spec/rails_helper.rb
:
require "action_view/component/test_helpers"
RSpec.configure do |config|
# ...
# Ensure that the test helpers are available in component specs
config.include ActionView::Component::TestHelpers, type: :component
end
Specs created by the generator should now have access to test helpers like render_inline
.
To use component previews, set the following in config/application.rb
:
config.action_view_component.preview_path = "#{Rails.root}/spec/components/previews"
In Ruby 2.6.x and below, ActionView::Component requires the presence of an initialize
method in each component.
However, initialize
is no longer required for projects using 2.7.x and above.
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.
ActionView::Component
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, actionview-component-demo
Bug reports and pull requests are welcome on GitHub at https://github.com/github/actionview-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.
actionview-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 |
The gem is available as open source under the terms of the MIT License.