Integrating Template::Recall with the Dancer framework

2014-12-12, James Robson, http://soundly.me

Since I've convinced myself that the template system I developed long ago is a contender in the template space, I got curious about how to add it to the Dancer framework.

It's so easy to assume that something will be a natural fit when it falls into the same general category. In this case that category is template. Since Dancer uses templates, has the template keyword, and Template::Recall is a template system, I expected it to be a straightforward integration.

Of course, I was wrong. Template::Recall thinks about templates in a completely different way than is 'standard'. Basically it's loathe to allow any logic into the template.

This fundamental difference in design posed a problem, as I found out. The template call is expecting a traditional logic-based template. Meaning that it is expected to be called exactly once in a route handler.

From the implementation side, this corresponds to implementing a single render() method. Because Recall is sort of the inverse of traditional templating, it calls its render() method multiple times, not just once, to render sections. So you see the problem.

I started my implementation by looking for the Simple template that comes with Dancer. It seemed like the most obvious place to start. I found it here on my system (OSX):

% mdfind -name Simple.pm | ack Dancer
/Library/Perl/5.16/Dancer/Session/Simple.pm
/Library/Perl/5.16/Dancer/Template/Simple.pm

I copied Template/Simple.pm to Template/Recall.pm and then begain sniffing through the code to see how it works. First thing to note is that it subclasses Dancer::Template::Abstract, and at the minimum you need to implement the render() method.

Here's where I went wrong. I didn't understand that you could only use template once per route. So I incorrectly assumed that in Dancer::Template::Recall::render() I could just do a few things to the parameters then call and return Template::Recall::render().

I had a working template, and tried it out, doing something like this:

get '/' => sub {
    template 'mytemplate.tr', { section => 'head', tokens => { title => 'helowrld' } };
    template 'mytemplate.tr', { section => 'content', tokens =>  { message => 'helowrld' } };
    template 'mytemplate.tr', { section => 'foot' };
};

Knowledgeable Dancers will know what happens above. The browser renders the foot section to the browser.

That was when the problem became clear to me. After some travail, and thinking that maybe I just couldn't get the two designs to work together, I hit on the following solution:

Since Recall wants to leave all logic in the user code rather than the template system, and Dancer expects the opposite, I had to use callbacks.

In order make this concept easier to use, I created a second class Dancer::Template::Recall::View. Here it is in its entirety:

package Dancer::Template::Recall::View;

sub new {
    my $class = shift;
    return bless {}, $class;
}
sub section {
    my ($self, $section, $data, $sub) = @_;

    if (!exists $self->{sections}) { $self->{sections} = [] };

    push @{$self->{sections}}, { section => $section, data => $data, sub => $sub };
}
1;

Remember that Recall wants to call its render() multiple times on multiple sections in the template. Using View makes setting up the sections -- with callbacks if necessary -- cleaner and more object oriented.

Probably before going much further, you should see the template. The items marked with [= and =] are sections and under those sections you have replaceable tokens, marked by [' and '].

[= head =]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">
<html>
  <head>
    <title>[' title ']</title>
  </head>
  <body>
    <div id="header">
      <a href="/index.html" class="logo" alt="Home Page"></a>
      <h1 class="headline">[' title ']</h1>
    </div>

[= content =]
    [' message ']
[= prod =]
<table>
[= prodrow =]
 <tr>
    <td>[' product ']</td>
    <td>[' description ']</td>
    <td>[' price ']</td>
</tr>
[=prodend=]
</table>

[= footer =]
    <div id="footer">
      <div id="copyright">
        &copy; [' copyright ']
      </div>
    </div>
  </body>
</html>

So with the above template saved as test.tr and the following route, you can see how we use the View object.

get '/:name' => sub {

    # array of data to output in 'prodrow'
    my @prods = (
            'soda,sugary goodness,$.99',
            'energy drink,jittery goodness,$1.99',
            'green tea,wholesome goodness,$1.59' );

    my $v = Dancer::Template::Recall::View->new();

    $v->section('head', { title => 'helowrld (View)' });
    $v->section('content', { message => 'HELO ' . params->{name} } );
    $v->section('prod');
    $v->section('prodrow', \@prods,
        sub {
            my ($tr, $data, $out) = @_; # passed in from Recall
            foreach my $line (@$data) {
                my @row = split /,/, $line;
                $$out .= $tr->render('prodrow',
                            {   product => $row[0],
                                description => $row[1],
                                price => $row[2] } );
            } # foreach
        });
    $v->section('prodend');
    $v->section('footer', { copyright => 'right now' });

    template 'test.tr', { view => $v };
}

And this renders to

Figure 1

Mainly, I want to talk about this section of the code above.

$v->section('prodrow', \@prods,
    sub {
        my ($tr, $data, $out) = @_; # passed in from Recall
        foreach my $line (@$data) {
            my @row = split /,/, $line;
            $$out .= $tr->render('prodrow',
                        {   product => $row[0],
                            description => $row[1],
                            price => $row[2] } );
        } # foreach
    });

The basic use of View is to call section() with the section tag and an optional data reference, i.e.

$v->section('head', { title => 'helowrld (View)' });

This renders pretty much how you would expect, interpolating the tokens from the data reference with the template string.

When you have to do more complex things, you need to use a callback. This basically means that you pass in a sub {} as the final parameter to section().

In the sub you define, the first line must always be

my ($tr, $data, $out) = @_; # passed in from Recall

The $tr parameter is the underlying Recall object, which allows you to directly call the Template::Recall::render() method.

$data is a reference to \@prods, the second parameter passed to section().

$out is a reference to the rendered template string that will ultimately be returned from the template. Note to actually assign to it you must de-reference, i.e. using $$out.

Other than that, what you do with sub {} is up to you. In this case we process the product data, and assign the rendered rows (<tr>) to $out. Or, $$out, to be accurate. ;)

Notice that the order in which you call section() is important. View basically builds an array of section data, and this will be processed top-down. You get to decide which sections come first (and that doesn't have to exactly correlate to the sections in the template file), but calling them in the right order is up to you.

Which leads us to the second way you can create a view, using just a data structure:

my $v = [
    { 'head' => { title => 'helowrld' } },
    { 'content' => { message => 'HELO ' . params->{name} } },
    { 'prod' => undef },
    { 'prodrow' => [
        \@prods,
        sub {
            my ($tr, $data, $out) = @_; # passed in from Recall
            foreach my $line (@$data) {
                my @row = split /,/, $line;
                $$out .= $tr->render('prodrow',
                            {   product => $row[0],
                                description => $row[1],
                                price => $row[2] } );
            } # foreach
        }
    ] }, # prodrow
    { 'prodend' => undef },
    { 'footer' => { copyright => 'right now' } }
];

template 'test.tr', { view => $v };

Here $v is just an anonymous array. Looks quite a lot like what we're doing with View, right? Well because it is. That's more or less all View does -- assemble sections in a top-down, ordered way. It just gives us a more object oriented representation of that. The above will render the exact same template.

I'm not certain how serious I am about turning Recall into an actual Dancer template. It was more a way to get my feet wet with Dancer, and seemed like a good exercise in lieu of having any real problems to solve.

You get the same performance (and in my opinion idiomatic) benefits Recall provides under Dancer, which is one reason for doing it, I suppose.