I'm a 37'ish year old web application developer from South Portland, Maine. I love meeting fellow techies, drop me a line if you want to talk shop.
Posted on 05/19/2008 at 05:46 AM
I’m a big fan of JQuery UI and have been using the Tab plugin a lot recently.
However, I found the code to create each Tab a bit repetitious. It became especially ugly in situations where you might want to display a tab conditionally ... first displaying the tab header, then the tab body, and having to re-use the same condition in both places. yuck!
Sooo, since I had recently given a presentation at our local Ruby Users Group on Design Patterns in Ruby, I thought maybe a little template pattern would be a nice fit here. The result: TabsRenderer!!
TabsRenderer is a view helper that lets me build my tabs in a logical order, and then call a special method to return the final result. As well, creating a tab conditionally is a breeze!
I should mention, since I created TabsRenderer as a helper Class and not just a helper method, I had to address the issue of not being able to use Rail’s other helpers within it, as they would be outside my class’s scope. My solution was to borrow a technique I learned recently over at railscasts called ”Refactoring Out Helper Object”.
On to the code ...
5/20/08 - updated with Dietrich’s 3rd param options hash suggestion
6/03/08 - updated with param for options hash on main div as well
6/22/08 - an official repository for this code has been set up at github
Plop this into your helpers directory, and name it tabs_renderer.rb
class TabsRenderer
def initialize(template, options={})
@template = template
@options = options
@tabs = []
end
def create(tab_id, tab_text, options={}, &block)
raise "Block needed for TabsRenderer#CREATE" unless block_given?
@tabs << [tab_id, tab_text, options, block]
end
def render
content_tag(:div, (render_tabs + render_bodies), {:id => :tabs}.merge(@options))
end
private # ---------------------------------------------------------------------------
def render_tabs
content_tag :ul do
@tabs.collect do |tab|
content_tag(:li, link_to(content_tag(:span, tab[1]), "##{tab[0]}") )
end
end
end
def render_bodies
@tabs.collect do |tab|
content_tag(:div, capture(&tab[3]), tab[2].merge(:id => tab[0]))
end.to_s
end
def method_missing(*args, &block)
@template.send(*args, &block)
end
end
Now, include your tab assets and initialize your tabs as usual. TabsRenderer assumes that you want to ID your tab’s div as ‘tabs’.
<% javascript_include_tag("tabs/ui.tabs.pack.js") %>
<% stylesheet_link_tag("tabs/ui.tabs.css") %>
<script type="text/javascript" charset="utf-8">
$(function(){
$('#tabs > ul').tabs();
});
</script>
Now use TabsRenderer to create some tabs ...notice how you can put conditions on the tail end of the block. We pass ‘self’ to our helper so that it knows where to delegate method calls that aren’t it’s own. Creating a tab is as simple as specifying a div #id and an Tab title.
<% tabs = TabsRenderer.new(self) %>
<% tabs.create('basic_tab', 'Basic Info') do %>
# ...
<% end %>
<% tabs.create('gallery_tab', 'Gallery') do %>
# ...
<% end unless @images.blank? %>
<% tabs.create('admin_tab', 'Admin') do %>
<%= render :partial => "super_secret_stuff" %>
<% end if current_user.has_role? 'admin' %>
<%= tabs.render %>
And here is the code that would be generated from the above example.
<div id="tabs">
<ul>
<li><a href="#basic_tab"><span>Basic Info</span></a></li>
<li><a href="#gallery_tab"><span>Gallery</span></a></li>
<li><a href="#admin_tab"><span>Admin</span></a></li>
</ul>
<div id="basic_tab">
# ...
</div>
<div id="gallery_tab">
# ...
</div>
<div id="admin_tab">
# ... rendered partial here
</div>
</div>
Hope this is of use to someone else! Its sure been useful to me. I would love to hear your comments and criticisms. Please feel free to request changes to the class.
ps. Rails folks wanting to use JQuery in their projects should check out jrails, it wraps a lot of the prototype helpers.
pps. JQuery Rocks@!
oh perfect, I hadn’t thought of that. Thanks for the tip Dietrich .... I’ll add that this evening. =)
Added in your suggestion Dietrich
Your tab helper is really great!
In my example below <%=p.name%> does not get updated in each loop it gets stuck to the first ‘p.name’ returned, so every tab body contains the same as the first tab body, any ideas?
<% tabs_for do |tab| %>
<o;r p in @protocols%>
<% tab.create(p.id, p.name) do %>
<%=p.name%>
<% end %>
<n;d%>
<% end %>
not sure what to say, the following worked fine for me:
strange, all I did was change it to .each like in your pastie, instead of ‘for’ and it works now. Thanks
Hi,
Cool renderer!
How would you go about changing it to allow a given tab, when clicked, to fire off an ajax call? Let’s say I have a tab called Invoices, and I want to fire off the index action on the Invoices controller when that tab is clicked. Index would populate the Invoices panel with the results of the index view or partial.
Chris
@Chris The Tabs plugins is ajax ready, all you have to do is put a URL in place of the #ID in the href attribute.
Hi,
I’ve been using this with great success! One question: are there any issues that you are aware of with having a set of tabs nested inside a specific tabs (subtabs)?
Chris
Hi there,
This might be a real noob question, but I can’t see where you got the following files…
<% javascript_include_tag("tabs/ui.tabs.pack.js") %>
<% stylesheet_link_tag("tabs/ui.tabs.css") %>
I’ve visited the Jquery UI site and looked at their tabs widget, but I’m not sure how I take that and implement it with your example. It seems I’m missing some understanding. Where do I find your JS library’s and where do I put them in my rails app? Can I simply plop them into the javascripts folder under tabs?
Any help is appreciated.
Chris
Hi Chris. ui.jquery.com has a downloader you can use to build your installation of the UI kit. If you choose to use their tabs it will be included in the download. As for where to put things in your rails app, just throw files into their appropriate directories under ‘public’.
Hey,
I’m using your code provided from github I think it’s very well laid out but I can’t seem to get it to work. I feel it’s my styling. Do you have a working example anywhere with the recent code from github?
Thanks,
bp
Thanks, nice tip!
Unfortunately it doesn’t work with Rails 2.3.8 because of the concatenated string in the render method (http://breakthebit.org/post/647352254/rails-2-3-8-forced-html-escaping-of-concatenated)
One workaround is to rewrite the method like in http://pastie.textmate.org/1005228
Heiko
Heiko, have a look at the repo, just pushed a change to handle that issue with rails 2.3.8, Thanks to Michelangelo Altamore for the heads up.
I think I am having html_safe issues when using this with Rails 3. I get a string of escaped tags displayed in the browser, no tabs :( any word on an update for Rails 3
Check the github version and you should be fine. if you still have any problems post an issue there and I’ll check it out
Often times I will release code for free or go that extra distance to help others online. If my skills were useful to you, please consider a small donation. Thank you very much.
@ github.com
@ twitter.com
@ calendaraboutnothing
Foundation's Edge, RJones Family, We're Not.com (only for staging), Ailee Jones (same as rjones for now)
Aaron, Barnaby, Brian, Chris, Dirk, Frank, Four, Justin, Matt, Mike, Monty, Paul, Sean, Travis
I can usually be found lounging on irc.freenode.net while I work, on the following channels: #fauna, #github, #hello-heroku, #jquery, #passenger, #ruby, #rubyonrails, #slicehost, #sproutcore, #textmate, #werenot.
Nice… gonna try this with some of my jquery/rails helpers…
You might want to add an options={} to pass styles for example:
def create(tab_id, tab_text, options={}, █)
@tabs << [tab_id, tab_text, options, block]
end
def render_bodies
@tabs.collect do |tab|
content_tag(:div, capture(&tab;[3]),tab[2].merge({:id => tab[0]}))
<% tabs = TabsRenderer.new(self) %>
<% tabs.create(’basic_tab’, ‘Basic Info’, {:style=>"background-color: #00FF00"}) do %>
# ...
<% end %>