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.resources in 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:

Plugin Directory Tree

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.



Comments

No comments yet. Be the first.



Leave a Comment

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

(not displayed)