Breadcrumbs on Rails with Gretel

Ilya Bodrov
Tweet

Hänsel und Gretel entdecken das Hexenhaus im Wald

You probably know that breadcrumbs are navigational elements that help users track their current location on a website. Breadcrumbs can list either previously visited pages or parent pages of the current one (on hierarchical websites). You also probably know where this term comes, but I’ll remind you just to be sure.

Wikipedia says that “Hansel and Gretel” is a well-known fairy tale of German origin recorded by Brothers Grimm and published in 1812. This tale tells about a young brother and sister who were left in the woods by their own father because the family had nothing to eat. Hansel, fortunately, happened to have a slice of bread with him and left a trail of bread crumbs so that they could find a way back home. Unfortunately, all the crumbs were eaten by birds and the children had to keep wandering, ending up facing a wicked witch. If you don’t know how this tale ends, read it some time.

Anyway, the term “breadcrumbs” has its origin in this tale. Just like Hansel left a trail of crumbs, we present a user with a trail of hyperlinks so they can see the trail they’ve taken through the site. Let’s hope our breadcrumbs are not eaten by birds as well…

Having breadcrumbs on a large hierarchical websites (like the Mozilla Developer Network) is a nice practice, but how can we integrate it into our Rails app?

This article creates a demo app with breadcrumbs using a gem called Gretel. The demo shows how breadcrumbs can be styled, customized, and scaled for large websites.

The source code for the demo app is available on GitHub.

A working demo can be found at http://sitepoint-gretel.herokuapp.com/.

Starting the Journey

Rails 4.1.4 is used for this demo, but the same solution can be implemented with Rails 3.

Okay, let’s go. We’ll create Music Shop, a really simple, online store selling music albums. Of course, we won’t implement everything, but let’s pretend that some day this app will grow into something big.

Create a new app without default testing suite:

$ rails new music_shop -T

We’ll use Twitter Bootstrap for style and layout, so drop this gem into your Gemfile:

Gemfile

[...]
gem 'bootstrap-sass'

Run

$ bundle install

Next, rename application.css to application.css.scss and change its contents to:

stylesheets/application.css.scss

@import 'bootstrap';
@import 'bootstrap/theme';

To take advantage of Bootstrap’s magic, change the layout:

layouts/application.html.erb

[...]
<div class="navbar navbar-inverse" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Music shop', root_path, class: 'navbar-brand' %>
    </div>
    <ul class="nav navbar-nav">
      <li><%= link_to 'Albums', albums_path %></li>
    </ul>
  </div>
</div>

<div class="container">
  <%= yield %>
</div>
[...]

Okay, now we can move on to the model and corresponding routes. For this demo, there is a single model – Album – with the following fields (not to mention default id, created_at, updated_at):

  • title (string)
  • description (text)
  • artist (text)
  • price (decimal)

Create the corresponding migration:

$ rails g model Album title:string description:text artist:string price:decimal

Open the migration file and alter this line:

xx_create_albums.rb

[...]
t.decimal :price, precision: 6, scale: 2
[...]

The price can have up to 6 digits with 2 digits after the decimal point. Apply the migration:

$ rake db:migrate

And some routes (we are going to skip update, destroy, new and create for now and pretend those will be created on the next iteration):

routes.rb

[...]
resources :albums, only: [:index, :edit, :show]
root to: 'albums#index'
[...]

Now, create the corresponding controller:

albums_controller.rb

class AlbumsController < ApplicationController
  def index
    @albums = Album.all
  end

  def edit
    @album = Album.find(params[:id])
  end

  def show
    @album = Album.find(params[:id])
  end
end

Let’s also take care of the index view:

albums/index.html.erb

<div class="page-header">
  <h1>Albums</h1>
</div>

<table class="table table-hover">
  <tr>
    <th>Title</th>
    <th>Artist</th>
    <th>Description</th>
    <th>Price</th>
  </tr>
  <% @albums.each do |album| %>
    <tr>
      <td><%= link_to album.title, album_path(album) %></td>
      <td><%= album.artist %></td>
      <td><%= album.description %></td>
      <td><%= number_to_currency album.price, unit: '$' %></td>
    </tr>
  <% end %>
</table>

Note that we are using the number_to_currency helper to format the price correctly (we are presuming the price is entered in USD).

Adding Some Albums

At this point, we have the model, controller and a view, but no data. We could enter them by hand, but that would be really tedious. Instead, let’s use seeds.rb to load some demo data into our database.

Generate some random titles and prices using the faker gem created by Benjamin Curtis. Add faker to the Gemfile:

Gemfile

[...]
gem 'faker'
[...]

and don’t forget to run

$ bundle install

Create a script to load the demo data:

seeds.rb

50.times do
  Album.create({title: Faker::Lorem.words(2).join(' ').titleize,
                description: Faker::Lorem.paragraph(3, true, 4),
                artist: Faker::Name.name, price: Faker::Commerce.price})
