Brian's Waste of Time

Fri, 25 Feb 2005

Rails for Struts-ters, Part 2: The Views

We looked at the basics of controllers for Ruby on Rails. Now lets take a look at rendering stuff. The default view technology for most Struts apps is JSP, which works reasonably well. The default renderer for Rails is ERB.

An ERB template would be named foo.rhtml and might look something like:

<h1>RSVP</h1>
<p>Just to make sure we've got it right, you are?</p>
<table>
<% for invite in @invitations %>
  <tr>
    <td>
      <b><%= link_to invite.name , :action => 'verify_invitation', :id => invite.id %></b>
    </td>
  </tr>
<% end %>
</table>

Which nicely highlights a big potential beef -- there are scriptlets there! Argh, woe, agony. Well, maybe. You can form your own opinion, but lets take a look at it. Let's look at the scriptlet used here:

<% for invite in @invitations %>
  ...
<% end %>

This loops through invites in the instance var @invitations. The instance var is defined in the controller (I am trying to follow DHH's advice here, I would prefer to expose it via a helper, personally). The equivalent jsp would be:

<%@ taglib uri="/tags/struts-logic" prefix="logic" %>
...
<logic:iterate name="invite" property="invitation">
  ...
</logic:iterate>

Disturbingly similar for a very similar thing. The big difference being that the erb template uses ruby, whereas the jsp defines a domain specific language via tag libraries. The JSP lets you drop into Java, but that is feared-and-not-to-be-spoken-of, mostly because people have a bad habit of putting application logic in JSP's, which was the ASP and PHP model (from which JSP was copied, go figure).

Now, RoR supports the equivalent of tag libraries, however, just to make things more fun, look at the lines inside the loop:

<tr>
  <td>
    <b><%= link_to invite.name , :action => 'verify_invitation', :id => invite.id %></b>
  </td>
</tr>

The equivalent JSP/Struts would be:

<%@ taglib uri="/tags/struts-html" prefix="html" %>
...
<tr>
  <td>
    <b>
      <html:link action="/VerifyInvitation" paramId="id" paramName="invite" paramProperty="id">
        $invite.name 
      </html:link>
    </b>
  </td>
</tr>

The Struts HTML taglib uses a jsp taglib DSL again to define links within the application, in this case Rails uses a Helper named link_to which is built in to the rails framework (Actionpack specifically). Helpers can be defined in several places. For helpers which are to be globally available, defining them on the application_helper is natural, such as:

# The methods added to this helper will be available to all templates in the application.
module ApplicationHelper
  
  # roles == list of roles required to see text
  def secure(text, roles = [])
    user = @session['user']
    return '' unless user
    if roles.all? { |role| user.roles.map {|r| r.name }.include? role  }
      return text
    else
      return ''
    end
  end
  
end

Methods defined on the ApplicationHelper are exposed as helpers to all views in the application. This example defines a single helper, secure which will return (and hence render) the text passed as the first argument if and only if the logge din user is in all of the roles passed in the array for the second argument, so it would be used like:

<%= secure link_to('Accepted Guests', :controller=>'rsvp', :action=>'accepted'), ['admin'] %>

Which will render a link to an action which lists the guests who have accepted invitations if the logged in user is in the role admin.

In addition to defining helpers ~globally, you can define per-controller helpers in a [ControllerName]Helper module, which will be automatically mixed into the controller it is executing for, so it will have access to that controllers scope. Helper modules can be explicitely mixed into a controller view the helper function, in addition to the implicit helper module. Finally, you can expose methods on the controller itself as helpers via the helper_method and helper_attr functions. Basically, you can define helpers in ways that are convenient for you. You can also declare that you need helpers, using the helper function directly in the template, which is analogous to declaring that you need a tag library in JSP.

That is a basic overview, now for more fun stuff, like form handling. I won't go into explaining Struts ActionForms, you presumably know all about them already. Rails places form parameters into an @params instance var on the controller. It supports structured form parameters, like the EL properties in Struts, but via more-natural-for-ruby hashes, take for example the following form:

<form action="<%= url_for :action => 'register', :controller => 'rsvp' %>" >  
  <table>
    <% @invite.number_of_seats.times do |slot| %>
    <tr>
      <td>Guest Attending:</td>
      <td>
          <input  type="text" 
                  name="invite_names[<%= slot %>]" 
                  length="30" 
                  <%= "value='#{@invite.split_names[slot]}'" %>/>
      </td>
      <td>
        <input type="radio" name="dinner[<%= slot %>]" 
               checked="true" value="chicken" /> <b>Chicken</b> or 
        <input type="radio" name="dinner[<%= slot %>]" 
               value="seafood" /> <b>Salmon</b>
      </td>
    </tr>
    <% end %>
    <tr>
      <td> </td>
      <td><input type="submit" value="Continue"/>
      <td> </td>
    </tr>
  </table>
</form>

An invitation has a specified number of seats attached to it (ie, if you invite a family of four, it is four, a couple it is two). We loop this number of times:

<% @invite.number_of_seats.times do |slot| %>
  ...
<% end %>

Assigning the index of the current seat to slot. Now, with this we build up the input names, such as:

<input type="radio" name="dinner[<%= slot %>]" 
       checked="true" value="chicken" /> <b>Chicken</b> or 
<input type="radio" name="dinner[<%= slot %>]" 
       value="seafood" /> <b>Salmon</b>

where the name of the parameter is dinner[<%= slot %>], so we'll have things like dinner[1] and dinner[2]. On the controller side

def register
  names = @params['invite_names']
  names.each do |key, name|
    if name and name != ''
      u = User.new('login' => name.downcase.strip, 'name' => name.strip)
      u.attending = true
      u.meal_preference = @params['dinner'][key]
      u.save
      @session['user'] = u unless @session['user']
    end
  end
  invite = Invitation.find(@session['invite_id'])
  invite.accepted = true
  invite.save
  redirect_to :action => 'thank_you' and return
end

we'll pull those out the indexed dinners (indexed the same as the user names ;-)

names.each do |key, name|
  ...
  u.meal_preference = @params['dinner'][key]
  ...
end

Note that names is a hash, so names.each will include the key and the value. In this case the key is an number, but it is not in fact ordered, it is like a Map. Despite iterating over names (to pull out the other info), we are going back to @params for the dinner hash and looking up values in it. I picked this example as it is somewhat complicated, most often you don't need to do much this fancy.

Now, JSP 2.0 introduced JSP 2.0 Tag Libraries, which are basically global macros. It seems that the Struts mail reader example app doesn't use them, but we'll assume you know about them. They are a nice way of thinking about Rails partials. Partials are basically little included snippets for things that are likely reused, or complicated, etc. Take for example a snippet for rending a table of things in a gift registry:

<% gifts = gift_table %>
<table>
<% for gift in gifts %>
  <tr>
    <td><%= link_to gift.name, :action => 'show', :id => gift.id %></td>
    <% unless gift.being_given? %>
      <td>
        <b><%= link_to 'Give this Gift', :action => 'give', :id => gift.id %></b>
      </td>
    <% end %>
  </tr>
<% end %>
</table>

which is used here:

<h2>Gifts Not Yet Selected</h2>
<%= render_partial 'gift_table', @gifts.find_all {|g| not g.being_given? }%>
<h2>Gifts Being Given</h2>
<%= render_partial 'gift_table', @gifts.find_all {|g| g.being_given? }%>

The partial takes a single argument named gift_table which is a collection of gifts to be rendered in a table. There is actually a convenience function for this exact use case (passing a collection in), but I didn't see it when I wrote this, so didn't use it, oops. The render_partial helper in the template passes in the collection to be assigned to the name 'render_partial'. Works nicely.

In JSP 2 tag file format, this might look something like (table.tag, totally untested and unvalidated, and just as pseudo-template ;-)

<%@ taglib uri="/tags/struts-logic" prefix="logic" %>
<%@ attribute name="gifts" required="true" %>

<table>
<logic:iterate name="gift" property="gifts">
  <tr>
    ...
  </tr>
</logic:iterate>
</table>

and be called via

<%@ taglib prefix="gifts" tagdir="WEB-INF/tags/gifts/" %>
...
<gifts:table gifts="unselectedGifts" />
<gifts:table gifts="selectedGifts" />

Final thing to look at in views is layouts. The quasi-official tool for this in Struts is Tiles, but I prefer Sitemesh, so will use a sitemesh example. Rails has Layouts, which are the same conceptually as Sitemesh decorators. Here is one

<html>
<head>
  <title>Brian and Joy's Wedding</title>
  <link href="/stylesheets/scaffold.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <%= render_menu %>
  <%= @content_for_layout %>  
</body>
</html>

The layout gets wrapped around any rendered output, with the output going at the <%= @content_for_layout %> point. The easiest way to use a layout for all views is to just have a layout named application.html, and it will be used implicitely. You can explicitely use a layout by declaring it in the controller, a la

class GiftController < ApplicationController
  layout 'scaffold'
  before_filter :require_logged_in

which will have it look for scaffold.rhtml to use as a layout. Sitemesh uses an external XML file to configure these, under typical usage, but it is conceptually the same.

RoR 0.10.0 introduced components, which I haven't used yet myself, so cannot comment much on. I suspect they are to be thought of like JSF components. You can investigate these further in the component documentation.

0 writebacks [/src/java] permanent link