Building flexible menu in rails

I am not using this technique anymore! If you're looking to build menus in rails, I suggest you read about my new technique. I am keeping this post public for academic reasons

If you've ever developed a dynamic website, chances are that you had to implement a dynamic menu or a breadcrumb. You know, something like this (taken from Ecrire's admin bar):

Menu used on this blog!

Here's how I did it, without gems.

Basic needs

When I browse for a gem to do that, I wasn't too sure about what I wanted. Many gems tried to be too smart about what they do. What if you want a dumb menu renderer, something you can feed it menu items and it renders them for you. Something that needs to:

  1. Add arbitrary menu item from the controller;
  2. Evaluate the request to see if the item should be highlighted;
  3. Change the CSS class/id like you would with a normal tag;
  4. Render the menu at the place you want.

In essence, this is how the menu could be used:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper_method :menu

  def menu
    @menu ||= Admin::MenuHelper::Menu.new request do |menu|
      menu.add :blog do |item|
        item.path = root_path
        item.label = "Home"
        item.id = 'blogPublicHome'
        item.highlight_when do |request|
          !request.path.start_with?('/admin')
        end
      end

      menu.add :posts do |item|
        item.path = admin_posts_path
        item.label = I18n.t('admin.navigation.posts')
        item.highlight_when do |request|
          request.env['action_controller.instance'].kind_of?(Admin::PostsController)
        end
      end

      menu.add :partials do |item|
        item.path = admin_partials_path
        item.label = I18n.t('admin.navigation.partials')
        item.highlight_when do |request|
          request.env['action_controller.instance'].kind_of?(Admin::PartialsController)
        end
      end
    end
  end

Start with ruby objects, end up with HTML

The idea with this menu is that you want to leverage plain old ruby objects (PORO) to store your links data until you are ready to render your menu.

So the goal is to have two classes: Menu and Item. From there, you want to be able to call Menu#render and have your menu rendered in HTML.

In this example, the ruby classes will live within a helper. Why in a helper you ask? Because it helps you render a menu on the screen, that's why.

I wanted to focus on readability and maintenance. Sure there's shorter way to do this but shorter doesn't always mean better. Anyhow, here's how it looks like.

# app/helpers/admin/menu_helper.rb
module Admin
  module MenuHelper
    class Menu
      include ActionView::Helpers
      include ActionView::Context

      def initialize(request)
        @request = request
        @items = Hash.new.with_indifferent_access
        raise StandardError unless block_given?
        yield self
      end

      def add(name, klass = Admin::MenuHelper::Item)
        raise ArgumentError unless block_given?
        raise IndexError if @items.has_key?(name)
        item = klass.new(@request)
        @items[name] = item
        yield item
      end

      def render
        @items.freeze.values.map(&:render).join.html_safe
      end
    end

    class Item
      include ActionView::Helpers
      include ActionView::Context
      include Rails.application.routes.url_helpers

      attr_reader :request, :css

      attr_accessor :path, :label, :id

      def initialize(request)
        @request = request
        @css = %w(link)
      end

      def highlight_when(&block)
        raise ArgumentError if block.nil?
        @callback = block
      end

      def render
        raise StandardError, "Admin::MenuHelper::Item is not configured properly, it's missing a callback" if @callback.nil?

        @css << "active" if @callback.call(@request)
        link_to(@label, path, class: @css, id: @id)
      end
    end
  end
end

Admin::MenuHelper::Menu

Line #5-6: You need to include the ActionView helpers to have access to the methods within a view i.e. content_tag, link_to, etc.

Line #11-12: Raise an exception if no block is given. I did this because I wanted to make sure some items would exist from the get go. The block is then called and a reference to this menu is passed so it's possible to use menu.add.

Line #15: Where your add items to your menu. This method has a klass argument that is optional should I want to customize an item with a different class. A block must be given. More on this later.

Line #23: Renders the menu to the screen. What it does is that it passes the request object to each item so they can check if they have to be highlighted and then iterates every items and render them.

Admin::MenuHelper::Item

Line #29-30: Same as above, included to gain access to the view's helper methods.

Line #42: It's the block that will be called to check if the item should be highlighted or not.

Line #47: The render block that will generate the HTML output for this item. The item will have a class named active if the block needs to be highlighted.

The finale

Once you have all of your items configured, you can render it using menu#render

# app/views/layout/application.html.erb
<html>
  <body>
    <%= menu.render %>
  </body>
</html>

I hope you like this!