Introducing Tabulous: Tabs in Rails

Easy Tabs for Your Rails Application

If you’re like me, most of the Rails applications you’ve written use tabbed navigation. And if you’re like me, you find that writing the code to handle tabs becomes increasingly more boring with each new application. So I wrote tabulous. Tabulous aims to solve this problem once and for all with a quick and easy way to set up and manage your tabs.

Getting Started

The tutorial will get you off to a good start. Have fun!

Update: This blog post is about an old version of tabulous. Tabulous 2 has a completely different syntax. Read more about the new version of tabulous.

Design Decisions

Consolidation

Artwork reminiscent of Jackson Pollock's style.

Whenever I implemented tab code in Rails I always ended up adding logic to my views and peppering my controllers with tab-related code.

Although it worked, it never felt quite right to have my tab-related code splattered all over my views and controllers in tiny little droplets like some sort of new Pollock-inspired software design pattern. So I decided to consolidate tab code as much as possible. It all lives in app/tabs/tabulous.rb. The only exceptions are the calls you have to make in your layout(s) to <%=tabs%> and <%=subtabs%> and any CSS you write for the tabs.

Ruby Table

Perhaps the most unusual design decision I made was to part ways with existing Ruby idioms and create my own idiom which I call a “Ruby table”. It looks like this:

  config.tabs do
    [
      #--------------------------------------------------------------------------------------------------------------------------------------------#
      #    TAB NAME                     |    DISPLAY TEXT      |    PATH                    |    VISIBLE?                         |    ENABLED?    #
      #--------------------------------------------------------------------------------------------------------------------------------------------#
      [    :home_tab                    ,    'Home'            ,    root_path               ,    true                             ,    true        ],
      [    :people_tab                  ,    'People'          ,    people_path             ,    true                             ,    true        ],
      [    :preferences_tab             ,    'Preferences'     ,    preferences_path        ,    true                             ,    true        ],
      [    :services_tab                ,    'Services'        ,    services_path           ,    true                             ,    true        ],
      [    :jobs_tab                    ,    'Jobs'            ,    jobs_path               ,    true                             ,    true        ],
      [    :schedules_tab               ,    'Schedules'       ,    "/schedules"            ,    can? :view, Schedule             ,    true        ],
      [    :unavailable_dates_subtab    ,    'Availability'    ,    "/unavailable_dates"    ,    can? :manage, UnavailableDate    ,    true        ],
      #--------------------------------------------------------------------------------------------------------------------------------------------#
      #    TAB NAME                     |    DISPLAY TEXT      |    PATH                    |    VISIBLE?                         |    ENABLED?    #
      #--------------------------------------------------------------------------------------------------------------------------------------------#
    ]
  end

Now Ruby has an idiom which I absolutely love: sending arguments to a method using a hash. Here is a method call with and without the idiom:

  display 'some message', 'red', 40, 5
  display 'some message', :color => 'red', :max_length => 40, :indentation => 5

Let’s see what happens when we use hashes to represent the above tab data:

  config.tabs do
    {
      :home_tab                 => { :path => root_path },
      :people_tab               => { :path => people_path },
      :preferences_tab          => { :path => preferences_path },
      :services_tab             => { :path => services_path },
      :jobs_tab                 => { :path => jobs_path },
      :schedules_tab            => { :path => "/schedules",
                                     :visible => can? :view, Schedule },
      :unavailable_dates_subtab => { :text => 'Availability',
                                     :path => "/unavailable_dates",
                                     :visible => can? :manage, UnavailableDate }
    }
  end

The hash version has two benefits:

  1. It can rely on intelligent defaults so the code is terser.
  2. Rubyists are quite used to hashes, even nested hashes.

The Ruby table also has some compelling benefits which is why I ultimately went with it. One is explicitness. The hash version relies on intelligent defaults which means that the programmer needs to be familiar with how these defaults are determined. The hash version also does not show the programmer all of the available options. The Ruby table explicitly shows all options and all values. No guesswork.

Secondly, the Ruby table is easier to scan than the nested hash. Since code is read more often than it is written, I opted for the solution that was easier to read even though it’s more verbose to write.

The Ruby table idiom is not without its drawbacks, however. It forces you into the drudgery of constantly realigning the table columns. That’s why tabulous comes with a tabs:format rake task that does that for you.

You can skip to the end and leave a response. Pinging is currently not allowed.

