Topic: Menu Builder / Building

GOAL: Produce a css tab menu system with page awareness.

We all have written bad code.  However I can't remember who said it, but there is no such thing has bad code only re-factoring opportunities.

The first implantation for this menu system was ugly.  But with the help from a railsweenie post http://www.railsweenie.com/forums/6/topics/1147 I was able to rid the ugliness.  And I learned a thing or two and wanted to pass things along.


THE MENU

<ul class="tabnav">
    <li class="active"><a href="#">Tab One</a></li>
    <li class="inactive"><a href="#">Tab Two</a></li>
    <li class="inactive"><a href="#">Tab Three</a></li>
    <li class="inactive"><a href="#">Tab Four</a></li>
</ul>

Super simple Tab menu from http://unraveled.com/projects/css_tabs/

THE CSS

ul.tabnav { 
text-align: left;
margin: 1em 0 1em 0;
font: bold 11px verdana, arial, sans-serif;
border-bottom: 1px solid #6c6;
list-style-type: none;
padding: 3px 10px 3px 10px;
}

ul.tabnav li {
display: inline;
}


ul.tabnav li.active a  {
background-color: #fff;
color: #000;
position: relative;
top: 1px;
padding-top: 4px;
}

ul.tabnav li a {
padding: 3px 4px;
border: 1px solid #6c6;
background-color: #cfc;
color: #666;
margin-right: 0px;
text-decoration: none;
border-bottom: none;
}

ul.tabnav a:hover {
background: #fff;
}


UGLY IMPLANTATION

A @page_id variable is set from a private method named "set_up" and called using a :before_filter

class Foo < ApplicationController
   
    before_filter :set_up
   

    private #-------
   
    def set_up
        @page_id = 'home'
        # other set up items
    end
end


Then in the view @page_id is passed into the "menu_builder" method located in the application_helper

In the view

<ul class="tabnav">
<%= menu_builder(@page_id) %>
</ul>

In the application_helper
def menu_builder(page_id)
  content = ""
  case page_id
  when 'home'
    content << "<li class=\"selected\"><a href=\"/main\">Home</a></li>\n"
    content << "<li class=\"inactive\"><a href=\"/tag\">Tags</a></li>\n"
    content << "<li class=\"inactive\"><a href=\"/tag\">Faq\'s</a></li>\n"
    content << "<li class=\"inactive\"><a href=\"/user\">My Account</a></li>\n"
  when 'tag'
        # repeat as need and sink deeper into the ugliness
  end
end

Appalling I know.  I hope you weren't eating, but lets fix things.

The nastiness really happens inside the menu_builder method.  We need to replace that case statement and tell no one of the coding crime we(I) committed.

BETTER IMPLEMENTATION

def menu_builder(page_id)
  tabs = ['home','store','faq']
  content = ""
  tabs.each do |tab|
    content << if page_id == tab
      content_tag('li', content_tag('a', tab, :href => nil ), :class => 'active') + " "
    else
      content_tag('li', content_tag('a', tab, :href => "/#{tab}" ), :class => 'inactive') + " "
    end
  end
  content_tag(:ul, content, :class => 'tabnav')
end

But what is all this jibber?  Let roll it up and smoke it and find out.

It is really straight forward.  We just get a lot of help from the "content_tag"

Here is the api documentation http://api.rubyonrails.com/classes/Acti … ml#M000598

Here is the api docs example

content_tag(:p, "Hello world!")
=> <p>Hello world!</p>

Back to the method..

First thing we create is an array to hold our menu items/tabs

tabs = ['home','store','faq']

Next we create an empty string to hold the results of our iteration
content = ""

Then we iterate thru our array putting the results in the empty string we created
tabs.each do |tab|
  content << if page_id == tab
    content_tag('li', content_tag('a', tab, :href => nil ), :class => 'active') + " "
  else
    content_tag('li', content_tag('a', tab, :href => "/#{tab}" ), :class => 'inactive') + " "
  end
end

While iterating thru the array we check to see if the array element is equal to the page_id that we passed the method. 

If they are equal.

Then we set css class for the li element to 'active' and the href property is set to nil.  You don't want a page linking to itself.  That is some bad juu juu.   

content << if page_id == tab
  content_tag('li', content_tag('a', tab, :href => nil ), :class => 'active') + " "

If they are not equal

Then we set the css class to "inactive" and set the href property.

else
  content_tag('li', content_tag('a', tab, :href => "/#{tab}" ), :class => 'inactive') + " "

And the last thing is to pull all the content from the iteration and place it in a content_tag
content_tag(:ul, content, :class => 'tabnav')

That is it..

Now we just call the 'menu_builder' method in the view

<%= menu_builder(@page_id) %>

And that is it.  I hope you may have learned something.  All comments are always welcome.

Good day railers
kirk out

Last edited by kirk (2007-03-15 17:35:18)

Re: Menu Builder / Building

Thanks for posting this kirk!  This seems to be just what I am looking for.  However, being an utter newbie, I am not sure if it is applicable or not.  Here's what I'm trying to do:

1. Provide a form with elements from multiple models -- I learned how to do this from Agile Web Development (Ch. 22, Multiple Models in a Form) using <% form_for ... %> / <fieldset> construct

2. There are five different models with a total of about 50 text fields, date selectors and text areas.  As you can see, I would end up with a really long form if I used just the <% form_for ... %> / <fieldset> method

3. I am hoping that I can use your CSS Tab Menu System as a container to hold the fields for each model.  And, instead of having a really long form, I would have 5 tabs -- one for the main model attributes, and 4 for the attributes of the related models -- i.e., one tab for each model and its respective <fieldset> 

4. All of this would need to be wrapped into a <% form_for ... %> / <fieldset> construct with CSS tags that would render each <fieldset> on the appropriate tab

Is this doable?  Any thoughts on how to approach this?  Thx,

Dondi.

Re: Menu Builder / Building

Good Tutorial. 
I have a question though. Is there any way to ensure the order of menu items? because it seems like ruby traverses the hash in an arbitrary order.

Last edited by addisu (2007-08-19 05:06:36)

Re: Menu Builder / Building

addisu: Yeah, by using an array and that's what the tabs variable is doing. A hash uses braces { } in Ruby and Rails while an array uses brackets [ ].