How Decorators Saved My Rails App
By: Ryan Francis / April 6, 2016
Introduction
The Rails app I am building is in the finance sector and as a result has lots of currencies and percentages. The challenge is that the database stores these values as floats (i.e. decimals) and as a result I need to convert these raw values into something presentable on the frontend.
For example, a currency value in the database of 50.12 becomes $50 when presented to the user in the HTML. Similarly, the percentage value of 0.0508 becomes 5.1% when presented to the user on the frontend.
The Wrong Solution
In my days as a novice, I would perform this conversion right in the view like so:
app/views/companies/show.html.erb
<ul> <li>Budget: <%= number_to_currency(@company.budget, precision: 0) %></li> <li>Actual: <%= number_to_currency(@company.actual, precision: 0) %></li> <li>Percent Spent: <%= number_to_percentage(@company.percent_budget_spent * 100, precision: 1) %></li> </ul>
Which produces:
Budget: $12,000 Actual: $11,485 Percent Spent: 5.1%
While this solves the problem, it is a poor solution. Anytime I want to display any of these three values on the page, I’ll need to duplicate the formatting logic, including each data’s:
- Format type (currency or percentage)
- Precision (number of decimals)
- Transformation Logic (percentages need to be multiplied by 100)
When my client calls up and says “Let’s display all percentages in the application as 2 decimal places” (by the way this actually happened), I would need to find all the places in my app using the number_to_percentage helper method and adjust the precision.
A Better Solution: Decorators
Code With Me
If you’re like me, learning is best accomplished through actually writing code. I’ve setup a basic Rails app for you to code alongside me. Just fork it and clone it down: Decorator App
Then in terminal run:
bundle bundle exec rake db:create db:migrate
Draper
Nowadays I use a gem called Draper so that I don’t have to put these helper methods all throughout my views. Here’s how it works:
Gemfile:
gem 'draper'
In terminal:
bundle rails g decorator Company
This generates the company decorator file, which we update to include budget, actual, and percent_budget_spent methods:
# app/decorators/company_decorator.rb class CompanyDecorator < Draper::Decorator delegate_all def budget h.number_to_currency(object.budget, precision: 0) end def actual h.number_to_currency(object.actual, precision: 0) end def percent_budget_spent h.number_to_percentage(object.percent_budget_spent * 100, precision: 1) end end
The last step is to “decorate” our ActiveRecord object. Find companies_helper.rb and change it like so:
module CompaniesHelper def company @decorated_company ||= @company.decorate end end
Now instead of referencing @company in our views, we simply call company to get the decorated company.
Change app/views/companies/show.html.erb to look like:
<ul> <li>Budget: <%= company.budget %></li> <li>Actual: <%= company.actual %></li> <li>Percent Spent: <%= company.percent_budget_spent %></li> </ul>
Which turns into…
Budget: $12,000 Actual: $11,485 Percent Spent: 5.1%
Voila! This is a much cleaner and scalable approach. If we ever need to use budget, actual, or percent_budget_spent elsewhere on the page, we can do so without duplicating the helper method.
However, we run into trouble when we go to create our first form…
Forms Trouble
<%= simple_form_for company do |f| %> <%= f.input :name %> <%= f.input :budget, as: :string %> <%= f.input :actual, as: :string %> <%= f.submit %> <% end %>
I want the budget field to look like this:
But it actually looks like this…
Do you see the dollar sign sitting inside the text field? This will pose a problem when we submit the form, as Rails will try to convert $15,000 to a float which results in nil. So why is it there?
Simple Form, like form_for, looks for a method on our object called budget. Since company is actually a CompanyDecorator object, it calls the budget method that was defined there first (if it doesn’t find that method on the decorator, then it goes on to call the method on the ActiveRecord object).
Remember, we did define a budget method on the decorator:
def budget h.number_to_currency(object.budget, precision: 0) end
So our form is calling this method which is returning a currency formatted string. Unfortunately, this is not what we want.
As it turns out, we don’t always want to format budget as a currency. In this case of the form field, we want to format it as a decimal.
My solution to this is to use two decorator methods for each column that needs formatted. So for budget we have a budget method that returns a formatted decimal and a display_budget method that returns a formatted currency.
# app/decorators/company_decorator.rb class CompanyDecorator < Draper::Decorator delegate_all def budget h.number_with_precision(object.budget, precision: 2) end def display_budget h.number_to_currency(object.budget, precision: 0) end def actual h.number_with_precision(object.actual, precision: 2) end def display_actual h.number_to_currency(object.actual, precision: 0) end def display_percent_budget_spent h.number_to_percentage(object.percent_budget_spent, precision: 1) end def percent_budget_spent h.number_with_precision(object.percent_budget_spent, precision: 1) end end
This allows me to use the following methods and receive the following corresponding outputs:
<%= company.budget %> => 1200.00 <%= company.display_budget %> => $1,200 <%= company.actual %> => 1000.00 <%= company.display_actual %> => $1,000 <%= company.percent_budget_spent %> => 5.1 <%= company.display_percent_budget_spent %> => 5.1%
Simple Form will use the budget method, and will therefore be formatted with the number_with_precision helper method. Everywhere else in our views we can use company.display_budget to render out the currency value.
Now we update app/views/companies/show.html.erb:
<ul> <li>Budget: <%= company.display_budget %></li> <li>Actual: <%= company.display_actual %></li> <li>Percent Spent: <%= company.display_percent_budget_spent %></li> </ul>
Which produces:
Budget: $12,000 Actual: $11,485 Percent Spent: 5.1%
While I find this to be quite effective, it is cumbersome to write two decorator methods for every currency or percentage field in our application.
The Best Solution: Base Decorator
Often times the way we format data like currencies and percentages is consistent across the application. Because of this, it would be great to be able to write our decorator like this:
class CompanyDecorator < BaseDecorator delegate_all currency :budget, :actual percent :percent_budget_spent end
Which should expose the following methods:
<%= company.budget %> => 1200.00 <%= company.display_budget %> => $1,200 <%= company.actual %> => 1000.00 <%= company.display_actual %> => $1,000 <%= company.percent_budget_spent %> => 5.1 <%= company.display_actual %> => 5.1%
Looks pretty cool, you say. But how?
Step 1: Create the Base Decorator
Create a Base Decorator file that all of our decorators will inherit from:
# app/decorators/base_decorator.rb class BaseDecorator < Draper::Decorator delegate_all end
And inherit from it…
# app/decorators/company_decorator.rb class CompanyDecorator < BaseDecorator delegate_all end
Step 2: Create the Currency Method
The goal is for the following:
class CompanyDecorator < BaseDecorator delegate_all currency :budget end
To end up producing something like:
class CompanyDecorator < BaseDecorator delegate_all def budget h.number_with_precision(object.budget, precision: 2) end def display_budget h.number_to_currency(object.budget, precision: 0) end end
With that in mind, let’s take a stab at implementing some meta programming to solve this.
Start by defining a class method currency on our BaseDecorator:
class BaseDecorator < Draper::Decorator delegate_all def self.currency(*keys) end end
*keys will end up being an array of keys:
[:budget, :actual]
So what we want to do is iterate over each of these keys and define two methods, the first which is just the name of the key and returns a decimal format, and the second which prepends display_ to the name of the key and is a currency format:
class BaseDecorator < Draper::Decorator delegate_all def self.currency(*keys) keys.each do |key| define_method(key) do end define_method("display_#{key}") do end end end end
In the first define_method block, we want to implement something like:
h.number_with_precision(object.budget, precision: 2)
While the second should look like:
h.number_to_currency(object.budget, precision: 0)
However, since the key is a variable, we’ll need to use the send method:
class BaseDecorator < Draper::Decorator delegate_all def self.currency(*keys) keys.each do |key| define_method(key) do h.number_with_precision(object.send(key), precision: 2) end define_method("display_#{key}") do h.number_to_currency(object.send(key), precision: 0) end end end end
Now in our CompanyDecorator, let’s call the newly added currency method:
class CompanyDecorator < BaseDecorator delegate_all currency :budget, :actual end
With that, the following methods produce these results:
<%= company.budget %> => 1200.00 <%= company.display_budget %> => $1,200 <%= company.actual %> => 1000.00 <%= company.display_actual %> => $1,000
Perfect.
Now only one more step: the percent method.
Step 3: Implement the Percent Method
I encourage you to try to implement this method on your own. The goal is that you come away with a pattern you can use in your Rails application. Likely your formatting rules and preferences will be different, so understanding how to create these Base Decorator methods is important.
In case you get stuck, here is the solution I came up with, which of course goes into the BaseDecorator file:
def self.percent(*keys) keys.each do |key| define_method(key) do h.number_with_precision(object.send(key) * 100.0, precision: 1) end define_method("display_#{key}") do h.number_to_percentage(object.send(column_name) * 100.0, precision: 1) end end end
Ready to Build Something Great?
Partner with us to develop technology to grow your business.