Pothibo

Navigation menu with yield and content_for in Rails

It's not rare to see a project with navigation menu. Scratch that.

Navigation menus are omnipresent.

A navigation menu example

When it was time to build my menus, I was wondering what was the obvious way to build a menu. Turns out that there isn't such a thing.

I've tried about every options there is under the sun. I tried gems, partials, my own abstraction which I blogged about a few months back.

In general, gems try to wrap menus in plain ruby object and you build menus through helpers. My solution was not different than the others.

Many of those solutions were good enough, but none of them satisfied me. Too complex, too clumsy, not flexible enough, etc.

Sometimes, you see different pieces of a puzzle and you can't make sense of them. You waste hours and days trying to figure things out without any real success. Then you come back at it a few weeks later and voilà!

Well, I was looking at Rails' guide a month ago for something completely unrelated and suddenly I had an epiphany.

The solution had all the thing I wanted: flexibility, transparency, very light architecture. Here's how it's done:

The pieces

This technique uses 2 different API that you may already know:

  • yield :name
  • content_for :name and its boolean version: content_for? :name

yield :name

If you've ever played a little bit with layouts/application.html.erb, there's a very high chance that you've come across yield. But did you know you could pass a symbol as argument?

When you pass an argument to that function, you tell Rails that somewhere in your view's hierarchy, you will create a block with the same symbol to be rendered in that place.

content_for :name

This is the method that needs to be used with yield :name to tell Rails what to render. It's important to know that those two methods work together. One won't work without the other.

Now that we've covered the theory, let's build a menu together to help you understand the process and how it can help you build menus in your project.

A menu

What you want to do is create a place in your layout where you render the navigation menu. You want to use a name easy to remember. Here, I aptly used :menu.

application.html.erb
<html> <body> <% if content_for? :menu %> <nav> <%= yield :menu %> </nav> <% end %> <main> <%= yield %> </main> </body> </html>

Now you can see the two different yield used in the layout. The first one is our menu and the second one is the one used to render actions from controller.

One thing to notice is how the navigation menu is wrapped in a condition. content_for? :menu will return false if you don't set up a menu in your view hierarchy.

This is a fallback that can be very helpful when a navigation menu needs to be shown/hidden.

Now that the layout can support a navigation menu, it's time to render it. Here's two different views that use content_for :menu to render their navigation menu.

posts/index.html.erb
<% content_for :menu do %> <%= archives_link %> <%= about_link %> <%= works_link %> <% end %> # The rest of the layout will be rendered in <main>'s yield.
posts/archives.html.erb
<% content_for :menu do %> <%= archives_link class: %w(active) %> <%= about_link %> <%= works_link %> <% end %> # The rest of the layout will be rendered in <main>'s yield.

The reason I believe this method is so superior to any other is because of how I wrote the menu above. Views are highly coupled with requests. They imply context. You don't normally check if a variable is set in a view, you just use it. Because you expect the controller to set those variables for you.

This kind of assumptions is awesome for menus. Your view knows you are rendering a single post or a list of them. It knows if you're editing a certain resource, etc. Because of those assumptions you can create complex menu relevant to your user without using complex conditions to build it.

Here's another cool example of how powerful this method can be. HTML 5 has a new feature where you can position input elements outside the <form> element. If you had to build a custom menu where you want to have the save button in it, here's how you would do it.

posts/edit.html.erb
<% content_for :menu do %> <%= archives_link class: %w(active) %> <%= about_link %> <%= works_link %> <%= submit_tag t('.update'), form: "posts_form" %> <% end %>

Your submit button is now part of your menu!

Helpers

You may have noticed that I'm using custom helper methods in my menus. The reason why I suggest you to do this is that you don't want to browse through all you views every time you change a route. Also, using helper methods can help you set default attributes for your items in you menu.

app/helpers/navigation_helper.rb
module NavigationHelper def archives_link(html_options = {}) link_to posts_archive_path, default_navigation_options(html_options) end def about_link(html_options = {}) link_to posts_archive_path, default_navigation_options(html_options) end def works_link(html_options = {}) link_to posts_archive_path, default_navigation_options(html_options) end private def default_navigation_options(html_options) html_options[:class] << 'item' end end

Now you have a working navigation menu that you can customize in every views. This method isn't only useful for building menus, you could build breadcrumbs using the same technique. Benefits for using this technique are numerous:

  • It allows you to add status to any link from the view;
  • You can customize the menu in any view the way you want;
  • It's lightweight and easily maintainable;
  • It uses rails' API which means you don't rely on any 3rd party.
  • Everything is transparent, whatever you do is directly rendered to your menu.

If you never used this technique before, I really hope this post will make you consider using it in your projects.

Get more ideas like this to your inbox

You will never receive spam, ever.