Creating a Breadcrumb Trail in Rails, Part 2

A breadcrumbs trail is a navigational aid that shows users a hierarchical view of the web pages they’ve traversed. It’s a simple and useful way to orient users within a web site, especially as web sites get deeper and more complex.

This is Part 2 of a series of articles on creating a breadcrumb trail. Part 1 focused on creating the basic code for generating a breadcrumb trail, albeit with a few minor limitations. This article focuses on streamlining the code a little, while packaging it up to be more reusable.

Streamlining the Code

Using the original code from Part 1, the show_breadcrumbs method could be called in a view to generate the HTML for the breadcrumb trail, as shown below:

       <%= show_breadcrumbs(
          ['Home', ['main'], 
           'Tools', ['tools'],
           'Antimatter', ['tools', 'antimatter']],
          :direction => 'left') %>

In the code above, the first argument is an array, where each breadcrumb element is represented by a pair of array entries. The first value is the name of the breadcrumb, e.g. – the name of the link. The second is an array containing the data necessary to construct the link, such as [controller, action] or [url]. In practice, this all turned out be overkill.

As much as possible, most developers nowadays are using a RESTful approach when building Rails web sites. With a RESTful approach, you use map.resourcesin your routes.rb file to define resources, which have well-defined behaviors, e.g. – List, Show, Create, Edit, etc. As a by-product, Rails defines helper methods that can be called in views and controllers to provide the URL for a route. For example, an “articles” resource will possess an articles_url method that represents the List action for instances of that type of resource.

Bottom Line: Most developers are tossing around URLs, rather than controller/action pairs. So, we can simplify the structure that we’ve been using to represent a breadcrumb trail:

       <%= breadcrumbs(
          ['Home', root_url, 
           'Tools', tools_url,
           'Antimatter', antimatter_url],
          :direction => 'left') %>

So, now a breadcrumb trail is represented as an array, with each consecutive pair of elements defining the label and URL, respectively. The caller is responsible for providing the URL, which can be either a relative (“/home/tools/antimatter”) URL or an absolute URL. It can even be an external URL or a URL with parameters.

Furthermore, we’ll rename the show_breadcrumbs method to simply breadcrumbs. This isn’t just a syntax change; the method generates the HTML for a breadcrumbs trail. It doesn’t actually show the breadcrumb trail.

This also facilitates the use of the method in both views and controllers, rather than just views. Within a view, the generated HTML can be displayed, as show in the code above. If used in a controller, the generated HTML can be placed in a variable that will be accessible to the view.

       @trail = breadcrumbs(
          ['Home', root_url, 
           'Tools', tools_url,
           'Antimatter', antimatter_url],
          :direction => 'left')

This approach seems reasonable to me. It provides developers with reusable code for generating the HTML for a breadcrumb trail, but lets the developer determine where the responsibility for generating a breadcrumb trail belongs.

The modified code is shown in Figure 1.

Listing 1 – Modified Breadcrumbs Code

module Breadcrumbs

  def self.included(klass)
    klass.send(:extend, ClassMethods)
    klass.send(:include, InstanceMethods)
  end

  module ClassMethods

  end # ClassMethods

  module InstanceMethods

    # Returns a string containing the HTML necessary to display a 
    # breadcrumb trail. The first arg contains an array of elements,
    # where the first element is the name of a breadcrumb, the 
    # second is an array containing values for assembling a URL
    # (controller, controller & action, or external URL), and so on in
    # alternating fashion. The final arg is a hash containing options,
    # where the only option currently defined is ":direction". This
    # can have values of either "left" or "right", and governs which
    # way the breadcrumbs will be oriented. The default is "right".
    #
    # An example of the method's usage in a view is:
    #
    # <%= breadcrumbs(
    #        ['Home', 'home_url', 
    #         'Tools', 'tools_url',
    #         'Antimatter', 'antimatter_url'], :direction => 'left') %>
  
    def breadcrumbs(crumbs, opts = nil)
      direction = 'right'                        # Default direction
      separator = breadcrumb_separator_right     # Default separator
      if opts != nil
        dir = opts[:direction]
        if dir == 'left'
          direction = dir
          separator = breadcrumb_separator_left
        end
      end
    
      str = ""
      if crumbs.size > 0
        str += '<div id="breadcrumbs">'
        if direction == 'right'
          i = 0
          while i < crumbs.size
            url = crumbs[i + 1]
            str += " #{separator} " if i > 0
            str += build_crumb(crumbs[i], url)
            i += 2
          end
        else # Direction equals left
          i = crumbs.size - 2
          while i >= 0
            url = crumbs[i + 1]
            str += " #{separator} " if i < (crumbs.size - 2)
            str += build_crumb(crumbs[i], url)
            i -= 2
          end
        end

        str += '</div>'
      end
    
      str
    end
    
    # Returns TRUE if the provided value is an external URL and
    # FALSE if it represents the name of a controller. External
    # URL's can be easily distinguished because they 
    # begin with "http://".
  
    def is_external_breadcrumb?(val)
      val.to_s.start_with?('http')
    end
  
    # Returns a string containing the HTML for one breadcrumb 
    # link within a breadcrumb trail. The first argument is the title
    # of the link, while the second is the destination URL for the
    # link.
  
    def build_crumb(title, url)
      str = ""
      if is_external_breadcrumb?(url)
        str += "<a href=\"#{url}\" class=\"bt_external\">#{title}</a>"
      else
        str += "<a href='#{url}'>#{title}</a>"
      end
      str
    end

    # Defines the separator used between breadcrumb elements
    # when the breadcrumbs are traversed from right to left, i.e. - the
    # separator points to the left.
  
    def breadcrumb_separator_left
      "&lt;"
    end
  
    # Defines the separator used between breadcrumb elements
    # when the breadcrumbs are traversed from left to right, i.e. - the
    # separator points to the right. This is the direction in which most
    # breadcrumb trails are oriented.
  
    def breadcrumb_separator_right
      "&gt;"
    end
  
  end # InstanceMethods

