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>
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
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
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>
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>
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
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>
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!
Top comments (1)
Need an extra pair of hands with the process of moving from partials to ViewComponent? Reach out!.