5 Ways to Deal With Messy Rails Views

Your views are getting out of hand. It's impossible for another developer (or you in 6 months) to understand what's going on in them. In this article, I'll introduce you to 5 strategies for tidying up your views and provide resources to dig in further.

1. Partials

Partials are built into Rails and are used to group related chunks of markup into logical files that can be reused throughout your application.

A typical partial might look like this:

# app/views/employees/new.html.erb
<h1>New Employee</h1>  
<%= render 'form' %>  
<%= link_to 'Back', employees_path %>  

This render statement would reference a file in the same director named _form.html.erb. Note the underscore.

If you had an employee index page that iterated over a collection of @employees, you can use the following shorthand:

<%= render @employees %>  

This would look for a partial named _employee and use it to render each employee in the @employees collection.

Resources

2. Decorators

The decorator pattern is used to encapsulate presentation logic that doesn't belong in the model, nor the view.

For an example of what presentation logic in the views looks like, let's say we have a blogging application that shows the publication date of an article (in the format "May 25th, 2015"), or "Draft" if it's not yet published.

Done entirely in the view, it would look like this:

<article>  
  <span class="publication-status">
    <% if @article.published? %>
      Published at: <%= @article.published_at.strfitme("%B #{@article.published_at.day.ordinalize}, %Y")
    <% else %>
      Draft
    <% end %>
  </span>
...
</article>  

As more of this presentation logic piles up, it becomes impossible to see the actual structure of the markup in the view.

Out of the box, Rails gives up View Helpers as an outlet for presentation-only logic, but these helpers are lacking in a few key areas:

  • Helpers are included into all views, so you have to worry about naming conflicts and unintended behavior.
  • Helpers are modules, so they don't have access to an object. This means you are constantly passing objects into redundantly named methods like article_published_at_date(article).

Draper is a popular gem that provides a well-defined decorator pattern that extends an ActiveRecord object with presentation logic without polluting the model.

Let's see what Draper can do for our tortured article view.

class ArticleDecorator < Draper::Decorator  
  delegates_all

  def publication_status
    if is_published?
      "Published at: #{published_at}"
    else
      "Draft"
    end
  end

  def published_at
    object.published_at.strfitme("%B #{published_day}, %Y")
  end

  private

  def published_day
    object.published_at.day.ordinalize
  end
end  

In order to apply this decorator to our article, we'll add the following to the ArticlesController:

class ArticlesController < ApplicationController  
  def show
    @article.find(params[:id]).decorate
  end
end  

The decorate method instantiates a new ArticleDecorator object with the found Article.

Now, we can write the article view much more tersely:

<article>  
  <span class="publication-status">
    <%= @article.publication_status %>
  </span>
...
</article>  

There's no more unnecessary logic. We can glance at this view and understand what's going on and how the markup is structured.

Resources

3. Null Object

Use the Null Object pattern to avoid branching conditional logic.

Let's say we have an application that users can view as admins, customers, or guests if they're not logged in. Depending on the user's role, we want to show a different navigation header.

<% if user_signed_in? %>  
  <% if current_user.role == 'admin' %>
    <%= render 'admin_nav' %>
  <% elsif current_user.role == 'customer' %>
    <%= render 'customer_nav' %>
  <% end %>
<% else %>  
  <%= render 'guest_nav' %>
<% end %>  

We're using partials, so that's a good start, but for every new role we add, we'll need to add another branch of logic. We can make this a lot cleaner.

Using a clever bit of refactoring, we can dynamically render a partial based on the user's role.

<% if user_signed_in? %>  
  <%= render "#{current_user.role}_nav" %>
<% else %>  
  <%= render 'guest_nav' %>
<% end %>  

That's great, but we're still left with the null case, where we don't have a current_user to give us a role. This is where the Null Object shines.

The Null Object is a powerful but simple pattern. A Null Object is simply a plain old Ruby object that responds to the same methods as the non-null object. Combined with Ruby's duck typing, this means we can treat our Null Objects the same way we treat our normal objects.

So how can we apply this pattern to our example above?

We'll start with a class, but rather than being lazy and calling it NullUser, we'll name it for what it represents in the domain of our application, a Guest.

class Guest  
  def role
    'guest'
  end
end  

This Guest class responds to .role with 'guest', the same way our User class responds to .role with either 'admin' or 'customer'.

Now, in our ApplicationController where current_user is defined, we'll return an instance of Guest instead of nil when no user is signed in.

class ApplicationController  
  def current_user
    super || Guest.new
  end
end  

This means we can now refactor our navigation partial rendering code to the following:

<%= render "#{current_user.role}_nav" %>  
Resources

4. Form Objects

Form objects are useful for simplifying multi-model forms and other complex form logic. Rather than using accepts_nested_attributes_for and dealing with the fallout of multiple models' validations, form objects allow you to group complex form markup and validation in a single location.

This is an extensive topic, so I'll be brief here and leave lots of resources in case you want to pursue this topic further.

If you had a Survey model that upon creation needed to create multiple Question models in turn, you might use a Form Object to centralize the logic of creating a Survey (oftentimes this ends up in the controller, so a Form Object can be an effective way to slim up fat controllers).

To demonstrate, I'll use Virtus to make an object that quacks like an ActiveRecord model, but there are plenty of other options for creating Form Objects.

class CreateSurvey  
  include Virtus

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attribute :title, String
  attribute :questions, Array[String]

  validates :title, presence: true

  def save
    if valid?
      persist!
      true
    else
      false
    end
  end

  private

  def persist!
    transaction do
      @survey = Survey.create!(title: title)
      @questions = questions.map{|question_text| Question.create(text: question_text)
    end
  end
end  

In the controller, we would handle the form submission using this CreateSurvey object:

SurveysController < ApplicationController  
  def create
    @survey = CreateSurvey.new(params[:survey])

    if @survey.save
      # logic if successful
    else
      # logic if unsuccessful
    end
  end
end  
Resources

5. Alternate Templating Languages

ERB is a perfectly fine templating language, but more advanced languages like Haml and Slim let you express HTML markup interpolated with Ruby code much more clearly.

Let's look at a tiny example to see the differences:

ERB

<section class=”container”>  
  <h1><%= post.title %></h1>
  <h2><%= post.subtitle %></h2>
  <div class=”content”>
    <%= post.content %>
  </div>
</section>  

Haml

%section.container
  %h1= post.title
  %h2= post.subtitle
  .content
    = post.content

Slim

section.container  
  h1= post.title
  h2= post.subtitle
  .content= post.content

Choosing a templating language can be more of a matter of personal preference. If you're comfortable with HTML tags, ERB will get the job done (and provide the most support). If you don't have anything against percent signs, Haml is pretty widely supported. If you're striving for the ultra-clean approach, Slim is a great option.

Resources

What patterns do you use in your views? Let me know in the comments.

Andrew Allen

Author of EfficientRails.com, Software Engineer @Munchery, Former startup founder.

comments powered by Disqus