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">
© [' 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
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.