From Partials (and Helpers) to Embracing ViewComponent in Rails

Rails Designer - Aug 1 - - Dev Community

This article was originally published on Rails Designer


Historically Rails' answer to the view layer was a mix of views, partials and helpers. And when you start out with a somewhat-basic app, this will work just fine.

Maybe you are starting to feel the pain of using Rails' partials and helpers already or maybe you want to make sure you never get there and want to move to ViewComponent already.

In either case you have some (or many) partials (that uses one or many methods from helpers).

Why use ViewComponent? I've written a full article on why you should choose ViewComponent. It covers everything from the pros, the cons and advanced features like slots.

What steps to go through when you want to take the plunge and move from partials to ViewComponent?

Start with one partial

Depending on the size of your app you might already have many partials already, and it might feel like a too big a task to pull off. Just remember that a big promise of ViewComponent is speed-increase (including testing). So over time, you and your team might see a productivity boost!

But whatever size you're at: start with one partial. Use it as a playground. Ideally a partial:

  • that uses a helper method;
  • uses a collection.

The reason is I want helpers to be only “global” (like some of the Rails Designer View Helpers). And a collection would be ideal as it generally needs a bit more work.

🎨 Your Rails app's UI not up to today's standard? Need some professionally-designed UI components ready to copy/paste into your app? Check out Rails Designer.

Example

Enough theory! Let's look an an example:

<div class="messages-list">
  <h2>Messages (<⁠%= @messages.count %>)</h2>

  <ul class="flex flex-col gap-y-3">
    <⁠% @messages.each do |message| %>
      <li class="message-item">
        <div class="flex items-center justify-between">
          <strong><⁠%= message.sender.name %></strong>

          <small><⁠%= format_message_timestamp(message.created_at) %></small>
        </div>

        <p><⁠%= truncate_message_body(message.body) %></p>

        <div class="flex items-center justify-between">
          <⁠%= message_status_badge(message.status) %>

          <⁠%= link_to "View", message_path(message), class: "btn btn--primary" %>
        </div>
      </li>
    <⁠% end %>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

It's your typical Rails partial to list a list of messages. As you can see it uses a few helper methods too:

module MessagesHelper
  def format_message_timestamp(timestamp)
    timestamp.strftime("%b %d, %Y at %I:%M %p")
  end

  def truncate_message_body(body)
    truncate(body, length: 100, separator: " ")
  end

  def message_status_badge(status)
    case status
    when "read"
      content_tag(:span, "Read", class: "badge badge-success")
    when "unread"
      content_tag(:span, "Unread", class: "badge badge-warning")
    else
      content_tag(:span, "Unknown", class: "badge badge-secondary")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The beautiful thing about ViewComponent is that it can work without much work. Let's create a component first (this assuming you have it added to your app): rails g component MessagesList. By default this creates two files:

  • app/components/messages_list_component.rb
  • app/components/messages_list_component.html.erb

The former is where typically all methods go (eg. format_message_timestamp and message_status_badge from the MessagesHelpers. The latter where your erb code goes.

Let's set up the app/components/messages_list_component.rb first.

class MessagesListComponent <  ViewComponent::Base
  def initialize(message:)
    @message = message
  end
end
Enter fullscreen mode Exit fullscreen mode

The app/components/messages_list_component.html.erb:

<li class="message-item">
  <div class="flex items-center justify-between">
    <strong><⁠%= @message.sender.name %></strong>

    <small><⁠%= format_message_timestamp(@message.created_at) %></small>
  </div>

  <p><⁠%= truncate_message_body(@message.body) %></p>

  <div class="flex items-center justify-between">
     <⁠%= message_status_badge(@message.status) %>

    <⁠%= link_to "View", message_path(@message), class: "btn btn--primary" %>
  </div>
</li>
Enter fullscreen mode Exit fullscreen mode

That looks the same as the partial! It is indeed just a copy/paste!

How to render this component in a view? Like so:

<div class="messages-list">
  <h2>Messages (<⁠%= @messages.count %>)</h2>

  <ul class="flex flex-col gap-y-3">
    <⁠%= render(MessagesListComponent.with_collection(@messages)) %>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

This uses ViewComponent's collections feature.

Let's now move to the helper's methods. Because they are “scoped” in a MessagesHelper module, they are by no means scoped in the real sense of the world. Anywhere in your app, you can use truncate_message_body. Which could turn into nasty bugs!

So move the methods from the MessagesHelper and delete that file! 🗑️

class MessagesListComponent <  ViewComponent::Base
  def initialize(message:)
    @message = message
  end

  def timestamp
    @message.created_at.strftime("%b %d, %Y at %I:%M %p")
  end

  def truncated_body
    truncate(@message.body, length: 100, separator: ' ')
  end

  def status_badge
    case @message.status
    when "read"
      content_tag(:span, "Read", class: "badge badge-success")
    when "unread"
      content_tag(:span, "Unread", class: "badge badge-warning")
    else
      content_tag(:span, "Unknown", class: "badge badge-secondary")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

With that done, let's update the app/components/messages_list_component.html.erb:

<li class="message-item">
  <div class="flex items-center justify-between">
    <strong><⁠%= @message.sender.name %></strong>

    <small><⁠%= timestamp %></small>
  </div>

  <p><⁠%= truncates_body %></p>

  <div class="flex items-center justify-between">
     <⁠%= status_badge %>

    <⁠%= link_to "View", message_path(@message), class: "btn btn--primary" %>
  </div>
</li>
Enter fullscreen mode Exit fullscreen mode

That's it. Your first ViewComponent done! 🌟 The component now uses its own methods instead of the globally available helper methods.

Use ViewComponent for new UI elementscomponents only

Once you get a good feel for the first new ViewComponent. Create that Pull Request! Share it with your team. See what they have to say. Any improvements to be made? What concerns to they have?

In general it's good practice to document as much as possible on certain decisions when you introduce a new technology. Provide extra articles, resources and so on. Enough reading material on this site!

Once you got the okay from your team. Don't move every partial over to ViewComponent. In fact not every partial might be moved over at all.

When to move a partial over?

  • it uses methods from helpers that are only concerned with that element;
  • it uses a lot of variables;
  • it needs solid testing (eg. admin controls).

For example, almost all the apps I built, use a views/shared/_head.html.erb partial. It has the typical head-element for every layout. There's no need for any variables, testing or whatever. So I keep it as a partial.

And then over time, when you and your team become more comfortable with ViewComponent start plucking away at your existing partials and move each of them over to ViewComponent. Keep those PR's small!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player