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):
Here's how I did it, without gems.
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:
- Add arbitrary menu item from the controller;
- Evaluate the request to see if the item should be highlighted;
- Change the CSS class/id like you would with a normal tag;
- 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:
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
Line #5-6: You need to include the
ActionView helpers to have access to the methods within a view i.e.
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
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.
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.
Once you have all of your items configured, you can render it using
# app/views/layout/application.html.erb <html> <body> <%= menu.render %> </body> </html>
I hope you like this!