View Helpers

Exercises

Objective

Next we want to format how some of the movie information is displayed on the movie listing page:

  • The movie description should be truncated to 40 characters.

  • When the total gross of a movie is less than $225M, it should show the word "Flop!" in place of the total gross amount.

  • The movie release date should be presented in a friendly format.

These formatting requirements are all presentation (view-level) concerns, so all of our work will be done in the view layer, with one exception we'll discuss when we get there.

Now, there's a slippery slope when beginning to format view content. It's tempting to start sprinkling little bits of Ruby logic amongst the HTML tags in view templates. It's just so darn easy to do! The problem is that it gets messy really quickly, and once you start down that slope the mess tends to spread (the copy/paste way) to other templates. Before long the templates are a rat's nest and common logic is duplicated across multiple templates. Trust us, we've been there and it's not a pretty place.

To avoid those problems, we'll use view helpers. A view helper is simply a Ruby method that a view calls when it needs help generating output. The helper typically runs view-related logic and, depending on the result, returns the appropriate text that then gets rendered into HTML.

1. Use a Built-In View Helper

Rails is chock full of built-in helpers for things that are common across all web apps: formatting numbers, manipulating text, generating hyperlinks and form elements, and so on. So we might as well start by using what we get for free...

Use a built-in helper method to shorten the movie description to 40 characters. As a bonus, make sure the shortened description breaks on a space rather than chopping off a word.

Show Answer

Hide Answer

<%= truncate(movie.description, length: 40, separator: ' ') %>

Make sure to refresh and check your work before moving on!

2. Write a Custom View Helper

Next, when the total gross of a movie is less than $225M, we'll declare it a flop. Flop movies in our application don't even deserve to have their total gross displayed. Instead, we want to display the word "Flop!".

