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.



Comments

No comments yet. Be the first.



Leave a Comment

Comments are moderated and will not appear on the site until reviewed.

(not displayed)