An Introduction to the ViewComponent Gem
Tags: ruby
Published: 2022-03-16 17:03:22

*This article was originally written by [Abiodun Olowode](https://www.honeybadger.io/blog/ruby-view-components/#authorDetails) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/ruby-view-components/).*

Inspired by React, ViewComponents are Ruby objects used to build markup for rendering views. ViewComponent is a framework for building re-usable, testable, and encapsulated view components in Rails. Typically, reusable views are created in Rails using partials and then rendered in different views as required, but with the introduction of the ViewComponent gem, partials can be swapped for view components, as they offer more advantages. Let's dive into what these advantages are.

## When and Why should I use ViewComponents?

As stated earlier, view components are reusable and testable. Hence, they can be applied whenever a view is to be reused or benefits from being tested directly. Some of the benefits of view components, as stated in its documentation, include the following:

- They are ~10x faster than partials.
- They are Ruby objects; hence, their initialize method clearly defines what is required to render a view. This means that they are easier to understand and reuse in several other views. Furthermore, Ruby code’s quality standards can be enforced, thereby offering a reduced risk of bugs.
- They can be unit tested as opposed to Rails’ traditional views, which require integration tests that also exercise routing and controller layers in addition to the view.

## ViewComponent Implementation

ViewComponents are subclasses of `ViewComponent::Base` and live in `app/components` . Their names end in `- Component`, and they should be named for what they render and not what they accept. We can generate a view component either manually or via the component generator, but not without adding the gem to our gemfile and running a `bundle install` first.

```ruby
gem "view_component", require: "view_component/engine"
```

To use the generator, run the following command:

```ruby
rails generate component <Component name> <arguments needed for initialization>
```

For example, if we wanted to generate a component responsible for showcasing a list of in-class courses available on a learning platform, we could use this command:

```ruby
rails g component Course course
```

We're passing the `course` argument to the command because we would initialize this Ruby object with the course we expect it to display, and we named it `Course` because it renders a course. What a coincidence!

![CourseComponent Generated](https://www.honeybadger.io/images/blog/posts/ruby-view-components/coursecomp_generation.png)

As we can see above, the component and its corresponding view are created in the `app/components` folder, along with a test file.

ViewComponent includes template generators for the `erb`, `haml`, and `slim` template engines but will default to whatever template engine is specified in `config.generators.template_engine`.
Nevertheless, you can indicate your preferred template engine using the following:

```ruby
rails generate component Course course --template-engine <your template engine>
```

Let's proceed to create our Course model and some courses to display.

```ruby
rails g model Course title:string price:decimal location:string
rails db:migrate
```

In our console, we can quickly create two new courses:

```ruby
Course.create(title: 'The Art of Learning', price: 125.00, location: 'Denmark')
Course.create(title: 'Organizing your Time', price: 55.00, location: 'London')
```

The `course_component.rb` file is generated with the initialize method in place, as shown below.

![CourseComponent Initialized](https://www.honeybadger.io/images/blog/posts/ruby-view-components/coursecomp_initialize.png)

We need to create a course controller that routes us to the list of courses.

```ruby
rails g controller Courses index
```

In our `routes.rb` file, we indicate our root route by adding the following:

```ruby
root 'courses#index'
```

Now that we're all set, the next step is view creation. This is done in the already generated `course_component.html.erb`.

```erb
<div>
  <h2><%= @course.title %></h2>
  <h4><%=  number_to_currency(@course.price, :unit => "€") %></h4>
  <h4><%= @course.location %></h4>
</div>
```

In our view, we display the course title, price, and location using the `@course` variable, which was already defined in the initialize method of our CourseComponent. This is similar to when you create a variable in a controller method, and then it is available in a view.

Knowing how controllers work, we would be routed to the corresponding `index.html.erb` of our index method. Hence, this is where we render our component.

```erb
#app/views/courses/index.html.erb
<%= render(CourseComponent.new(course: Course.find(1))) %>
```

As seen above, we render a new instance of our CourseComponent by initializing it with the course we intend it to render in its view. This course becomes the `@course` variable made available to the `course_component.html.erb` file.

It is also possible to render this component directly from our controller, thereby bypassing the index file view:

```ruby
class CoursesController < ApplicationController
  def index
    render(CourseComponent.new(course: Course.find(1)))
  end
end
```

Regardless of which method you choose, this will show up on the server:

![First course displayed](https://www.honeybadger.io/images/blog/posts/ruby-view-components/course1_display.png)

Additional content can also be passed to the component in one of the following ways:

```erb
<%= render(CourseComponent.new(course: Course.find(1))) do %>
  container
<% end %>
```

```erb
<%= render(CourseComponent.new(course: Course.find(1)).with_content("container")) %>
```

In our view component file, we can include the content wherever we want. In this case, we'll include it as a class by editing our div to look like this:

```erb
<div class=<%= content %>>
```

All of the above methods of rendering the additional content yield the image below:

![Additional content being rendered](https://www.honeybadger.io/images/blog/posts/ruby-view-components/extra_content.png)

## Rendering a Collection

What if we wanted to render our entire list of courses? ViewComponent provides a very straight forward way of doing this using the `with_collection` tag. Instead of initializing the component using `.new` , it is initialized using `.with_collection`, and the collection is passed to it as a variable, as shown below.

```ruby
CourseComponent.with_collection(Course.all)
```

This yields the following:

![The Course collection rendered](https://www.honeybadger.io/images/blog/posts/ruby-view-components/with_collection.png)

There is also a `with_collection_parameter` tag available in case we wish to address the collection by a different name.

```ruby
class CourseComponent < ViewComponent::Base
  with_collection_parameter :item

  def initialize(item:)
    @item = item
  end
end
```

In the above case, the course parameter has been addressed as `item`. Hence, in the corresponding view, `@course` will be replaced with `@item` to yield the same result.

Additional parameters can also be added to the collection. These parameters will be displayed per item in the collection. Let's add a `Buy Me` text to each item via this method.

```erb
#app/views/courses/index.html.erb
<%= render(CourseComponent.with_collection(Course.all, notice: "Buy Me")) %>
```

```ruby
# app/components/course_component.rb
class CourseComponent < ViewComponent::Base
  with_collection_parameter :item
  def initialize(item:, notice:)
    @item = item
    @notice = notice
  end
end
```

We add a new paragraph to the `app/components/course_component.html.erb` file to indicate the text for the newly added notice variable.

```erb
<p><a href='#'> <%= @notice %> </a></p>
```

This yields the following:

![Courses rendered with an additional notice parameter](https://www.honeybadger.io/images/blog/posts/ruby-view-components/with_notice.png)

Lastly, under collections, we have a counter variable that can be enabled to number the items in a view. It is enabled by adding `_counter` to the collection parameter and making it available to the views via the initialize method.

```ruby
#app/components/course_component.rb
def initialize(item:, notice:, item_counter:)
  @item = item
  @notice = notice
  @counter = item_counter
end
```

In our views, beside the item title, we add our counter:

```erb
<h2><%= @counter %>. <%= @item.title %></h2>
```

Let's generate a third course from the console to better understand the counter phenomenon.

```ruby
Course.create(title: 'Understanding Databases', price: '100', location: 'Amsterdam')
```

This yields the following

![Counter added to each course](https://www.honeybadger.io/images/blog/posts/ruby-view-components/counter.png)

## Conditional Rendering

ViewComponent has a `render?` hook, which, when used, determines whether a view should be rendered. To implement this, we're going to give a 10% discount for courses with prices equal to or greater than 100 Euros. Let's create a component for this purpose.

```ruby
rails generate component Discount item
```

![DiscountComponent generated](https://www.honeybadger.io/images/blog/posts/ruby-view-components/discount.png)

This component is already automatically initialized with the item it should display a discount for, as seen below.

![DiscountComponent initialized](https://www.honeybadger.io/images/blog/posts/ruby-view-components/discount_init.PNG)

Hence, in the `discount_component.html.erb` file, we add the text we intend to display.

```erb
<p class="green"> A 10% discount is available on this course </p>
```

Don't hesitate to add the class `green` to your css file and assign it any shade of green you prefer. Furthermore, in our `discount_component.rb` file, we add the `render?` method to determine when this component should be rendered.

```ruby
def render?
  @item.price >= 100
end
```

Now, we can go ahead and render the discount component within the view that renders each course.

```erb
# app/components/course_component.html.erb
<%= render(DiscountComponent.new(item: @item)) %>
```

This yields the following:

![DiscountComponent rendered](https://www.honeybadger.io/images/blog/posts/ruby-view-components/discount_shown.png)

Isn't that awesome?

## Helpers

In the traditional Rails views, we can easily plug in our helpers by calling the method name in our views, but it works differently with view components. In view components, helper methods cannot be called directly in the views but can be included in a component. We already have a `courses_helper.rb` file that was automatically generated when the CoursesController was created, so let's take advantage of it. First, let's create a helper method that tells us how many people have enrolled in a course so far. Let's make the value a quarter of the price :).

```ruby
module CoursesHelper
  def count_enrollees(course)
    count = (course.price / 4).round()
    tag.p "#{count} enrollees so far"
  end
end
```

Next, we'll create a component in which we'll call the helper. This is the component that will be rendered in our view. In it, we'll add an `include` statement, including the helper, and then we can call any method in the helper within this component.

```ruby
# app/components/enrollee_component.rb
class EnrolleeComponent < ViewComponent::Base
include CoursesHelper

  def total_enrollees(course)
    count_enrollees(course)
  end
end
```

The last step is adding the EnrolleeComponent to the view that displays our courses.

```erb
# app/components/course_component.html.erb
<%= EnrolleeComponent.new.total_enrollees(@item) %>
```

Note that we are not using the render word for the EnrolleeComponent, as it doesn't have a view, and its output will be that of the helper method called. This yields the following:

![Number of enrollees shown using Helpers](https://www.honeybadger.io/images/blog/posts/ruby-view-components/enrollee.png)

Helpers can be used for icons, gravitars, or whatever you might choose. ViewComponent doesn’t change the use of helpers; it just changes how we call them in our components.

## The before_render method

ViewComponent offers a `before_render` method that can be called before a component is rendered. Let's add a star beside our discount notice. We start by adding a helper method that fetches the star; a `star.png` image has also been downloaded and placed in the `app/assets/images` folder.

```ruby
#app/helpers/courses_helper.rb
def star_icon
  image_tag("/assets/star.png", width: "1%")
end
```

Let's add a `before_render` method to our Discount component that calls this helper method.

```ruby
# app/components/discount_component.rb
def before_render
  @star_icon = helpers.star_icon
end
```

As we can see above, another manner of calling helpers is introduced. Helpers can also be called using `helpers.method` in the component.rb file. Then, in our render method for the DiscountComponent, we input our star icon, which is now made available via the `@star_icon` variable.

```erb
# app/components/discount_component.html.erb
<p class="green"> <%= @star_icon %> A 10% discount is available on this course </p>
```

This yields the following:

![Star icon shown using the before render method](https://www.honeybadger.io/images/blog/posts/ruby-view-components/star_icon.png)

We do not necessarily have to use helpers for the `before_render` method to work. I have used helpers to introduce another approach to calling helper methods.

## Previews

Like Action Mailer, ViewComponent makes it possible to preview components. This has to first be enabled in the `config/application.rb` file.

```ruby
config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
```

Preview components are located in `test/components/previews` and can be used to preview a component with several different inputs passed in. We'll be previewing our DiscountComponent.

```ruby
# test/components/previews/discount_component_preview.rb
class DiscountComponentPreview < ViewComponent::Preview
  def with_first_course
    render(DiscountComponent.new(item: Course.find(1)))
  end

  def with_second_course
    render(DiscountComponent.new(item: Course.find(2)))
  end
end
```

Two methods have been added to preview the DiscountComponent in different scenarios. To view this, visit `http://localhost:3000/rails/view_components`, where we find all the preview components created and their methods. We can click on any of them to view what they look like, as shown below.

![DiscountComponent previewed](https://www.honeybadger.io/images/blog/posts/ruby-view-components/previews.gif)

As you can see in the video above, the first course renders the discount component, but nothing gets rendered for the second course. Do you know why this happened?  The `render?` method check happened. The first scenario is when the cost price is more than 100 euros(the 1st course), but the cost is less than 100 euros for the second course. The method names were not more descriptive to enable you figure out the cause before it is highlighted here :).

Previews can be enabled or disabled in any environment using the `show_previews` option, but in development and testing environments, they are enabled by default.

```ruby
# config/environments/test.rb
config.view_component.show_previews = false
```

## JS AND CSS Inclusion

It's possible to include JavaScript and CSS alongside components, sometimes called "sidecar" assets or files. This is still an experimental feature. Hence, we won't dive into its inner workings in this article, but you can find more about this ViewComponent feature [here](https://viewcomponent.org/guide/sidecar_assets.html).

## Templates

The template of a view component can be defined in several ways. The simpler option is inserting the view and component in the same folder, as shown below.

![A simple template for view components](https://www.honeybadger.io/images/blog/posts/ruby-view-components/template_1.png)

As we can see here, we have every component and view in the `app/components` folder.

Another option is to place the view and other assets in a subdirectory with the same name as the component. Thus, in the `app/components` folder, we have the `component.rb` file, which houses the component, and then a separate `course_component` folder, which houses the view `course_component.html.erb` and every other asset related to the course_component.

![ViewComponent template for asset inclusion](https://www.honeybadger.io/images/blog/posts/ruby-view-components/template_2.png)

To generate component files in this way from the command line, the `--sidecar` flag is required:

```ruby
rails g component Example name  --sidecar
```

This enables you add your css and js files to the component folder.
ViewComponents can also render without a template file by defining a `call` method. An example of this is provided in the next section, where we discuss _slots_.

## Slots

Multiple blocks of content can be passed to a single ViewComponent using _slots_. Similar to the `has_one` and `has_many` attributes in Rails models, slots are defined with `renders_one` and `renders_many`:

- renders_one defines a slot that will be rendered at most once per component: renders_one :header.
- renders_many defines a slot that can be rendered multiple times per-component: renders_many :titles.

Imagine that we want to have a page that renders a header and the titles of all the courses we have available; this can be achieved using **slots**. Let's create a **ListComponent**, which will contain a header that is only rendered once, and a **TitleComponent**, which renders many titles.

```ruby
#app/components/list_component.rb
class ListComponent < ViewComponent::Base
  renders_one :header, "HeaderComponent"
  # `HeaderComponent` is defined within this component, so we refer to it using a string.
  renders_many :titles, TitleComponent
  # `titleComponent` will be defined in another file, so we can refer to it by class name.
  class HeaderComponent < ViewComponent::Base
    def call
      content_tag :h1, content
    end
  end
end
```

In the component above, we stated that the header is rendered once, but the titles are rendered many times, as this page would contain many titles. We have also defined the HeaderComponent within the ListComponent. Yes, this is possible with ViewComponent; a class can be defined within another class. Let's also take note of the call method discussed earlier under the Templates section and how it's being used in the HeaderComponent to render an h1 tag, thereby eliminating the need for a corresponding view(html.erb file). The corresponding HTML file for the ListComponent would contain the following;

```erb
#app/components/list_component.html.erb
<div>
  <%= header %> <!-- renders the header component -->
  <% titles.each do |title| %>
    <div>
      <%= title %> <!-- renders an individual course title -->
    </div>
  <% end %>
</div>
```

In the html file, we have included the header, iterated through all the titles passed into the component, and rendered them. As you can see, we do not have to specify the name of the component to be rendered in the list view file; our slots have taken care of that. Hence, we just identify them as `header` and `title`.

The next step is to create our TitleComponent and its corresponding HTML file, as this is what will be rendered for every title passed.

```ruby
# app/components/title_component.rb
class TitleComponent < ViewComponent::Base
  def initialize(title:)
    @title = title
  end
end
```

```erb
# app/components/title_component.html.erb
<div>
  <h3> <%= @title %> </h3>
</div>
```

Finally, in our `index.html` file, let's temporarily erase what we have and replace it with the ListComponent render.

```erb
#app/views/courses/index.html.erb
<%= render ListComponent.new do |c| %>
  <% c.header do %>
  <%= link_to "List of Available Courses", root_path %>
  <% end %>
  <%= c.title(title: "First title") %>
  <%= c.title(title: "Second title!") %>
<% end %>
```

![Individual titles rendered using Slots](https://www.honeybadger.io/images/blog/posts/ruby-view-components/titles_1.png)

Now, let's pass our courses into this view as a collection. To pass a collection into a slot, we have to pass the collection as an array of objects containing the variable needed for initialization. As we can see, every course passed in should be initialized with the title argument. We can extract the titles of all the courses in our db into an array of hashes and render it.

![A hash containing the titles of all the courses](https://www.honeybadger.io/images/blog/posts/ruby-view-components/hash.png)

We can now replace our list of several titles with a single `c.titles` for a collection and pass it to our hash of titles, which we define using the variable `course_titles`.

```erb
# app/views/courses/index.html.erb
<% course_titles = Course.all.pluck(:title).map { |title| {title: title}} %>

<% c.titles(course_titles) %>
```

This yields the following:

![A collection of titles rendered using Slots](https://www.honeybadger.io/images/blog/posts/ruby-view-components/slots.png)

This is exactly how slots work: rendering several components within a single ViewComponent. Slots can be rendered in several other ways, and you can find more info [here](https://viewcomponent.org/guide/slots.html).

## Testing

Testing view components is done by requiring "view_component/test_case" in the test file and using the `render_inline` test helper so that assertions are made against the rendered output. Let's begin by testing our DiscountComponent.

```ruby
require "test_helper"
require "view_component/test_case"

class DiscountComponentTest < ViewComponent::TestCase
  def test_render_component
    render_inline(DiscountComponent.new(item: "my item"))
  end
end
```

When we run this test using the command `rails test test/components/discount_component_test.rb`, we get the following error:

![DiscountComponent Test Error Message](https://www.honeybadger.io/images/blog/posts/ruby-view-components/test_error.png)

This proves to us that the test is getting to the right component but is lacking a suitable prop, as the item must be a course with a price property and not a string, as we passed. It also tells us that there is a `render?` method being checked before this component renders. Let's pass in the right variables now.

```ruby
def test_render_component
  course = Course.create(title: 'Organizing your Time', price: 55.00, location: 'London')
  render_inline(DiscountComponent.new(item: course))
end
```

This runs successfully. Let's proceed to add assertions to this test.

```ruby
def test_render_component
  course = Course.create(title: 'Organizing your Time', price: 155.00, location: 'London')
  render_inline(DiscountComponent.new(item: course))
  assert_selector 'p[class="green"]'
  assert_text "10% discount"
end
```

This test also passes.
Recall that there is a render condition for this component. Don’t worry, though, because ViewComponent also provides a way to test that a component is not rendered, by using `refute_component_rendered`. We can test this using a course with a price below 100 euros.

```ruby
def test_component_not_rendered
  course = Course.create(title: 'Organizing your Time', price: 55.00, location: 'London')
  render_inline(DiscountComponent.new(item: course))
  refute_component_rendered
end
```

This test also passes.

Let's write another test for our CourseComponent to test that it renders all the components nested within it.

```ruby
require "test_helper"
require "view_component/test_case"

class CourseComponentTest < ViewComponent::TestCase
  def test_component_renders_all_children
    course = Course.create(title: 'Organizing your Time', price: 155.00, location: 'London')
    render_inline(CourseComponent.new(item: course, notice: 'A new test', item_counter: 1))
    assert_selector("h2", text: "Organizing your Time")
    assert_selector("h4", text: "€155.00")
    assert_selector("h4", text: "London")
    assert_text("enrollees")
    assert_text("discount")
  end
end
```

This test also passes. It tests that the Enrollee and Discount components are also rendering properly.

Recall that we have a slot component, and as shown in the image below, it renders one header and many titles.

![ListComponent using slots to render other Components](https://www.honeybadger.io/images/blog/posts/ruby-view-components/slot_2.png)

To test this, we pass the component a block of code containing the header and titles it should render, and then we can assert against the rendered component.

```ruby
require "test_helper"
require "view_component/test_case"

class ListComponentTest < ViewComponent::TestCase
  def test_renders_slots_with_content
  render_inline(ListComponent.new) do |component|
  component.header { "A Test List" }
  component.titles [{title: 'Test title 1'}, {title: 'Test title 2'}]
  end

  assert_selector("h1", text: "A Test List")

  assert_text("Test title 1")
  assert_text("Test title 2")
  end
end
```

This test also passes :).

## Rspec Testing

In addition to all that has been said above about testing, if the preferred testing framework is RSpec, some additional configurations must be carried out to enable RSpec for ViewComponents.

```ruby
# spec/rails_helper.rb
require "view_component/test_helpers"
require "capybara/rspec"

RSpec.configure do |config|
  config.include ViewComponent::TestHelpers, type: :component
  config.include Capybara::RSpecMatchers, type: :component
end
```

Our DiscountComponent test can be re-written and retested using Rspec, as shown below:

```ruby
require "rails_helper"

RSpec.describe DiscountComponent, type: :component do

  it "renders the component correctly" do
    course = Course.create(title: 'Organizing your Time', price: 155.00, location: 'London')
    render_inline(DiscountComponent.new(item: course))
    expect(rendered_component).to have_css "p[class='green']", text: "10% discount"
    expect(rendered_component).to have_css "img[src*='/assets/star.png']"
  end
end
```

This test passes elegantly. Hell yeah, we can see our star icon.

## Conclusion

Writing several view components for your Rails app not only makes your code more readable and less prone to errors from unwanted complications but also makes it possible to test your views in isolation and not only during HTTP requests. View components are easy to inculcate into an existing Rails app, and its best to start with views that are mostly reused. With everything learned so far, this should be an easy-peasy task. Nevertheless, if you would like more information on ViewComponent, do not hesitate to go through its [documentation](https://viewcomponent.org/) or the [RubyDoc info](https://www.rubydoc.info/gems/view_component/2.21.0).