end

words(2) means that we generate an array of two random words. titleize is used to format those words like a title. paragraph(3, true, 4) creates three paragraphs with four random sentences. name, as you’ve might have guessed, generates a random name and price creates a price. Easy, isn’t it?

Load the data:

$ rake db:seed

Run the server and check out how this all looks. We’ve done the ground work and are ready to start integrating breadcrumbs.

Getting the Slice of Bread

Just like Hansel had his slice of bread, we need our own tool to start creating our bird-proof trail. Meet Gretel, the flexible Rails breadcrumbs plugin created by Lasse Bunk. As stated in this nice summary image, Gretel does not mess with your controller – all required configuration is placed elsewhere.

Drop the gem into your Gemfile:

Gemfile

[...]
gem 'gretel'
[...]

and run

$ bundle install

Use the generator to create the basic configuration file:

$ rails generate gretel:install

Inside the config directory, you will find the breadcrumbs.rb file that contains all your configuration. You are probably thinking, “This means that I will have to reload my server each time I modify this file?”. Fortunately, you don’t.

Starting from version 2.1.0, in the development environment this file gets reloaded automatically each time it is changed. In the production environment, it is loaded only once.

Now, we can create our first crumb using the following code:

breadcrumbs.rb

crumb :root do
  link "Home", root_path
end

The layout and views also requires modifications:

layouts/application.html.erb

<%= breadcrumbs pretext: "You are here: " %>

The pretext option specifies the text that should be placed in front of the crumbs on the page. You can also specify posttext to put something after the crumbs.

albums/index.html.erb

<% breadcrumb :root %>
[...]

This line is required to tell which crumb to render.

Fire up the server, navigate to the main page, and… breadcrumbs will not show up. Why is that???

By default, Gretel does not show breadcrumbs if it has only one link. To fix this, alter the breadcrumbs method:

layouts/application.html.erb

<%= breadcrumbs pretext: "You are here: ", display_single_fragment: true %>

You should now see your breadcrumbs block. Another problem is styling – right now it looks pretty ugly. Luckily, Gretel already knows how to render breadcrumbs for Bootstrap. Just provide the style option like this:

layouts/application.html.erb

<%= breadcrumbs pretext: "You are here: ", display_single_fragment: true, style: :bootstrap %>

style may take other values like ul, ol, foundation5 (renders breadcrumbs to be styled by Foundation) and inline.

Reload the page et voila! Bootstrap will take care of the styling for us. However, there is a problem with the separator between crumbs. This is because Bootstrap’s styles append / as a separator and Gretel uses by default. So, again, alter the breadcrumbs method:

layouts/application.html.erb

<%= breadcrumbs pretext: "You are here: ", display_single_fragment: true, style: :bootstrap, separator: '' %>

Cool! By the way, if you want the current crumb to be rendered as a link set the link_current option to true. You may also use semantic breadcrumbs by setting semantic to true. Gretel has a handful of other options, which can be found here.

Creating the Trail

Okay, how about extending the trail a bit? We still have show and edit views to be created. Let’s start with show:

albums/show.html.erb

<div class="page-header">
  <h1><%= @album.title %></h1>
</div>

<p><%= @album.description %></p>

<%= link_to 'Edit', edit_album_path(@album), class: 'btn btn-primary' %>

In a real app, you’d need some kind of authentication and authorization system so that the “Edit” button is not visible to everyone, but this is just a demo.

Now create a new crumb:

breadcrumbs.rb

crumb :album do |album|
  link album.title, album
  parent :root
end

Here, the album variable is used to fetch the album’s title and display it on the page. We also speciy the crumb’s parent as root. By the way, we could have skipped this line because, by default, Gretel makes root the parent of the crumb (it is controlled by the autoroot option).

Add the breadcrumb method to the ‘show’ view:

albums/show.html.erb

<% breadcrumb :album, @album %>
[...]

As you see here, we not only provide the crumb’s name, but also pass the resource. Actually, we can provide as many arguments as we want. This line can also be simplified (breadcrumb will be automatically inferred):

<% breadcrumb @album %>

Lastly, let’s deal with the edit view:

albums/edit.html.erb

<% breadcrumb :edit_album, @album %>

<div class="page-header">
  <h1>Edit <%= @album.title %></h1>
</div>

<p>Coming in the next version...</p>

Don’t forget to create a new crumb:

breadcrumbs.rb

[...]
crumb :edit_album do |album|
  link "Edit #{album.title}", album
  parent :album, album
end

Again, we use the Album resource and specify the album crumb as a direct parent. Reload your page, navigate to an album, and click “Edit” – note how the breadcrumbs change. That was easy, huh?

Extending the Trail

Okay, let’s create some more crumbs. As we’ve already discussed, the real online would have some kind of authentication. We are not going to do this, but we will create a user’s profile page and add a crumb for it.

Create a new route:

routes.rb

resources :users, only: [:show]

and the corresponding controller:

