Growing Devs

Jan 29, 2014

Refactoring Rails Views with SimplestView

by Tony Pitale

Most of the projects I've worked with grow in similar ways. Code often ends up where it is easiest to place it. For code in our Rails "Views", under app/views, that's either in the controller (with lots of instance variables assigned), in ApplicationHelper (shared with every view), or in the view templates themselves.

I'm going to try to address each of these scenarios, and show you how to refactor them into much simpler code, using SimplestView. But first, a little background.

The Rails Way

In Rails, our ERB templates go under app/views. There is a logical structure to how they are organized. This is The Rails Way. By convention, the views for a controller SalesController goes inside the folder app/views/sales. Further, each action gets a file inside the sales folder, using the name of the action as the filename.

The tricky bit is these Rails "Views" aren't really view objects. The ERB templates we build, are rendered inside the context of an instance of an anonymous class, which inherits it's rendering abilities from ActionView::Base.

But why an anonymous class? Why does Rails hide this away from us completely?

With SimplestView

Now, if we set up our Rails application to use SimplestView, we get clear, conventional access to these previously hidden view classes. We give them a name. By convention, that name will match the action, and be placed inside a folder matching the controller name.

For our SalesController's #index and #show we would have classes named Sales::IndexView and Sales::ShowView (which inherit from ActionView::Base) inside of a folder in app/views/sales. Note: SimplestView has you move your ERB into a more aptly named app/templates folder to keep things cleanly separated, and to give you a nice Rails-y place to put your new views.

Now that we have that out of the way, let's get into some refactorings!

ApplicationHelper Overload

How often have you had to dig through an ApplicationHelper that simply contains every single helper in the whole project? I've seen projects with an ApplicationHelper thousands of lines long.

Let's look at a specific method that we can refactor, and get it out of there!

def format_tax_rate(value)
  '%.2f%' % (value.to_f * 100)
end

And it might be used in our ERB like so:

<%= format_tax_rate(@sale.state_sales_tax) %>

This is some very simple code. It's a naive approach to formatting the decimal value as a percentage to two decimal places.

More importantly, it's only really needed in one or two views to display a formatted sales tax rate. Why then, should it clutter up our ApplicationHelper in order to be shared? And what happens when you have 10, 20, or more small methods just like this one?

With SimplestView, we can move the method to a better place. If we were to handle this in our Sales::IndexView, for example, we could do something like this:

class Sales::IndexView < ActionView::Base
  def formatted_state_sales_tax
    '%.2f%' % (@sale.state_sales_tax.to_f * 100)
  end
end

So, what did we do here?

  1. We made our view match the conventions. For the SalesController#index controller and action, we'll use Sales::IndexView as the context for the template now found in app/templates/sales/index.html.erb.
  2. We made a nicely named method which contains just the formatting we need.
  3. We use the instance method for @sale, which we assigned in our controller, and is accessible to us inside of the view.

What are the benefits? Well, we get nicer encapsulation. We don't have to expose the instance variable inside of the ERB for this particular case

Hello OO!

After this section, I want to make a quick aside to point out something important. With SimplestView, we're back to using Ruby as the Object-Oriented language that it is! If we need to share code, we make a module to mixin. Or, if it makes sense, we can use inheritence.

If we have another method formatted_county_sales_tax, we can extract a method to do the formatting. Then, if that new formatting method is used in multiple views, it's as easy as extracting a module like SalesTaxFormattable.

In addition, we can now easily test, and refactor this view because it's just a Ruby class.

Controller Assignment

One of the more troubling ways I've seen developers cope with growing complexity in their ERB templates has been to move calculations and logic BACK into the controller.

What started out as an action that assigns a single instance variable, baloons into methods full of before_action (previously before_filter) calls to assign and calculate many instance variables.

Let's refactor an example:

class SalesController < ApplicationController
  def show
    @sale = Sale.find(params[:id])
    @purchase_count = @sale.purchases.count
  end
end

Now, we can output both data about our @sale, but we can also output our @purchase_count number, too.

Instead, let's do it with SimplestView.

class Sales::ShowView < ActionView::Base
  def purchase_count
    @purchase_count ||= @sale.purchases.count
  end
end

Now, we can still output our purchase_count in our template, without cluttering our controller!

ERB Templates

By now, it may be clear what I'm going to say. But, I'll say it anyway.

Extraneous logic in your ERB templates is very likely to be a code smell. Extensive branching logic, local variable assignment, etc. Time to get it out!

Inside of our SalesController#show action, we'll likely render the template found in show.html.erb. It might have some code like:

<% if @sale.starts_on <= Date.today && @sale.ends_on > Date.today %>
  On Sale!
<% else %>
  Sale Starts On: <%= @sale.starts_on.strftime("%B %d, %Y") %>
<% end %>

There are two things in this code that we really don't need. Both the if conditional and the date formatting code can be encapsulated in our Sales::ShowView.

class Sales::ShowView < ActionView::Base
  def active_sale?
    @sale.starts_on <= Date.today && @sale.ends_on > Date.today
  end

  def formatted_start_date
    @sale.starts_on.strftime("%B %d, %Y")
  end
end

This is probably my favorite refactoring. And, it is likely to be the one you will use on a regular basis.

But Why?

The benefits of these changes might not be the biggest pain you feel in your code, day to day. But together, all of the little changes you get to make, the added ease of refactoring, and the extra convention, make it a whole lot easier to write well-factored, maintainable code, every day.

I'm using SimplestView daily, in as many of my projects as possible. Not because the code I already have is horrendous, and not because there are no alternatives to refactor and clean up my code. I'm using it because it enables functionality in Rails that should be available already, and it gives me the conventions I kept searching for.

If SimplestView doesn't strike your fancy, you can go the decorator route with the fantastic Draper gem. Both Draper and SimplestView accomplish very similar things. Regardless, you should try using one of them, and see how it helps you clean up your code.