My First Ruby on Rails Presenter

Max Antonucci - Nov 21 '17 - - Dev Community

As part of my ongoing career quest to become full-stack, there's several tasks I must complete, such as:

  • Storm the castle
  • Slay the dragon
  • Save the princess
  • Learn Ruby on Rails

Thankfully I can go in any order, so I started with Rails!

After finishing the always helpful Ruby on Rails Tutorial, I looked to learn topics it didn't cover. I'd actually touched on one before in my job - the Presenter pattern! Creating and designing my first presenter was a surprising challenge, so I wanted to share my process here for others. Sadly the tutorial on saving the princess will have to wait.

I assume anyone reading this has at least a basic knowledge of Rails tools and functionality, so I'll skip recapping controllers, routes, and all that. If all that's unfamiliar to you, I recommend going through the Rails tutorial first!

What is a Presenter?

A Presenter is an extra layer between a controller and view that better organizes the data being used. Chances are the raw data from your app's database won't just be used as-is:

  • Some numbers may need corresponding string or hash values
  • Large groups of data may need reorganization or recalculation
  • All the above may depend on other info or require extra arguments

Without a presenter, there's two other ways to address this issue that aren't as good.

  1. Do it all in controllers, but that's not what controllers are for. They control the data, layout, and other actions for rendering the page. Making the data fit for the view falls is another matter, and makes controllers bulky and hard to maintain.
  2. Do it all with helpers, but similar issues arise. Helpers will get overly-long and complex, and become harder to organize among other helpers with more general-purpose tasks (otherwise called "helper hell").

The common, and most important, issue with both these answers is it mixes too many responsibilities together. A presenter fills the role of restructuring data into what the page needs. That's an important, specific task purpose separate from the other two.

This specific presenter was for a Rails project I'm making for managing a budget. It lets users keep track of expenses (what they spend on) and incomes (what they earn money on). There's several different categories for their budget, and a category contains certain types of expenses or incomes. I made this presenter for when users wanted to see important info from a certain category.

With the background set, let's make the actual presenter!

Setting up the Basics

My first obstacle was presenters aren't like controllers, routes, or views - no Rails magic was included. That gives setup a few extra steps.

First is set up a base presenter, for any functionality I want to be shared among them all. Mine just had one function so all presenters could use the application's helpers (with an h prefix).

class BasePresenter

  private

  def h
    ApplicationController.helpers
  end
end
Enter fullscreen mode Exit fullscreen mode

Now to set up my category presenter to inherit from this one.

class CategoriesPresenter < BasePresenter

end
Enter fullscreen mode Exit fullscreen mode

The lack of rails magic also meant I had to explicitly define the category properties for the presenter. I also had to initialize them with other variables it needed - here they were the start and end date, as users would view this info by month.

class CategoriesPresenter < BasePresenter

delegate :category,
         :name, 
         :description, 
         :expense, 
         :expenses, 
         :length,
         :budget, to: :@category

  def initialize(category = nil, start_date = nil, end_date = nil)
    @category = category
    @start_date = start_date
    @end_date = end_date
  end
end
Enter fullscreen mode Exit fullscreen mode

With the setup done, I can access to all category info from the database. Now I can start designing data for the view!

Defining Normal Presenter Values

...Unfortunately, the fun parts will have to wait. Just because all the category values are passed in doesn't mean I can use them - they need to be explicitly defined as functions.

This isn't tough, it's just an extra step that's somewhat tedious:

  def id
    @category.id.to_i
  end

  def created_at
    @category.created_at
  end

  def budget
    @category.budget
  end
Enter fullscreen mode Exit fullscreen mode

Values I want to use as-they-are just need to defined in the presenter like so.

But even simple functions can be made more useful in a presenter. For example, while my data has all the categories' expenses, it doesn't include their total value. I have a helper to calculate this (since it's a common calculation), so I can use that.

  def total
    h.category_total(expenses)
  end
Enter fullscreen mode Exit fullscreen mode

Plus the data has all the expenses, but I only want to find ones within the start and end date. So I redefine the expenses to get those within that range and reorganize them by value.

  def expenses
    expenses = @category.expenses.select { |expense| expense.created_at.between?(@start_date, @end_date) }.sort_by &:value
    expenses.reverse
  end

Enter fullscreen mode Exit fullscreen mode

Another piece to define: despite each category having "expenses," some may actually be set as a source of income. That's why the "expense" value from the database is a boolean, marking it as an expense or income. The presenter translates that boolean to the needed label.

Plus the view often refers to "expenses" or "incomes" in the plural, so that can be defined here too for convenience.

  def type
    if @category.expense
      "Expense"
    else
      "Income"
    end
  end

  def type_p
    type.pluralize(type)
  end
Enter fullscreen mode Exit fullscreen mode

Defining more Interesting Presenter Values

With the easier stuff done, let's get to the more complex presenter functions.

The first two define important links for the category: info about the category on that month, and all the categories on that month. These make it easier to link around pages in the time frame. They require certain info from the date objects being interpolated into a url.

  def date_link
    year = @start_date.strftime("%Y")
    month = @start_date.strftime("%-m")

    "#{year}/#{month}/#{@category.id}"
  end

  def month_link
    year = @start_date.strftime("%Y")
    month = @start_date.strftime("%-m")

    "/categories/#{year}/#{month}/"
  end
Enter fullscreen mode Exit fullscreen mode

Looking back, since they need the same variables for the setup, I'd put them together and output a hash.

  def link
    year = @start_date.strftime("%Y")
    month = @start_date.strftime("%-m")

    {
      :date => "#{year}/#{month}/#{@category.id}",
      :month => "/categories/#{year}/#{month}/"
    }
  end
Enter fullscreen mode Exit fullscreen mode

If not this, then have the year and month defined elsewhere and referenced in separate functions. I'm still learning so I'm unsure, but in the future I may output fewer hashes for simplicity.

The last function I wrote also outputs a hash for the category's balance, or how far off total expenses or incomes are from what was expected. This is calculated differently for expenses and incomes - it's better for expenses to be below the budget, and for incomes to go above (earning more than expected).

This function returns both the numerical balance, and uses a helper to return a related string if it's positive or negative. I combined these in one function's hash since I judged they were close enough to group together.

  def balance
    if @category.expense
      total = @category.budget - h.category_total(expenses)
    else
      total = h.category_total(expenses) - @category.budget
    end

    {
      :total => total,
      :state => h.check_state(total)
    }
  end
Enter fullscreen mode Exit fullscreen mode

With that, my presenter was complete!

In Conclusion

I plan to keep using presenters as I learn more Rails, due to the important need they fill. They add needed functionality without it leaking into unwanted areas (like my controllers or helpers), and keep my views closer to "plug variables in the right place" templates. Coding this app without them made it descend into the "helper hell" of having too many complex helpers to keep track of. Had I not added a presenter, I may have stopped out of simple frustration or confusion.

Here's hoping Rails makes the presenter pattern an official part of the framework, so they're less of a hassle. Personally, I almost can't imagine using Rails without them.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .