Flexible Templating/Layouts With Rails

Rails’ layout mechanism follows the standard pattern embodied in Sun’s J2EE patterns and familiar to users of Struts and other frameworks. The page template outputs the main content, and it’s automatically wrapped by a generic template (as opposed to the earlier SSI-style paradigm, where each page manually includes headers, footers, etc.). There’s always a complication with this pattern, which is that the page author usually wants to influence some aspects of the wrapping as well.

For instance, you’re writing books.rhtml for an E-Commerce site. You’re obviously going to set the main content – a list of books – but you also want to influence the wrapping, such as: change the page title (in the doc head), highlight “books” in the side menu, etc.

With many frameworks, you have to jump in to an XML file to influence the wrapping, which often means no-one bothers, and usability – not to mention search engine optimisation – suffers as a result…all pages end up with the same title and meta-information, and you’re probably downloading extra Javascript because you’ve made it the same for all pages.

Rails makes it quite easy, and being Rails, there’s no funny config files to mess with…it’s all code. The layout template is parsed after the main content, so the main content can set any variables, and the layout template can make use of them.

For example, standard-layout.rhtml says:

  1. <html>
  3. <head>
  4.     <title><%= @page_title || "My App" %></title>
  5. </head>

Then, each page gets to choose its own title, e.g. books.rhtml:

  1. <% @page_title="Book Bargains" %>
  3. <!-- Main content for books -->
  4. <h1>Lots of Books Today!</h1>
  5. ....

