Best practices to keep you Rails views tidy and your components smart.
Think in terms of component. Can those few lines be reused somewhere else in your application? If the answer is yes, it should be in a partial.
A good rule of thumb when iterating using each
is to always use a partial. If you have more than a few lines of HTML in the block of an each
, you should probably have a partial, especially if those lines could be reused somewhere else (=component).
<% collection.each do |element| %>
<!-- Not more than a few lines here... else use a partial! -->
<% end %>
A partial may be used anywhere in your app. You can't count on always having an @instance variable set in the controller. Instead, explicitly pass the instance you need to render a partial.
<!-- app/views/posts/_card.html.erb -->
<h1><%= @post.title %></h1>
<!-- app/views/posts/show.html.erb -->
<%= render 'posts/card' %>
You can't use this partial:
- on any page that doesn't have an
@post
in the controller, or even - in a simple iteration like
@posts.each do |post|
(you have apost
, not a@post
)!
<!-- app/views/posts/_card.html.erb -->
<h1><%= post.title %></h1>
<!-- app/views/posts/show.html.erb -->
<%= render 'posts/card', post: @post %>
Just like you would give an argument when calling a method, you give a post
to your partial when you render (=call) it.
Rails has some nice conventions with filenames that you can use to clean up your views.
For example, each model usually has a main card partial. By saving your default post
card under views/posts/_post.html.erb
, instead of:
<%= render 'posts/post', post: @post %>
... you can now simply use:
<%= render @post %>
render
will automatically iterate over a collection of posts π€―
<%= render @posts %>
is equivalent to
<% @posts.each do |post| %>
<%= render 'posts/post', post: post %>
<% end %>
According to the Rails documentation:
The lookup order for an admin/products#index action will be:
app/views/admin/products/ app/views/admin/ app/views/application/
Which means app/views/application/
is a convenient place to store all of the shared partials of your app.
For example, if you create a partial
app/views/application/_awesome_button.html.erb
you can render it with:
<%= render 'awesome_button' %>
You can pass any kind of local variables to your partials, not just instances. For example:
<%= render 'avatar', user: user, large: true %>
<!-- app/views/users/_user.html.erb -->
<!-- ... -->
<%= image_tag user.avatar_url, class: large ? 'avatar large' : 'avatar' %>
<!-- or since Rails 6.1 -->
<%= image_tag user.avatar_url, class: class_names('avatar', large: large) %>
<!-- ... -->
class_names
is a new helper introduced in Rails 6.1 which is useful to add conditional classes to your HTML elements.
The previous implementation of the partial will raise undefined variable
if you don't set a value for large
when rendering the partial. Ideally, large
should be optional.
<!-- app/views/users/_user.html.erb -->
<!-- ... -->
<%= image_tag user.avatar_url, class: local_assigns[:large] ? 'avatar large' : 'avatar' %>
<!-- Or since Rails 6.1 -->
<%= image_tag user.avatar_url, class: class_names('avatar', large: local_assigns[:large]) %>
<!-- ... -->
local_assigns
is a hash that contains all the local variables passed to your partial. If a variable was not passed, it returns nil
(like a normal hash would when accessing a key that doesn't exist).
Sometimes, you need a standard wrapper to use with many different components. For example, a card wrapper containing a consistent structure and style for the cards throughout your app. However, the content inside the card wrapper needs to be flexible.
How can we avoid copy-pasting that same wrapper in every card partial...?
Create a wrapper partial, and add yield
where you want some content to be inserted later.
<!-- app/views/application/_card_wrapper.html.erb -->
<div class="card-wrapper">
<% if local_assigns[:photo] %>
<%= img_tag :photo, class: 'card-photo' %>
<% end %>
<div class="card-content">
<%= yield %> <!-- content will be inserted here -->
</div>
</div>
Create your card component, render and pass a block to the wrapper partial:
<!-- app/views/posts/_post.html.erb -->
<%= render 'card-wrapper', photo: post.photo_url do %>
<h1><%= post.title %></h1>
<p><%= post.summary %></p>
<% end %>
Now, you can just use <%= render post %>
in your view, which will render:
<div class="card-wrapper">
<% if post.photo_url %>
<%= img_tag post.photo_url, class: 'card-photo' %>
<% end %>
<div class="card-content">
<h1><%= post.title %></h1>
<p><%= post.summary %></p>
</div>
</div>