end  # Module

Packaging As a Plugin

We’ve made some changes to the breadcrumbs code, but the code is still just a single Ruby source file stored in the lib directory of a Rails application. So, let’s turn it into a plugin.

Run the following command from the command-line:

          $ script/generate plugin breadcrumbs

This will generate the files and directories for a plugin within the vendor/plugins/breadcrumbs directory. The files generated are shown below:

Our code from Listing 1 should be copied into the breadcrumbs.rb file in the lib directory of the plugin. We also need to add some initialization code to the init.rb file:

          require 'breadcrumbs'

          ActionController::Base.send :include, Breadcrumbs
          ActionController::Base.send :helper_method, breadcrumbs

This code ensures that the methods from the plugin are included as instance methods within the ActionController::Base class, where they’ll be accessible to controllers. However, the breadcrumbs method is also registered as a helper method, which means that it will be accessible to views, too.

This is also an improvement over the breadcrumbs code in Part 1 of this article series. The user no longer has to explicitly include the breadcrumbs code. The plugin code is automatically added to the class load path and made available to both controllers and views.

Conclusion

We’ve made some improvements to the breadcrumbs code, and we’ve packaged it up in a more reusable fashion as a Rails plugin. By doing so, we’ve also made the code a little easier for developers to use because the breadcrumbs functionality is automatically accessible to controllers and views.

We’re not done with improvements, though. Plugins have been gently deprecated in the Rails community due to their limitations, of which the primary one is their lack of a versioning capability (something that is, however, provided by gems). In Part 3 of this series, we’ll further enhance the reusability of the breadcrumbs code by converting it into a gem and, later, a gem plugin.

Creating a Breadcrumb Trail in Rails, Part 1

A common feature of many web sites is what’s known as a breadcrumb trail, or sometimes a rabbit trail. A breadcrumb trail is just a listing, generally on one concise line, of the pages that have been traversed in a hierarchy to get to the current page. Each element of the breadcrumb trail is a link to that previous page, and there’s usually some character or image separating the elements of a rabbit trail.

Antimatter < Tools < Home

I decided to create some code in Ruby that would facilitate the creation of breadcrumb trails for Rails web sites. Listing 1 shows the code that I came up with. My goal, of course, was to make this a generic solution that could easily be reused in numerous web sites. 

In this article, I’ll just focus on presenting the code that generates the breadcrumbs. In a follow-up article, I’ll detail some strategies for packaging up this type of feature to facilitate code reuse.

Listing 1: Helper Code for Breadcrumbs – breadcrumbs_trail.rb