29 Responses to “Introducing Tabulous: Tabs in Rails”

  1. Thierry says:

    It would be great if we could have a screenshot or an online demo

  2. techiferous says:

    @Thierry Good idea. I think a screencast would work best. In the meantime, install the gem and go to the test/applications/subtabs directory in the gem’s code. Type ‘bundle install’. Then type ‘rails server’ and you should have an interactive example.

  3. Andrius Chamentauskas says:

    I would probably go with different syntax for defining, something like:

    config.tabs do
      tab :jobs_tab, :path => jobs_path
      tab :schedules_tab, :path => "/schedules",
                          :visible => can?(:view, Schedule)
    end

    This would allow you to loose that ugly lambda {} syntax and it looks cleaner that nested hashes.

  4. techiferous says:

    @Andrius I’m familiar with that DSL style and in general I like it a lot. I’m in an experimental mood, though, so I’m pushing the boundaries of normal Ruby usage. The Ruby table is easy to scan (provided it doesn’t get too big) and doesn’t force you to look up defaults or options.

    I agree that the lambdas are ugly but they are necessary. They defer execution of the code until the view is being rendered.

  5. Andrius Chamentauskas says:

    I know they’re necessary when you choose syntax like that with tables or hashes. However with DSL syntax you wouldn’t need the lambdas, as you could save the whole block between `config.tabs do … end` and later execute it against some proxy object that has method `tab` and proxies everything else to controller/view. That way you could avoid lambdas. You could also do same syntax using table syntax if you wanted (or better yet support both syntaxes).

  6. techiferous says:

    @Andrius What a great idea! I would love to get rid of those ugly lambdas. I’ll probably update the syntax sometime soon so that the table cells don’t need to be wrapped in lambdas. I’m also considering giving users an option of using your block DSL if they find the Ruby table unpalatable.

  7. Matt Bauer says:

    Count me in on preferring Andrius’s block DSL far more than the table thing.

  8. Shane Sveller says:

    I would like to chime in just to say that a screencast demo is not always the answer. In my opinion, it is rarely the answer at all. If you can explain through code samples and screenshots, so much the better. Some people don’t enjoy using bandwidth for 30MB+ of video that could’ve been done just as well with less than 1MB of text and images.

  9. nzed says:

    That sounds nice. Are you gonna put html5 option? ()

  10. techiferous says:

    @nzed Yes, there is an html5 option.

  11. techiferous says:

    So I was considering adding syntax that looked like this:

      config.tabs do |t|
        t.tab :home_tab,                   :text => 'Explanation',
                                           :path => '/'
        t.tab :galaxies_tab,               :path => '/galaxies/elliptical_galaxies'
        t.tab :elliptical_galaxies_subtab, :path => '/galaxies/elliptical_galaxies'
        t.tab :spiral_galaxies_subtab,     :path => '/galaxies/spiral_galaxies'
        t.tab :lenticular_galaxies_subtab, :path => '/galaxies/lenticular_galaxies'
        t.tab :planets_tab,                :path => '/exoplanets'
        t.tab :exoplanets_subtab,          :path => '/exoplanets'
        t.tab :rogue_planets_subtab,       :path => '/rogue_planets'
        t.tab :stars_tab,                  :text => request.path =~ /stars/ ? 'Stars!' : 'Stars',
                                           :path => stars_path
        t.tab :hidden_tab,                 :path => '/hidden/always_visible',
                                           :visible => request.path =~ /(hidden|galaxies)/
        t.tab :always_visible_subtab,      :path => '/hidden/always_visible'
        t.tab :always_hidden_subtab,       :path => '/hidden/always_hidden',
                                           :visible => false
        t.tab :disabled_tab,               :path => '/disabled/always_enabled',
                                           :enabled => request.path =~ /(disabled|stars)/
        t.tab :always_enabled_subtab,      :path => '/disabled/always_enabled'
        t.tab :always_disabled_subtab,     :path => '/disabled/always_disabled',
                                           :enabled => false
      end
     
      config.actions {
        'home' => :home_tab,
        'elliptical_galaxies' => :elliptical_galaxies_subtab,
        'spiral_galaxies' => :spiral_galaxies_subtab,
        'lenticular_galaxies' => :lenticular_galaxies_subtab,
        'exoplanets' => :exoplanets_subtab,
        'rogue_planets' => :rogue_planets_subtab,
        'stars' => :stars_tab,
        'misc#always_visible' => :always_visible_subtab,
        'misc#always_hidden' => :always_hidden_subtab,
        'misc#always_enabled' => :always_enabled_subtab,
        'misc#always_disabled' => :always_disabled_subtab
      }

    Although it’s a more popular way to program in Ruby, it’s not as scannable to me as the Ruby table. I also feel strongly about encouraging people to try out new things and experiment, so I’m not going to go to the significant effort of supporting this other syntax. If someone feels strongly about adding this syntax, I’m open to accepting patches to the gem.

  12. Mike Wyatt says:

    this seems very un-ruby, with the repetition. and very un-rails, with no I18n and no named routes.

    this is what I came up with after reading your article and seeing your requirements.

    https://gist.github.com/855882

    uses named routes to simplify mostly everything, I18n to name links, and you can use a block to paint your own anchors

  13. techiferous says:

    @Mike I think you mean un-DRY. The main benefit of DRY is that you don’t have information duplicated in a system. Duplicated info makes for fragile systems because you update one part of the system and forget to update the other. In the case of the Ruby table, there is no real danger of this happening. The repetition is only annoying in the sense that you have to type more. The repetition actually has benefits of producing easier-to-read code. I’ve very intentionally departed from typical ways of programming in Ruby in part to get people to think more creatively about how they program.

    I’m not sure what you mean by no i18n. You can put any Ruby expression in the Ruby table.

    The gist you included is pretty nifty. If that’s all you need to solve your tab problem, then by all means use it and not tabulous. After all, it’s all about solving problems in the most effective way possible.

  14. techiferous says:

    @Andrius Thanks again for pointing out how I could avoid the overuse of lambdas in the Ruby table by wrapping the whole table in a block instead of each cell. I incorporated your advice and released a new version of the gem.

  15. techiferous says:

    @Shane Good point. I created a demo with code samples and screenshots here: http://techiferous.com/2011/03/tutorial-for-adding-tabs-to-rails-using-tabulous/

  16. Chris says:

    I find the tab sytax very disturbing and I hope others don’t go this way. You know something’s wrong when you have to use a rake task to clean up your code.

  17. Chris says:

    Tabulous is awesome though.

  18. techiferous says:

    @Chris I think programmers need to feel free to experiment more and break the rules. I miss the early days of Ruby with _why.

    About using the rake task: you’re absolutely right. Any experienced programmer will likely have that reaction–it’s a design or code smell. Now if you can turn off that part of your programming conscience that’s warning you this is a bad thing and actually use the syntax you may be pleasantly surprised. Or you may hate it. Suspending judgment is a great thing when you come across a new way of doing something.

    One analog is the config/routes.rb file. The routes DSL has been crafted to keep things DRY and terse. However, you need to run ‘rake routes’ in order to get a nice view of all of the routes. In other words, the interface of config/routes.rb is optimized for writing, not reading. I’m experimenting with merging the two into one: a source code file that’s both easily readable and easily writable, with preference on the readability.

  19. Luis Hurtado says:

    I’m getting this error when deploying to Heroku.

    Installing tabulous (1.0.0) /usr/ruby1.8.7/lib/ruby/site_ruby/1.8/rubygems/installer.rb:170:in `install': tabulous requires RubyGems version >= 1.4.0. Try ‘gem update –system’ to update RubyGems itself. (Gem::InstallError)

    What is the recommended fix for this?

    Thanks in advance.

  20. Luis Hurtado says:

    Response: I have vendored the gem an seems that would do.

  21. techiferous says:

    @Luis Thanks for bringing this to my attention. I changed tabulous’s rubygems version dependency and rereleased it. The new version of tabulous now works with Heroku.

  22. John says:

    I am experimenting with tabulous and I find it very easy and nice tool to have. Just wondering, is it possible to have two sets of tabs in one application? For example, one set for admin interface and another one for public.
    Thank you.

  23. techiferous says:

    @John I’m glad you’re finding tabulous useful.

    The way tabulous is designed to handle admin functionality is to have the admin tabs/subtabs mixed in with all the other tabs. Then you only show the admin tabs to admins by putting Ruby code similar to this in the “Visible?” column:

      current_user.admin?

    Will that meet your needs? Or is there some other reason you still need a completely separate set of tabs?

  24. enric ribas says:

    Hello,

    Thanks for taking the time to create tabulous. I appreciate the fact that you are trying something new and in a different way. While it may not work out or people may not like it, I think it’s great that you are experimenting with new formats and design styles.

    Without failed experiments, we would get nowhere new in life, and I’m not convinced this is a failed attempt either.

    Keep up the good work!

  25. techiferous says:

    Thanks, Enric!

Leave a Reply