Welcome back to the third installment of the Ruby on Rails Patterns and Anti-Patterns series. In the previous posts, we covered patterns and anti-patterns in general as well as in relation to Rails Models. In this post, we are going to go over some patterns and anti-patterns associated with Rails views.
Rails views can sometimes work perfectly and be fast, and at other times, they can have all sorts of issues. If you want to increase confidence over how you handle your views or you just want to learn more on the topic, then this blog post is for you. Let's dive right in.
As you probably know, the Rails framework follows convention over configuration. And since Rails is big on the Model-View-Controller (MVC) pattern, the motto naturally applies to the View code as well. This includes your markup (ERB or Slim files), JavaScript and CSS files. At first glance, you might think that the View layer is pretty straightforward and easy, but keep in mind that these days, there is a mix of technologies living in the View layer.
We use JavaScript, HTML, and CSS in the view. These three can lead to confusion and disorganization of code — leading to implementation that doesn't make much sense in the long run. Luckily, today we are going to go through some common problems and solutions with the Rails View layer.
Powerlifting Views
This is a mistake that doesn't happen that often, but when it does, it's an eyesore. Sometimes, people tend to put the domain logic or querying directly inside the View. This makes the View layer do the heavy-lifting or powerlifting. What is interesting is that Rails actually allows this to easily happen. There is no 'safety net' when it comes to this, you are allowed to do whatever you want in the View layer.
By definition, the View layer of the MVC pattern should contain presentation
logic. It shouldn't be bothered with domain logic or with querying data. In Rails,
you get ERB files (Embedded Ruby) that allow you to write Ruby code
that will then get evaluated into HTML. If we consider an example of a
website that lists songs on the index page, then the view logic would be in
the app/views/songs/index.html.erb
.
To illustrate what "powerlifting" means and what not do to, let's take a look at the following example:
1# app/views/songs/index.html.erb
2
3<div class="songs">
4 <% Song.where(published: true).order(:title) do |song| %>
5 <section id="song_<%= song.id %>">
6 <span><%= song.title %></span>
7
8 <span><%= song.description %></span>
9
10 <a href="<%= song.download_url %>">Download</a>
11 </section>
12 <% end %>
13</div>
A huge anti-pattern here is the fetching of songs right in the markup. The responsibility of fetching the data should be delegated to the controller or a service that is being called from the controller. I sometimes see people prepare some data in the controller and later fetch more data in the views. This is bad design and it makes your website slower because you are stressing your database with queries more often.
What you should do instead is to expose a @songs
instance variable from the
controller action and call that in the markup, like so:
1class SongsController < ApplicationController
2 ...
3
4 def index
5 @songs = Song.all.where(published: true).order(:title)
6 end
7
8 ...
9end
1# app/views/songs/index.html.erb
2
3<div class="songs">
4 <% @songs.each do |song| %>
5 <section id="song_<%= song.id %>">
6 <span><%= song.title %></span>
7
8 <span><%= song.description %></span>
9
10 <a href="<%= song.download_url %>">Download</a>
11 </section>
12 <% end %>
13</div>
These examples are far from perfect. If you want to keep your controller code more readable and avoid SQL Pasta, I urge you to check out the previous blog post. Also, leaving out the logic in the View layer increases the chances that other people will try to build their solutions off of it.
Make Use of What Rails Gives You
We will keep it short here. Ruby on Rails as a framework comes with a lot of neat helpers, especially inside the view. These nifty little helpers allow you to build your View layer quickly and effortlessly. As a beginner user of Rails, you might be tempted to write the full HTML inside your ERb files like so:
1# app/views/songs/new.html.erb
2
3<form action="/songs" method="post">
4 <div class="field">
5 <label for="song_title">Title</label>
6 <input type="text" name="song[title]" id="song_title">
7 </div>
8
9 <div class="field">
10 <label for="song_description">Description</label>
11 <textarea name="song[description]" id="song_description"></textarea>
12 </div>
13
14 <div class="field">
15 <label for="song_download_url">Download URL</label>
16 <textarea name="song[download_url]" id="song_download_url"></textarea>
17 </div>
18
19 <input type="submit" name="commit" value="Create Song">
20</form>
With this HTML, you should get a nice form for a new song as seen in the screenshot below:
But, with Rails, you don't need and you shouldn't write plain HTML like that
since Rails has your back right there. You can use the form_with
view helper that
will generate the HTML for you. form_with
was introduced in Rails 5.1 and it
is there to replace form_tag
and form_for
that might be familiar to some
folk. Let's see how form_with
can relieve us from writing extra code:
1<%= form_with(model: song, local: true) do |form| %>
2 <div class="field">
3 <%= form.label :title %>
4 <%= form.text_field :title %>
5 </div>
6
7 <div class="field">
8 <%= form.label :description %>
9 <%= form.text_area :description %>
10 </div>
11
12 <div class="field">
13 <%= form.label :download_url do %>
14 Download URL
15 <% end %>
16 <%= form.text_area :download_url %>
17 </div>
18
19 <%= form.submit %>
20<% end %>
Besides generating HTML for us, form_with
also generates an authenticity
token that prevents CSRF attacks. So in almost all cases, you are better off
using designated helpers since they might play well with the Rails framework.
If you tried to submit a plain HTML form, it will fail because there was no
valid authenticity token submitted with the request.
Besides form_with
, label
, text_area
, and submit
helpers, there are a
bunch more of these view helpers that come out-of-the-box with Rails. They are
there to make your lives easier and you should get to know them better. One of the "all-stars" is definitely link_to
:
1<%= link_to "Songs", songs_path %>
Which will generate the following HTML:
1<a href="/songs">Songs</a>
I won't go into much detail on each helper, since this post will be too long and going through all of them is not part of today's topic. I suggest you go through Rails Action View helpers guide and pick what you need for your website.
Reusing and Organizing View Code
Let's imagine the perfect web application. In the perfect use-case, there are no if-else statements, just pure code that takes data from the controller and puts it between HTML tags. That kind of application exists maybe in hackathons and dreams, but real-world applications have a bunch of branches and conditions when rendering views.
What should you do when the logic for showing parts of a page gets too complex? Where do you go from there? A general answer would be to perhaps reach for a modern JavaScript library or framework and build something complex. But, since this post is about Rails Views, let's look at the options we have inside them.
After-Market (Custom) Helpers
Let's say you want to show a call-to-action (CTA) button below a song. But, there is a catch — a Song can either have a download URL or, for whatever reason, it can be missing. We might be tempted to code something similar to the following:
1app/views/songs/show.html.erb
2
3...
4
5<div class="song-cta">
6 <% if @song.download_url %>
7 <%= link_to "Download", download_url %>
8 <% else %>
9 <%= link_to "Subscribe to artists updates",
10 artist_updates_path(@song.artist) %>
11 <% end %>
12</div>
13
14...
If we look at the example above as an isolated presentational logic, it doesn't look too bad, right? But, if there are more of these conditional renders, then the code becomes less readable. It also increases the chances of something, somewhere not getting rendered properly, especially if there are more conditions.
One way to fight these is to extract them to a separate helper. Luckily, Rails
provides us a way to easily write custom helpers. In the app/helpers
we can create a SongsHelper
, like so:
1module SongsHelper
2 def song_cta_link
3 content_tag(:div, class: 'song-cta') do
4 if @song.download_url
5 link_to "Download", @song.download_url
6 else
7 link_to "Subscribe to artists updates",
8 artist_updates_path(@song.artist)
9 end
10 end
11 end
12end
If we open up the show page of a song, we will still get the same results.
However, we can make this example a bit better. In the example above, we used an
instance variable @song
. This might not be available if we decide to use this
helper at a place where @song
is nil
. So to cut off an external dependency in the form of an instance variable, we can pass in an argument to the helper like so:
1module SongsHelper
2 def song_cta_link(song)
3 content_tag(:div, class: 'song-cta') do
4 if song.download_url
5 link_to "Download", song.download_url
6 else
7 link_to "Subscribe to artists updates",
8 artist_updates_path(song.artist)
9 end
10 end
11 end
12end
Then, in the view, we can call the helper like below:
1app/views/songs/show.html.erb
2
3...
4
5<%= song_cta_link(@song) %>
6
7...
With that, we should get the same results in the view as we did before. The good thing about using helpers is that you can write tests for them ensuring that no regression happens regarding them in the future. A con is that they are globally defined and you have to ensure that helper names are unique across your app.
If you are not a big fan of writing Rails custom helpers, you can always opt-in for a View Model pattern with the Draper gem. Or you can roll your own View Model pattern here, it shouldn't be that complicated. If you are just starting out with your web app, I suggest starting slowly by writing custom helpers and if that brings pain, turn to other solutions.
DRY up Your Views
What I really liked when I started with Rails was the ability to easily DRY up your markup that it was almost unbelievable to me. Rails gives you the ability to create partials — reusable code pieces that you can include anywhere. For example, if you are rendering songs in multiple places, and you have the same code across multiple files, it makes sense to create a song partial.
Let's say you show your song as shown below:
1# app/views/songs/show.html.erb
2
3<p id="notice"><%= notice %></p>
4
5<p>
6 <strong>Title:</strong>
7 <%= @song.title %>
8</p>
9
10<p>
11 <strong>Description:</strong>
12 <%= @song.description %>
13</p>
14
15<%= song_cta_link %>
16
17<%= link_to 'Edit', edit_song_path(@song) %> |
18<%= link_to 'Back', songs_path %>
But, you also want to show it on another page with the same markup. Then you can
create a new file with an underscore prefix like app/views/songs/_song.html.erb
.
1# app/views/songs/_song.html.erb
2
3<p>
4 <strong>Title:</strong>
5 <%= @song.title %>
6</p>
7
8<p>
9 <strong>Description:</strong>
10 <%= @song.description %>
11</p>
12
13<%= song_cta_link(@song) %>
And then wherever you want to include the song partial, you just do the following:
1...
2
3<%= render "song" %>
4
5...
Rails will do an auto-lookup of whether the _song
partial exists and it will
render it. Similar to an example with custom helpers, it is best if we get rid of the instance variable @song
in our partial.
1
2# app/views/songs/_song.html.erb
3<p>
4 <strong>Title:</strong>
5 <%= song.title %>
6</p>
7
8<p>
9 <strong>Description:</strong>
10 <%= song.description %>
11</p>
12
13<%= song_cta_link(song) %>
Then, we will need to pass in the song variable to the partial, making it more reusable and suitable to being included in other places.
1...
2
3<%= render "song", song: @song %>
4
5...
Final Thoughts
That's all folks for this post. To summarize, we went through a few patterns and anti-patterns that you can come across in the Rails View realm. Here are a few takeaways:
- Avoid complex logic in the UI (do not make the View do lots of powerlifting)
- Learn what Rails gives you out-of-the-box in terms of View helpers.
- Structure and reuse your code with custom helpers and partials
- Do not depend on instance variables too much.
In the next post, we will cover Rails Controller patterns and anti-patterns where things can get pretty messy. Stay tuned for that.
Until the next one, cheers!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!