It’s easy to make Javascript (and CSS) flexible. The main layout loops through each specified script:

  1. <% (@scripts||&#91;&#93;).each do | script | %>
  2.     <script type="text/javascript" src="/javascripts/<%= script %>.js"></script>
  3. <% end %>

(Some people might prefer Rails’ script_include tag instead of manual script tags.)

Then, the page can optionally indicate the Javascripts it needs

  1. <% @page_title="Book Bargains" %>
  2. <% @scripts = &#91;"dojo", "util", "books"&#93; %>
  4. <!-- Main content -->

Bitten By Rails Pluralization/Inflection

If you’ve heard DHH or other Rails enthusiasts introducing Rails, they’ll often point to the neat inflection module, which maps back and forth between plurals and singular forms, among other things. Even if not the most killer thing about Rails, it’s an excellent introductory topic as it quickly conveys the humane nature of the Rails API as well as the principle of Convention Over Configuration.

It’s also the kind of thing where you think “These exception cases probably won’t happen to me…how likely am I to be coding Octopi, Sheep, or Oxen” (unless you work in the underwater farm sector). Actually, it turns out exceptions do happen more than I at first expected and are generally handled in a clean way. But how about exceptions that can’t be handled?

Where the inflector isn’t smart enough, I’ve seen the advice (e.g. in PragRails) to override ActiveRecord.set_table_name. e.g.

  1. class Sheep < ActiveRecord::Base
  2.    set_table_name "sheep"
  3. end
  4. [/ruby]
  6. But today I had a problem. My class, "Fave", was leading to an error message about class "Fafe" not found, while loading the fixture. After some bemused grepping, I realised that it was due to this pluralisation issue. Seems that the fixture loader performs some singularization - here's an extract from fixtures.rb:
  8. [ruby]
  9.     @class_name = class_name ||
  10.                   (ActiveRecord::Base.pluralize_table_names @table_name.singularize.camelize : @table_name.camelize)
  11. [/ruby]
  13. **Overriding set_table_name doesn't solve this problem**. Which is surprising given the usual advice, so maybe I missed something!!! The solution that worked for me was to <a href="http://wiki.rubyonrails.com/rails/pages/Turn">configure the Inflection module</a>. I now have this in my config/environment.rb:
  15. [ruby]
  16. Inflector.inflections do |inflection| inflection.irregular "fave", "faves"
  17. end

Now that Rails’ notion of grammar has been redefined, the set_table_name can be removed as it’s redundant.

Side note: Since people are often intrigued by the pluralization algorithm but probably not enough to hunt down the code, here it is – taken from active_support/inflections.rb (which confirms why “Faves” became “Fafe”):

  1. inflect.plural(/$/, 's')
  2.   inflect.plural(/s$/i, 's')
  3.   inflect.plural(/(ax|test)is$/i, '1es')
  4.   inflect.plural(/(octop|vir)us$/i, '1i')
  5.   inflect.plural(/(alias|status)$/i, '1es')
  6.   inflect.plural(/(bu)s$/i, '1ses')
  7.   inflect.plural(/(buffal|tomat)o$/i, '1oes')
  8.   inflect.plural(/([ti])um$/i, '1a')
  9.   inflect.plural(/sis$/i, 'ses')
  10.   inflect.plural(/(?:([^f])fe|([lr])f)$/i, '12ves')
  11.   inflect.plural(/(hive)$/i, '1s')
  12.   inflect.plural(/([^aeiouy]|qu)y$/i, '1ies')
  13.   inflect.plural(/([^aeiouy]|qu)ies$/i, '1y')
  14.   inflect.plural(/(x|ch|ss|sh)$/i, '1es')
  15.   inflect.plural(/(matr|vert|ind)ix|ex$/i, '1ices')
  16.   inflect.plural(/([m|l])ouse$/i, '1ice')
  17.   inflect.plural(/^(ox)$/i, '1en')
  18.   inflect.plural(/(quiz)$/i, '1zes')
  20.   inflect.singular(/s$/i, '')
  21.   inflect.singular(/(n)ews$/i, '1ews')
  22.   inflect.singular(/([ti])a$/i, '1um')
  23.   inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '12sis')
  24.   inflect.singular(/(^analy)ses$/i, '1sis')
  25.   inflect.singular(/([^f])ves$/i, '1fe')
  26.   inflect.singular(/(hive)s$/i, '1')
  27.   inflect.singular(/(tive)s$/i, '1')
  28.   inflect.singular(/([lr])ves$/i, '1f')
  29.   inflect.singular(/([^aeiouy]|qu)ies$/i, '1y')
  30.   inflect.singular(/(s)eries$/i, '1eries')
  31.   inflect.singular(/(m)ovies$/i, '1ovie')
  32.   inflect.singular(/(x|ch|ss|sh)es$/i, '1')
  33.   inflect.singular(/([m|l])ice$/i, '1ouse')
  34.   inflect.singular(/(bus)es$/i, '1')
  35.   inflect.singular(/(o)es$/i, '1')
  36.   inflect.singular(/(shoe)s$/i, '1')
  37.   inflect.singular(/(cris|ax|test)es$/i, '1is')
  38.   inflect.singular(/([octop|vir])i$/i, '1us')
  39.   inflect.singular(/(alias|status)es$/i, '1')
  40.   inflect.singular(/^(ox)en/i, '1')
  41.   inflect.singular(/(vert|ind)ices$/i, '1ex')
  42.   inflect.singular(/(matr)ices$/i, '1ix')
  43.   inflect.singular(/(quiz)zes$/i, '1')
  45.   inflect.irregular('person', 'people')
  46.   inflect.irregular('man', 'men')
  47.   inflect.irregular('child', 'children')
  48.   inflect.irregular('sex', 'sexes')
  49.   inflect.irregular('move', 'moves')
  51.   inflect.uncountable(%w(equipment information rice money species series fish sheep))

Must Have 5 Years “Ruby on Rails” Experience

If there’s one certainty in this industry, it’s the ongoing hilarity of job ads. David Heinemeier Hansson found a job ad that mentions “Rails” in the same breath as “enterprise” and “whitepaper”, and “struts” in the same breath as “latest and greatest”:

Should have ideally 5 years experience with all of the latest and greatest tools out there. The more the better. J2EE, struts, Ruby on Rails, Websphere, etc. Should have experience constructing Enterprise wide Java solutions from whitepapers.

“Why yes, I just pushed the Rational Rose Activationate button and Rose generated the Enterprise wide Java solution for me, along with the whitepaper. I didn’t even have to tell it anything first. Does that count?”

David’s reaction:

Rails has truly broken through the awareness barrier when its included as part of a job application that’s the antithesis of everything Rails is about. Clueless recruiters of the world, I salute you!

The “Must have 5 years Java” requirement, at a time when Java was barely out of the Sun Labs, have become legendary. There can be no finer way to attract dishonest candidates than to construct an infinitely high hurdle. I’ll donate a Pragmatic Rails text to whoever spots the first serious (as in seriously misguided) Rails equivalent.