Breadcrumbs in Rails

I’ve been working a little with Ruby on Rails recently.  One of the things I needed was a good module for handling navigational breadcrumbs.  I googled around a bit, but wasn’t able to find anything that fit my needs.  The closest to what I wanted was this example on stackoverflow.  It was a great starting point, but I ran into issues when I wanted to use nested controllers.  I wanted something a little more flexible.  Here’s my final solution.  I’m still a complete noob at rails, so feel free to point out any improvements you may have.

Features:

  • Splits up the URL and looks up the controllers for each section
  • Assumes you have a ‘name’ field on your data structure
  • Doesn’t link the last item since it should be the current pag
  • Works with nested controllers
  • Converts underscores to spaces and titleize’s the labels
  • Easy to modify to fit your own purposes

Code:

def get_bread_crumb(url)
    begin
        breadcrumb = ''
        so_far = '/'
        elements = url.split('/')
        for i in 1...elements.size
       
            so_far += elements[i] + '/'
           
            if elements[i] =~ /^\d+$/
                begin
                    breadcrumb += link_to_if(i != elements.size - 1, eval("#{elements[i - 1].singularize.camelize}.find(#{elements[i]}).name").gsub("_"," ").to_s, so_far)
                rescue
                    breadcrumb += elements[i]
                end
            else
                breadcrumb += link_to_if(i != elements.size - 1,elements[i].gsub("_"," ").titleize, so_far)
            end
           
            breadcrumb += " » " if i != elements.size - 1
        end
        breadcrumb
    rescue
        'Not available'
    end
end

Usage:

<%= get_bread_crumb(request.request_uri) %>

Which converts a URL like this:

/admin/posts/12/comments/8

to breadcrumbs like this:

Admin » Posts » Breadcrumbs in Rails » Comments » Great Post!