users_controller.rb

class UsersController < ApplicationController
end

Provide a link to the show page (with a fake user id):

layouts/application.html.erb

<div class="navbar navbar-inverse" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Music shop', root_path, class: 'navbar-brand' %>
    </div>
    <ul class="nav navbar-nav">
      <li><%= link_to 'Albums', albums_path %></li>
    </ul>

    <ul class="nav navbar-nav navbar-right">
      <li><%= link_to 'Profile', user_path(1) %></li>
    </ul>
    [...]

and create the actual view:

users/show.html.erb

<% breadcrumb :user %>
<div class="page-header">
  <h1><%= current_user %>'s profile</h1>
</div>

<p>Coming soon...</p>

Suppose we have the current_user helper method that returns the user’s name. Can we use this helper when creating the crumb? Let’s try it out!

application_helper.rb

module ApplicationHelper
  def current_user
    "Someone"
  end
end

breadcrumbs.rb

[...]
crumb :user do
  link "#{current_user}'s profile"
end

It appears that helpers can be used without any problems! We can also implement conditions inside the crumb method. Suppose we want to include a link to the admin area in the breadcrumb if the user is an admin. Add this admin? stub method:

application_helper.rb

[...]
def admin?
  true
end
[...]

And alter your crumb:

breadcrumbs.rb

[...]
crumb :admin_root do
  link "Admin's area"
  parent :root
end

crumb :user do
  link "#{current_user}'s profile"
  if admin?
    parent :admin_root
  else
    parent :root
  end
end

After reloading the page, you’ll see the Admin area” crumb. Note that the second argument has not been provided to the link method, which causes the crumb to be rendered as plain text, without a link.

Okay, one more experiment. Imagine we have a search page and want to include breadcrumbs there as well. Could we show the actual search term inside the breadcrumb?

Add one more route:

routes.rb

[...]
get '/search', to: 'pages#search', as: :search
[...]

Create controller:

pages_controller.rb

class PagesController < ApplicationController
  def search
    @term = params[:q]
  end
end

View:

pages/search.html.erb

<% breadcrumb :search, @term %>

<% parent_breadcrumb do |link| %>
  <%= link_to "Back to #{link.text}", link.url %>
<% end %>

<div class="page-header">
  <h1>Searching for <%= @term %></h1>
</div>

<p>Guess what? Coming soon!</p>

And a search form:

layouts/application.html.erb

[...]
<div class="navbar navbar-inverse" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Music shop', root_path, class: 'navbar-brand' %>
    </div>
    <ul class="nav navbar-nav">
      <li><%= link_to 'Albums', albums_path %></li>
    </ul>

    <ul class="nav navbar-nav navbar-right">
      <li><%= link_to 'Profile', user_path(1) %></li>
    </ul>

    <%= form_tag search_path, method: :get, class: 'navbar-form navbar-right' do %>
      <%= text_field_tag 'q', nil, placeholder: 'Search' %>
    <% end %>
  </div>
</div>
[...]

Create a new crumb:

breadcrumbs.rb

[...]
crumb :search do |keyword|
  link "Searching for #{keyword}", search_path(q: keyword)
end

As you can see, route helpers can be used to generate a link with the actual search term that was provided by the user. Go on and try this out!

By the way, you can also fetch the parent breadcrumb and create a “Go back” link like this:

<% parent_breadcrumb do |link| %>
  <%= link_to "Back to #{link.text}", link.url %>
<% end %>

Which can come in handy sometimes.

The last thing to mention is that one crumb may have multiple links. For example, we could do this:

breadcrumbs.rb

crumb :root do
  link "Music shop"
  link "Home", root_path
end
[...]

Now, there will be a “Music shop” link displayed before the “Home” link.

We can also tweak the link’s name depending on a condition. For example, if our shop has nice discounts on Mondays, we can notify users about that (OK, that is a really contrived example):

breadcrumbs.rb

crumb :root do
  link "Music shop" + (Time.now.monday? ? " (SALES!)" : '')
  link "Home", root_path
end
[...]

Scaling the Trail

For large websites, the breadcrumbs.rb file may become really complicated. Fortunately, Gretel allows the configuration to be split into multiple files easily. To do this, create a new directory called breacrumbs inside config and place .rb files with your config there.

In this demo, I’ve created albums.rb with the crumbs related to albums and other_stuff.rb with other crumbs. You may remove the breadcrumbs.rb file or leave it with some other configuration – it will still be loaded.

Another thing worth mentioning is that breadcrumbs will also be loaded from any engine your app has, however breadcrumbs in the main app take a higher priority.

Conclusion

This is all for today, folks. We’ve taken a look at the Gretel gem to easily add breadcrumbs to your app. The gem is flexible and really easy to use, so I encourage you to give it a try!

What do you think of breadcrumbs? Are they useful? Have you ever implemented them in your app? Did you use a gem or did you write everything from scratch? Share your experience!

Thanks for reading!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments