DRYML, meet ActiveRecord

Posted by Tom on 2006-11-11

DRYML has a nifty little feature called implicit context. Despite the title of this post, this feature has got nothing directly to do with ActiveRecord. Implicit context makes it a lot easier to populate your pages with data, and in Rails that means ActiveRecord models. So this post is about getting your models into your views.

For those that want all the details, we’ll do the whole thing from scratch: create the Rails app, install Hobo, create some models… For those who don’t, you can skip the stuff between the two rules


<skippable>

Let’s get started with a fresh Rails app and get Hobo installed:

$ rails hobo_demo
$ mysqladmin create hobo_demo_development
$ cd hobo_demo
$ ./script/plugin install svn://hobocentral.net/hobo/trunk
$ ./script/generate hobo

…then create some models, populate with some data. Hmm, I guess we need to pick some kind of domain to model. How about a simple to-do list app? [Groan] OK but it’s a cliché for a reason - simple, familiar… Let’s move on :-)

To start we’ll just have TodoLists and Tasks, where a TodoList has many Tasks.

$ ./script/generate model todo_list
$ ./script/generate model task

Now let’s get those tables created – edit the two migrations so the create tables look like these:

create_table :todo_lists do |t|
  t.column :name, :string
end

create_table :tasks do |t|
  t.column :name, :string
  t.column :todo_list_id, :integer
end

Declare the relationships in the models:

app/models/task.rb

class Task < ActiveRecord::Base
  belongs_to :todo_list
end

app/models/todo_list.rb

class TodoList < ActiveRecord::Base
  has_many :tasks
end

Then just a quick

$ rake migrate

…and we’re good to go. Gotta love Rails eh?

If you’re following along you might want to pause here to get some data in there. Personally I’d use ./script/console but you might prefer scaffolding, your MySQL GUI…

</skippable>


(Welcome back to the skimmers :-) What you missed: we’re working with a skeleton app with a TodoList model that has a name and has_many :tasks and a Task model that has a name and belongs_to :todo_list)

OK lets create a view of a single list

$ ./script/generate controller todo_lists

app/controllers/todo_lists_controller.rb

class TodoListsController < ApplicationController

  def show
    @this = TodoList.find(params[:id])
  end

end

@this?? That’s kind of unconventional isn’t it? The implicit context in a DRYML template is held in a variable this (actually it’s not a variable, it’s a method, but don’t worry about it). To set the context for the whole page, we assign to @this in the controller.

Now the view. We can access the context as this in a regular ERB scriptlet.

app/views/todo_lists/show.dryml

<h1>Todo List: <%= this.name %></h1>

<ul>
  <% this.tasks.each do |task| %>
    <li><%= task.name %></li>
  <% end %>
</ul>

Now lets clean that up with some DRYML goodness. First we’ll use <repeat>, which is part of the core taglib (available everywhere) instead of that loop. Let’s see the code first, and then go through how it works

app/views/todo_lists/show.dryml

<h1>Todo List: <%= this.name %></h1>

<ul>
  <repeat attr="tasks">
    <li><%= this.name %></li>
  </repeat>
</ul>

Starting to look cleaner! Two things to notice. First: we gave repeat just the name of the attribute we were interested in (tasks). It implicitly found that collection in the current context (the current value of this). We are therefore iterating over this.tasks. In other words, you just say “how to get there from here”: the page is displaying a TodoList, so iterate over the tasks. Nice and easy.

(Aside: We use the term ‘attribute’ here in the Ruby sense, as in, say, attr_reader. Unfortunately, in a mark-up setting that term conflicts confusingly with XML attributes. We might change this name. What else could you call the attributes of a model object? Properties? Fields?)

The second thing to notice is that inside the repeat, we’re using this again, only now it refers to the individual task. repeat sets the context to each item in the collection in turn. If you’ve ever worked with XSL-T, this idea may be familiar.

Notice now that both the scriptlets that display the name are identical. Bad programmer! Don’t repeat yourself! No problemo:

app/views/todo_lists/show.dryml

<def tag="name"><%= this.name %></def>

<h1>Todo List: <name/></h1>

<ul>
  <repeat attr="tasks">
    <li><name/></li>
  </repeat>
</ul>

Note that the name tag declares this as an attribute. Any tag that’s going to use this should declare so in this way, even though we never actually give this as an attribute when we use the tag.

Next we should move that <def> to the global taglib (application.dryml, which lives in app/views/hobolib). While we’re at it, <ul>/<li> lists tend to crop up all over the place, right? Bad programmer! Don’t repeat yourself! Let’s make a tag for those lists.

app/views/hobolib/application.dryml

<def tag="name"><%= this.name %></def>

<def tag="ul_for">
  <ul>
	<repeat>
	  <li><tagbody/></li>
	</repeat>
  </ul>
</def>

The ul_for tag expects the context to be a collection. Here’s how we use it:

app/views/todo_lists/show.dryml

<h1>Todo List: <name/></h1>

<ul_for attr="tasks"><name/></ul_for>

You can’t ask for much more concise than that! Let’s now improve things a bit. Suppose the individual tasks were each going to have their own page in this app, perhaps displaying notes and such-like. In that case, we’d like that list to be a list of links. Here’s a handy tag that makes it easy to create a link to anything with a name:

app/views/hobolib/application.dryml (fragment)

<def tag="name_link">
  <%= link_to this.name,
              :controller => this.class.name.underscore.pluralize,
              :action => 'show',
              :id => this %>
</def>

There’s a little reflective name-mangling magic going there, but the beauty is that we can forget about that and just use the name_link tag. The change to our page to add our links is trivial:

app/views/todo_lists/show.dryml

<h1>Todo List: <name/></h1>

<ul_for attr="tasks"><name_link/></ul_for>

And we’re done. For today at least. Now – bearing in mind that this page is dramatically simpler than a typical page in a production app – let’s just compare:

Traditional ERB

<h1>Todo List: <%= @todo_list.name %></h1>

<ul>
  <% @todo_list.tasks.each do |task| %>
    <li>
      <%= link_to task.name, :controller => "tasks",
                             :action => "show",
                             :id => task %>
    </li>
  <% end %>
</ul>

…to…

DRYML

<h1>Todo List: <name/></h1>

<ul_for attr="tasks"><name_link/></ul_for>

I don’t know about you but to me that’s like a breath of fresh air. Now imagine that clarity in the context of a vastly more complex production app, and you might see what Hobo is all about.

(And we still haven’t shown you the best bit…)



(edit)