module BreadcrumbsTrail

  # Defines the separator used between breadcrumb elements when
  # the breadcrumbs are traversed from right to left, i.e. - the
  # separator points to left.

  def breadcrumb_separator_left
    "<"
  end

  # Defines the separator used between breadcrumb elements when the
  # breadcrumbs are traversed from left to right, i.e. - the separator
  # points to the right. This is the direction in which most
  # breadcrumbs are oriented.

  def breadcrumb_separator_right
    ">"
  end

  # Returns TRUE if the provided value is an external URL and FALSE if
  # it represents the name of a controller. External URL's can be easily
  # distinguished because they begin with "http://".
 
  def is_external_breadcrumb(val)
    val.start_with?('http://')
  end

  # Returns a string containing the HTML for one breadcrumb link within
  # a breadcrumb trail. The first argument is the title of the link, while
  # the second is an array containing the components necessary to build
  # the destination URL for the link.
  #
  # WARNING: Does not work in controllers because the link_to method
  # is only available to views.

  def build_crumb(title, args)
    str = ""
    if is_external_breadcrumb(args[0])
      str += "<a href='#{args[0]}'>#{title}</a>"
    else
      cmd = "link_to '#{title}', :controller => '#{args[0]}'"
      cmd += ", :action => '#{args[1]}'" unless args.size < 2
      str += "#{eval cmd}"
    end
    str
  end

  # Returns a string containing the HTML necessary to display a breadcrumb
  # trail. The first arg contains an array of elements, where the first
  # element is the name of a breadcrumb, the second is an array containing
  # values for assembling a URL (controller, controller & action, or
  # external URL), and so on in alternating fashion. The final arg is a
  # hash containing options, where the only option currently defined
  # is ":direction". This can have values of either "left" or "right", and
  # governs which way the breadcrumbs will be oriented. The default
  # is "right".
  #
  # An example of the method's usage in a view is:
  #
  # <%= show_breadcrumbs(
  #        ['Home', ['main'],
  #         'Tools', ['tools'],
  #         'Antimatter', ['tools', 'antimatter']], 
  #        :direction => 'left') %>

  def show_breadcrumbs(crumbs, opts = nil)
    direction = 'right'                        # Default direction
    separator = breadcrumb_separator_right     # Default separator
    if opts != nil
      dir = opts[:direction]
      if dir == 'left'
        direction = dir
        separator = breadcrumb_separator_left
      end
    end
 
    str = ""
    if crumbs.size > 0
      str += '<div id="breadcrumbs">'
      if direction == 'right'
        i = 0
        while i < crumbs.size
          args = crumbs[i + 1]
          str += " #{separator} " if i > 0
          str += build_crumb(crumbs[i], args)
          i += 2
        end
      else # Direction equals left
        i = crumbs.size - 2
        while i >= 0
           args = crumbs[i + 1]
           str += " #{separator} " if i < (crumbs.size - 2)
           str += build_crumb(crumbs[i], args)
           i -= 2
        end
      end
      str += '</div>'
    end

    str
  end

end

In Listing 1, the show_breadcrumbs method is the real workhorse, i.e. – the method that users are expected to use to generate their breadcrumbs trail. The code can be included within a helper, such as application_helper.rb. From there, the code would be accessible to all views within a Rails application.

Within a view, the show_breadcrumbs method could be called to generate the HTML for the breadcrumb trail:

       <%= show_breadcrumbs(
          ['Home', ['main'], 
           'Tools', ['tools'],
           'Antimatter', ['tools', 'antimatter']],
          :direction => 'left') %>

The first argument is an array, where each breadcrumb element is represented by a pair of array entries. The first value is the name of the breadcrumb, e.g. – the name of the link. The second is an array containing the data necessary to construct the link, such as [controller, action] or [url].

After the initial array argument, the method accepts options. The only currently supported option is “:direction” which can have values of “left” or “right” to define the direction of flow for the trail.

Paths Not Chosen

Is a breadcrumb trail an element of a view? Or should the generation of a breadcrumb trail be the responsibility of a controller?

There’s no right answer, but I chose to consider it part of a view. I placed the breadcrumb generation code in application_helper.rb, which makes it available to all of the views in a Rails application.

      include BreadcrumbTrail

The code could just as reasonably be included in a controller. Of course, it wouldn’t work as-is because the build_crumb method won’t work in a controller. It uses the link_to helper method, which is only available in views. Still, if the method were rewritten to be independent, it could be used in a controller.

The obvious location to reference the library would be application_controller.rb, which would make the code available to all other controllers.

A controller could generate the HTML for a breadcrumb trail as shown below:

       @trail = show_breadcrumbs(
          ['Home', ['main'],
           'Tools', ['tools'],
           'Antimatter', ['tools', 'antimatter']],
          :direction => 'left')

The @trail variable will be visible to the associated view. The breadcrumb trail could be included within a view as shown below:

       <%= @trail %>

Another design decision that I made was to use an array to represent the elements of the breadcrumb trail, with each element represented by two entries within the array. At first glance, it would seem more logical to use a hash. After all, it would be easy to set up a hash with entries for “Home,” “Tools,” and “Antimatter.” 

The value for each hash entry would then be an array. The downside is that a hash doesn’t guarantee the order in which the keys are stored, which makes it problematic to retrieve the breadcrumb entries in the proper order. So, the hash is out as a solution.

Note: Hash order is not guaranteed in Ruby 1.8.x. However, in Ruby 1.9 the hash order will be preserved, which will potentially make hashes a lot more useful.

Conclusion

We’ve ended up with a relatively simple set of methods for generating breadcrumbs. It probably doesn’t handle all cases, but it handles most of the likely ones. As a fall-back, users can explicitly provide a URL for the link, which should allow users to handle cases where controller and action are not sufficient to build a full URL for the link.

It’s not an ideal solution, though. A high proportion of users are likely to want to generate a trail in a controller, which this code doesn’t support. Plus, the array syntax for defining a breadcrumb trail still seems a little cumbersome, though still workable.

In Part 2 of this article, we’ll look at some ways to futher improve this solution.