Rails doesn't have a helper method that handles flop movies—it's not that smart. But this can't be difficult. All we need is a Ruby conditional that generates different text depending on the value of the total_gross attribute. And where should we put that snippet of view-related logic? In a custom view helper, of course.

  1. To get your bearings, remember that we're already using the built-in number_to_currency helper to format the total gross as currency. Find this line in your index.html.erb template:

    <%= number_to_currency(movie.total_gross, precision: 0) %>
    

    That'll work, but only in the case where the movie isn't a flop.

  2. Let's start with some wishful thinking, working from the outside in. Suppose we want our custom view helper to be called total_gross and take a movie object as its parameter. Go ahead and replace the number_to_currency line with the following:

    <%= total_gross(movie) %>
    

    There, that neatly encapsulates what we want.

  3. Refresh the index page and you should get the following error:

    undefined method `total_gross' for #<#<Class:0x007fd0e3a3ece0>:0x007fd0e3b12ae0>

    No surprise. We haven't defined the total_gross method yet.

  4. So where exactly should we define the total_gross helper? Unfortunately, the error doesn't give us a clue, but the fact that our project has an app/helpers directory is conspicuous. And when we generated the MoviesController, Rails took the liberty of creating a file named movies_helper.rb. Open that file and you'll see that it contains an empty Ruby module:

    module MoviesHelper
    end
    

    Ruby modules have a number of uses, but in this case you can think of the module as a bucket of methods. Any helper methods defined in this module will be accessible by any view. The module basically serves as an organizational aid. And it's a good idea to group related helpers into separate modules. As our total_gross helper is related to displaying movies, this module seems like a reasonable home.

  5. Let's take another incremental step toward our goal just to get things working. Start by implementing the total_gross method so that it simply returns the result of calling the built-in number_to_currency method (yup, helpers can call other helpers).

    Remember that the total_gross method takes a movie object as a parameter. You'll need to pass the movie's total_gross value to the number_to_currency helper.

    module MoviesHelper
      def total_gross(movie)
        number_to_currency(movie.total_gross, precision: 0)
      end
    end
    
  6. Refresh the movie listing page and the error should go away. Each movie's total gross should be displayed as currency, just as it was before. That's because we don't have any movies that are flops. But now we know that our custom helper method is being called without errors.

  7. Jump into a console and either change one of the movies so that it has a total gross less than $225M, or create a new flop movie.

    Show Answer

    Hide Answer

    >> movie = Movie.new
    >> movie.title = "Fantastic Four"
    >> movie.rating = "PG-13"
    >> movie.total_gross = 168_257_860
    >> movie.description = "Four young outsiders teleport to an alternate and dangerous universe"
    >> movie.released_on = "2015-08-07"
    >> movie.save
    

    Then refresh the movie listing and you should see the flop movie's total gross. Oh, the shame. We just can't allow that!

  8. Next, change the total_gross helper to use a conditional. If the total_gross is less than $225M, return the string "Flop!". Otherwise return the total gross amount formatted as currency.

    Show Answer

    Hide Answer

    module MoviesHelper
      def total_gross(movie)
        if movie.total_gross < 225_000_000
          "Flop!"
        else
          number_to_currency(movie.total_gross, precision: 0)
        end
      end
    end
    
  9. Refresh and this time the flop movie should stick out like a sore thumb. And to think, $225M used to go a long way, even in Hollywood.

  10. Before you think about crossing this task off the to-do list, here comes the part that a lot of developers unfortunately skip. In the helper, we added this innocent little comparison expression:

    movie.total_gross < 225_000_000
    

    That single expression is the definition of what it means for a movie to be a flop in our application. That's not really a view-level concern, is it? You can imagine other areas of our app wanting to know if a movie is a flop or not. And, if you really stretch your imagination, you can envision a time when some business person decides to change that definition to be less than $300M, for example. So this is actually business logic, and we need to encapsulate it in one definitive place in our app: the Movie model.

    Define an instance method in the Movie class called flop?. (By convention, Ruby methods that end in a question mark (?) return true or false.) Implement the flop? method so that it returns true if the movie's total gross is blank or less than $225M. Otherwise the method should return false.

    Show Answer

    Hide Answer

    class Movie < ApplicationRecord
      def flop?
        total_gross.blank? || total_gross < 225_000_000
      end
    end
    

    Then change the total_gross helper to call the flop? method to make the decision about whether the movie is a flop or not.

    Show Answer

    Hide Answer

    module MoviesHelper
      def total_gross(movie)
        if movie.flop?
          "Flop!"
        else
          number_to_currency(movie.total_gross, precision: 0)
        end
      end
    end
    

    Refresh the movie listing to make sure everything still works as you'd expect.

  11. So we shuffled code around a little, pushing logic that was in the helper back to the model. Was it worth it? Absolutely! One of the benefits of creating the flop? method in the Movie model is you can now call that method from anywhere in your app, or even from the console. Suppose, for example, you wanted to know whether "Iron Man" was a flop. How would you do that in the console?

    Show Answer

    Hide Answer

    >> reload!
    
    >> movie = Movie.find_by(title: "Iron Man")
    >> movie.flop?
    

    Having your business logic totally decoupled from the web like this is not only really handy, it's also one of the secrets of building flexible applications.

That's all there is to writing custom view helpers!

3. Format the Release Date

Finally, as a nice touch we'd like to change the format of the movie's release date. Instead of showing the year, month, and day ("2008-05-02" for example) we just want to show the year the movie was released ("2008"). And for practice, we want to encapsulate that formatting in a reusable view helper method.

  1. Write a custom view helper method named year_of that takes a movie object as its parameter and returns the year the movie was released. In the video we didn't show the strftime directive for formatting the year, but you can probably guess it.

    Show Answer

    Hide Answer

    module MoviesHelper
      def year_of(movie)
        movie.released_on.strftime("%Y")
      end
    end
    
  2. Then use that helper method in the index.html.erb template to display the year the movie was released.

    Show Answer

    Hide Answer

    <%= year_of(movie) %>
    
  3. Once you have that working, you might be delighted to learn that Rails adds a year method to all Date objects. And the released_on attribute is a Date object, so you can ask for the year. Give it a try!

    Show Answer

    Hide Answer

    module MoviesHelper
      def year_of(movie)
        movie.released_on.year
      end
    end
    

And that completes all our formatting tasks!

Solution

The full solution for this exercise is in the helpers directory of the code bundle.

Bonus Round

Trying Helpers in the Console

It's often handy to experiment with view helpers in a Rails console session. To do that, you need to use the special helper object. For example, here's how to try the number_to_currency view helper from inside a Rails console session:

>> helper.number_to_currency(1234567890.50)
=> "$1,234,567,890.50"

Notice you call the helper method on the helper object, which you don't need to do when calling a helper method inside of a view template.

And of course in the console you have access to your models, so you could find a movie and format its total gross as currency, like so:

>> movie = Movie.find_by(title: "Iron Man")
>> helper.number_to_currency(movie.total_gross, precision: 0)
=> "$585,366,247"

Another useful helper method is documentation for the pluralize. You can copy the examples straight from the documentation and try them out in the console:

>> helper.pluralize(1, 'person')
=> "1 person"

>> helper.pluralize(2, 'person')
=> "2 people"

Then once you have the hang of how a helper works, you can confidently use it in a view template. Just remember that you don't use the helper object when inside of a view template. The helper object only exists in the console.

Wrap Up

If you take away one lesson from this exercise, let it be this: Strive to always keep your view templates as clean and concise as possible. The Ruby code should be kept to a minimum, basically just ERb tags for outputting values and iterating through collections. If it's more complicated than that, it's time to either use a built-in view helper or write a custom helper. Later on we'll talk about how to decompose view into partials, which is another design technique for eliminating duplication and generally keeping views maintainable over time.

But first, speaking of maintainable views, in the next section we'll learn how to use layouts to give the app a consistent look and feel.

Dive Deeper

Before writing your own custom helper, it's wise to spend a few minutes getting familiar with what Rails gives you for free. Check out the helper methods in these modules:

The point isn't to challenge yourself to use every single one of these in an app. Rather, it's just good to know what's available should you need it.

You can also search for a helper method in the Rails API documentation.