Comments

  1. Ben said at 8:10 pm on June 19th, 2010:

    You can change the code for is_int to use regular expressions like so:

    value =~ /^d+$/


  2. Josh Fraser said at 3:20 am on June 20th, 2010:

    You sure? That doesn't seem to be working for me. Is that version specific?


  3. Josh Fraser said at 3:32 am on June 20th, 2010:

    you're right though, a regular expression would be cleaner. i'm going to use this instead:

    elements[i] =~ /^[0-9]*$/

    thanks for the feedback!


  4. Matt said at 9:14 pm on June 19th, 2010:

    It should be:

    <code>

    elements[i] =~ /^d+$/ # d means digits, + ensures at least one

    </code>

    If you don't require at least one digit, an empty string will match (""), like when a URL like "foo//bar" is requested.


  5. @mtodd said at 10:26 pm on June 19th, 2010:

    Eh, it's taking out the backslash in front of the d in the regex. Lame.


  6. Josh Fraser said at 5:29 am on June 20th, 2010:

    Yeah that's annoying. I'll bring it up to my friend at Intense Debate and see if we can get that fixed.


  7. @mtodd said at 9:16 pm on June 19th, 2010:

    It should be:

    <code>
    elements[i] =~ /^d+$/ # d means digits, + ensures at least one
    </code>

    If you don't require at least one digit, an empty string will match (""), like when a URL like "foo//bar" is requested.


  8. @beerlington said at 9:25 pm on June 19th, 2010:

    Ben's missing a before the d. You should also use + instead of * if you want there to be at least one digit.

    /^d+$/


  9. @beerlington said at 9:37 pm on June 19th, 2010:

    Just realized the forward slash was getting removed from comments


  10. Josh Fraser said at 4:40 am on June 20th, 2010:

    ah, i figured there had to be something like. good catch.


  11. Jason said at 8:12 pm on June 19th, 2010:

    Nice! Here's a couple tips on making it more idiomatic. Ruby has lots of nice shortcuts and helpers:

    elements.each do |e| (vs the for loop, you'll rarely see for loops in Ruby)
    if elements[i].class == Fixnum (vs your is_int function)


  12. Josh Fraser said at 3:19 am on June 20th, 2010:

    Thanks! Duly noted.

    I'm getting an array back for element[i].class instead of Fixnum.


  13. @mtodd said at 9:21 pm on June 19th, 2010:

    Be careful of your plurality:

    element[i] != elements[i]


  14. Josh Fraser said at 3:23 am on June 20th, 2010:

    Oh and the reason I went with the for loop was because I needed to be able to get back to the i-1 element. Is there an easy way to do that using your proposed syntax?


  15. Brent said at 8:32 pm on June 19th, 2010:

    you can get the index of element by using something like this: elements.each_with_index { |e,i| … }


  16. Alex said at 8:50 pm on June 19th, 2010:

    each_with_index


  17. Anon said at 9:26 pm on June 19th, 2010:

    Don't do foo.class =, do foo.is_a? X

    In this case, X should be Integer rather than Fixnum.


  18. Jeff Smick said at 9:22 pm on June 19th, 2010:

    Here's my rewrite: http://gist.github.com/445553

    Shouldn't need eval. And Array#join is your friend.


  19. Josh Fraser said at 4:46 am on June 20th, 2010:

    Sweet. That looks good, but it doesn't seem to be working quite right. The numbers still show up in the output instead of the name values.

    For example:

    Admin » Posts » 12 » Comments » 8


  20. @beerlington said at 10:28 pm on June 19th, 2010:

    Josh, the rescue block is likely picking up the failed active record call. I'm guessing you're going to want to change that section to …elements[i-1].classify.constantize.find(element).name.humanize… (note the addition of classify, replacing singularize and camelize from your original code)


  21. Josh Fraser said at 5:39 am on June 20th, 2010:

    tried that. still no luck.


  22. @mtodd said at 10:29 pm on June 19th, 2010:

    That probably means there's an exception every time… It might be because of "users".constantize should be "users".singularize.camelize.constantize


  23. @beerlington said at 9:50 pm on June 19th, 2010:

    Great rewrite. No offense to Josh, but the eval and for loop were killing me.


  24. Josh Fraser said at 4:54 am on June 20th, 2010:

    Yeah as you can tell from my blog, I'm a PHP guy and am literally just getting started with this whole rails thing. I'm super grateful for all the tips here. It's cool to be a part of a community where I get this much great feedback on some code (on a Saturday night no less!)


  25. @beerlington said at 10:20 pm on June 19th, 2010:

    Welcome to the community and apologies for the blunt remark about your code. I came to Ruby/RoR from PHP a few years ago and the first thing I realized was that everything I thought I knew how to do in Ruby was wrong. It was one of the best decision I've ever made though.


  26. Josh Fraser said at 5:24 am on June 20th, 2010:

    No offense taken. A big reason for me posting it was to try and get feedback on what I can do better.


  27. @mtodd said at 10:31 pm on June 19th, 2010:

    This is a *great* way to get feedback, and clearly it's working. When I was starting out as a professional photographer, I posted lots of my horrible photos to mailing lists and sites designed around providing critical feedback from all kinds of levels of photographers. This taught me so much and improved my craft, my awareness, and my understanding of my own work.

    Keep it up!


  28. @mtodd said at 10:25 pm on June 19th, 2010:

    I might refactor this somewhat to be more idiomatic and clear, such as http://gist.github.com/445588


  29. @mtodd said at 11:05 pm on June 19th, 2010:

    Just to clarify, I changed the call to Record#name to be Record#display_name and then I would define a custom method on each of the models that would show up in the breadcrumb, allowing for lots of customization and defaults. For example:

    class User < ActiveRecord::Base
    def display_name
    "%s %s" % [self.first_name, self.last_initial]
    end
    end

    or even

    class Post < ActiveRecord::Base
    alias_method :display_name, :title
    end


  30. @mtodd said at 11:18 pm on June 19th, 2010:

    Thinking about it, I was kinda disappointed that there wasn't an obvious way to iterate over an array with the previous and the next elements conveniently available. Seeing the Enumerable#each_cons method, I saw that it was most like what I wanted, so I decided to hack this together:
    http://gist.github.com/445605

    Wrapping this further there may be a way to define methods Enumerable#first? and Enumerable#last? to make it easier to add tests without requiring tracking the index, et al.


  31. Josh Fraser said at 6:25 am on June 20th, 2010:

    oooh very nice. i like that a lot.


  32. @randland said at 8:56 am on June 22nd, 2010:

    Rewrote breadcrumb generator to handle resources and actions via Routes.recognize_path… http://gist.github.com/448656


  33. @indirect said at 12:40 am on June 20th, 2010:

    Nearly all that code is unnecessary if you are using resources in Rails. You don't even need to parse the URL (which is kind of gross, IMHO). Something like this should do everything: http://gist.github.com/445652


  34. weppos said at 2:12 am on June 21st, 2010:

    Sometimes, you can't rely on the URL because it doesn't contain all the necessary information.
    For this reason (and to get more flexibility), I created http://github.com/weppos/breadcrumbs_on_rails.

    Feel free to try it and let me know your feedback 🙂


  35. Anthony Greeb said at 11:49 pm on June 28th, 2010:

    Worth reading "The problem with breadcrumb trails" before going all out http://derivadow.com/2010/02/18/the-problem-with-


  36. Josh Fraser said at 6:59 am on June 29th, 2010:

    Thanks for sharing. Interesting stuff.


  37. Anu said at 2:42 pm on December 12th, 2010:

    Good point!