Tired of building custom navigation code for each site?
Have you seen Ryan Heath's Navigation Helper? It's very usable, but it only does one nav-bar. I often have sites where there are many, e.g.
+-----------------------------------------------------------+
| MyApp | Home | Sign Out | FAQ | | # "site" nav bar
|===========================================================|
| | *Projects* | Friends | | # "area" nav bar
|===========================================================|
| | Project Overview | *Budget* | History | | # "tabs" nav bar
| |
| |
| Project "Foo" Budget |
| ==================== |
| ... |
+-----------------------------------------------------------+
Each navigation bar has dynamic content. The site one will often have a "Sign In" and "Sign Up" link when no user is signed-in, but will have a "Sign Out" link otherwise. The area bar will often change based on the permissions/roles of the person signed in. The tabs bar will certainly depend on the area and the permissions/roles, and possibly also the features of the item being viewed. (Do all projects have budgets? Also, if the "Budget" link goes to "./budget" then it doesn't have to be generated for each project, but it might not be so easy.)
(Of course, these site, area, and tabs aren't the only possibilities, merely a common pattern.)
Another common pattern is to differentiate the currently-selected navigation item in each applicable bar. (The asterisks in the above display.) In the case above, it probably makes sense to leave the "Projects" area item as a link when browsing a subject to let the user easily return to the projects listing page. On the other hand, the currently active tabs item probably doesn't need to be a link. Clicking that would be the equivalent of a refresh.
Thus, I propose the following plugin. I would greatly appreciate any thoughts -- please fork this Gist or send me a note. Let's thrash early.
Each of these is really just a shortcut to creating a before_filter
. The one significant problem is that path helpers and instance variables aren't available here, so they have to be described as Proc
s.
The absolute simplest usage assumes that there is only one navigation bar for the site and that each item has some text that is the same as a path helper:
class ApplicationController
nav_bar do |nb| # assumes the :default nav bar
nb.nav_item :home # like link_to('home', :home_path)
nb.nav_item :faq # like link_to('faq', :faq_path)
end
end
Have multiple navigation bars on your site? Use nav_bar(name) {...}
or nav_item_for(nav_bar_name, item_name)
:
class ApplicationController
# individual style:
nav_item_for :header, :home
# block style:
nav_bar(:footer) do |nb|
nb.nav_item :about_us
nb.nav_item :faq
nb.nav_item :feedback
nb.nav_item :privacy_policy
end
end
# in app/views/layouts/application.rb:
<%= render_navigation_bar :site -%>
...
<%= render_navigation_bar :area -%>
Have a navigation item that should only appear conditionally? Use :if
or :unless
:
class ApplicationController
nav_item_for :site, :sign_in, :unless => :signed_in?
nav_item_for :site, :sign_out, :if => :signed_in?
nav_item_for :area, :admin,
:if => lambda { |controller| controller.current_user.admin? }
end
Question: you probably want to declare a navigation item like the 'Admin' area in a central place like ApplicationController
, but you might want it to be the rightmost item in the area navigation bar. How should we specify position?
Or just for certain actions? Use :only
or :except
:
class ProjectsController
nav_bar(:tabs, :only => [:show, :budget, :history, :edit]) do |nb|
...
end
end
Want some prettier text? Pass :text
:
class ApplicationController
nav_item_for :site, :faq,
:text => "<abbr title='Frequently Asked Questions'>FAQ</abbr>"
end
Have an external link? Pass a String
URL:
class ApplicationController
nav_item_for :site, :blog, :link => 'http://blog.example.org'
end
Want to use a named route other than the one generated from the item_name
? Pass a Proc
that, when called, generates a path:
class ApplicationController
nav_item_for :site, :faq,
:link => lambda { |controller| controller.page_path('faq') }
end
Want to specify which area is active for all actions within a Controller
? Use nav_bar.selected
:
class ProjectsController
nav_bar(:area).selected = :projects
end
All of the class versions above are just before_filter
s that wrap instance methods, so they're all available within an action.
class ProjectsController
nav_bar(:area).selected = :projects
def index
nav_bar(:tabs) do |nb|
nb.nav_item :by_alpha, :text => 'A-Z',
:link => projects_path(:tab => 'by_alpha')
nb.nav_item :recent, :text => 'Recently Updated',
:link => projects_path(:tab => 'recent')
nb.selected = params[:tab] || :by_alpha
end
end
def show
@project = Project.find(params[:id])
prepare_project_tabs :overview
end
def budget
@project = Project.find(params[:id])
prepare_project_tabs :budget
end
def history
@project = Project.find(params[:id])
prepare_project_tabs :history
end
protected
def prepare_project_tabs(selected)
nav_bar(:tabs) do |nb|
nb.nav_item :overview, :link => project_path(@project)
nb.nav_item :budget, :link => budget_project_path(@project)
nb.nav_item :history, :link => history_project_path(@project)
nb.selected = selected
end
end
end
Generate the :default
navigation bar (usually for a single-bar site):
<%= nav_bar -%>
That does (approximately) the same thing as this:
<ul class="default nav">
<li class="home"><a href="<%= home_path %>">Home</a></li>
<li class="faq"><a href="<%= faq_path %>">Faq</a></li>
</ul>
Generate a named navigation bar:
<%= nav_bar(:area) -%>