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.
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
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_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.
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
1<%= link_to "Songs", songs_path %>
Which will generate the following HTML:
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.
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
@song. This might not be available if we decide to use this
helper at a place where
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
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...
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!