I recently had to include a breadcrumb style navigation system on a site I’m working on.

You know the score… Like on a forum:

Home > Random Crap > My new pet narwhal

Anyway, I found a few gems and plugins out there which help out with this, but nothing really nailed it for what I needed.

My requirements were:

  • Simple to implement
  • Smartness. I don’t want to write config files for this.
  • ..but not too smart. I don’t want it to guess, guess wrong and for me to have no control over fixing this.
  • Flexibility. Different breadcrumb trails within the same controller is a must.

I found this solution which I liked, but it didn’t handle dynamic linking so well within my slightly complex setup. That’s to say, I didn’t want the page we’re currently on to be an active link, because this is a bit of a usability faux pas, according to Jacob Nielsen. I don’t remember the exact reasoning, but something a little more elaborate was required.

I based my implementation on the above solution, so it has a few similarities.

Without further ado:

lib/breadcrumb_helper.rb

module BreadcrumbHelper

  def self.included receiver
    receiver.extend ClassMethods
  end

  module ClassMethods    
    def breadcrumb name, options = {}
      before_filter options do |controller|
        controller.send(:breadcrumb, name, options)
      end
    end
  end

  def breadcrumb name, options
    @breadcrumbs ||= []
    
    unless options[:url]
      url = "#{name}_path"
    else
      url = options[:url]
    end 
        
    name = name.to_s.capitalize if name.class == Symbol
      
    if options[:link] != false
      url = eval(url) if url =~ /_path|_url|@/ #ouch, ugly hack.
      params_pos = url =~ /\?/
      if params_pos
        useful_url = url[0..params_pos-1]
      else
        useful_url = url
      end
    
      path_parts = ActionController::Routing::Routes.recognize_path(useful_url, :method => :get)

      if path_parts[:controller] == params[:controller] and path_parts[:action] == params[:action]
        @breadcrumbs << { :name => name }
      else
        @breadcrumbs << { :name => name, :url => url }
      end
    else
      @breadcrumbs << { :name => name }
    end
    
  end
  
  def breadcrumbs delimeter = " / "
     links = []
     @breadcrumbs.each do |breadcrumb|
       links << @template.link_to_if(breadcrumb[:url], breadcrumb[:name], breadcrumb[:url])
     end
     links.join("<span class='delimeter'>#{delimeter}</span>")
   end
   
   def breadcrumbs_title
     @template.html_escape(@breadcrumbs.last[:name])
   end
   
end

in app/controllers/application_controller.rb :

include BreadcrumbHelper
helper_method :breadcrumbs
helper_method :breadcrumbs_title
breadcrumb :home

This allows you to use and in your views, as well as setting up an initial breadcrumb of “Home”, which maps to home_path. If you don’t have a home_path in your routes, you might want to change it to something else.

Usage

breadcrumb :items

Items [links to items_path, /items]

breadcrumb "My pet narwhal", :url => item_path(narwhal)

My pet narwhal [links to item_path(narwhal), /item/42 for example] You could only do this in a place where the object narwhal exists in scope, so you might have to put it into a method in your controller.

breadcrumb "People", :url => "users_path"

People [links to users_path, /users]. If you quote the path, it will be eval’d within the controller, this helps avoid putting your breadcrumb defs within methods.

breadcrumb "Something Nice", :link => false

Self explanatory, I hope. In your view

<%= breadcrumbs %>

becomes Home / Items / My pet narwhal

<%= breadcrumbs ":" %>

becomes Home : Items : My pet narwhal

You can also generate the page title using the last breadcrumb, using

<%= breadcrumbs_title %>

I’m sure this can be simplified and optimised further, but.. Enjoy?

Sunday, January 31, 2010