=pod
=head1 NAME
Bric::HTMLTemplate - Writing HTML::Template scripts and templates
=head1 INTRODUCTION
This document describes how to use Bricolage's HTML::Template templating
system. To get the most out of this document you'll need to have some
familiarity with Bricolage templating using Mason -- see
L and
L for details. I'll try to keep the
overlap between the those documents and this one to a minimum. It also helps
to have an idea of how HTML::Template works outside of Bricolage -- for that
you can refer to HTML::Template's documentation.
=head1 TEMPLATES IN BRICOLAGE
Bricolage uses templates to produce output for stories when they are previewed
and published. Most likely you'll be creating templates to format your stories
as XHTML pages but you can also use HTML::Template to output WML, XML, email
and more.
Templates are created in the same category tree as your stories and media.
When a story is published the category tree is searched for templates starting
in the primary category for the story. The search proceeds up the tree until a
matching template is found.
Bricolage allows you to create three types of templates: element templates,
category templates, and utility templates. Element templates are assigned to a
single element (e.g., Article, Page, Pull Quote, etc.). Category templates are
assigned to the category. Utility templates must be placed into a category,
but otherwise have no relationship to elements or categories.
=head1 SCRIPTS AND TEMPLATES
HTML::Template works by separating Perl code from HTML design. In Bricolage
this results in two types of template files -- F<.pl> script files and F<.tmpl>
template files. The script files contain Perl code. The template files contain
a mix of HTML tags and HTML::Template's C<< >> tags.
This divide between programming and design allows for a division of labor
between programmers and designers. First, the programmer can create a set of
elements and scripts (F<.pl> files). Usually the programmer will also create
some bare-bones example templates (F<.tmpl> files). Next the designers can
edit the template files to match the desired design.
As an additional benefit, if per-category design changes are required, a
designer can create template files in each category that will automatically be
used by the existing scripts in the root category. Of course, the same is true
of script files, but it is much more common to tweak the design by category
than the code.
=head1 CHOOSING A BURNER
Bricolage decides which burner module to use -- Mason, HTML::Template,
Template Toolkit, or PHP -- by looking at the burner setting for the top-level
story element being published. To start using HTML::Template to publish a
story type go to Admin -E Elements, find the story element and set its
burner to HTML::Template.
When you're creating templates you'll also see a pull-down called "Burner".
This determines whether you're creating a Mason F, an HTML::Template
F script, an HTML::Template F or some other templating
architecture template.
=head1 AN EXAMPLE STORY TYPE
We'll examine a simple example story type called "Story". Here's the element
tree for "Story":
Story
- Deck (textbox field)
+ Page (repeatable element)
- Paragraph (repeatable textbox field)
- Pull Quote (repeatable textbox field)
The Story element has one field called Deck and can contain any number of Page
elements. Pages are composed of Paragraph fields and Pull Quote subelements,
both of which can be repeated.
If this doesn't immediately make sense then you should probably go check out
L before continuing -- it's hard to
write templates if you don't understand elements!
=head1 CHOOSING A STRATEGY
Bricolage is an exceedingly flexible system and the HTML::Template burner is
no exception; there are a number of different ways you can write scripts and
templates for the Story element tree. I'll start with what I think is the
easiest to understand and proceed to more complicated approaches pointing out
the advantages and drawbacks along the way.
=head1 STRATEGY 1: ONE SCRIPT, ONE TEMPLATE
For a simple element tree you can often get away with just a single pair of
files -- a script and a template for the top-level element. Here's an example
script file that could be used to setup variables and loops for the example
story above.
=head2 THE SCRIPT: F
# get our template
my $template = $burner->new_template(autofill => 0);
# setup story title
$template->param(title => $story->get_title);
# get deck and assign it to a var
$template->param(deck => $element->get_value('deck'));
# setup the page break variable
$template->param(page_break => $burner->page_break);
# loop through pages building up @page_loop
my @page_loop;
for my $page ($element->get_elements('page')) {
# build per-page element loop
my @element_loop;
foreach my $e ($page->get_elements) {
# push on a row for this element
push @element_loop, { $e->get_key_name => $e->get_value };
}
# push element_loop and a page_count on this loop
push @page_loop, {
element_loop => \@element_loop,
page_count => $e->get_object_order,
};
}
# finish the page_loop
$template->param(page_loop => \@page_loop);
# call output and return the results
return $template->output;
There's a lot going on in the script above so we'll take it step by step. The
first thing the script does is get a new $template object:
# get our template
my $template = $burner->new_template(autofill => 0);
You may be wondering where $burner came from. Every script has access to three
global variables: $burner, $story and $element. The $burner object is an
instance of the Bric::Util::Burner::Template class. The $story and $element
variables are the same as in the Mason system -- check out
L for details.
The new_template() method (like all the $burner method calls) is documented in
L. I've turned off
autofill since we're doing all the hard work ourselves here. With autofill on,
the script would be two lines long which wouldn't teach you much about writing
HTML::Template scripts! More on autofill later.
So, now that we have a template object we'll start by setting up some
variables:
# setup story title
$template->param(title => $story->get_title);
# get deck and assign it to a var
$template->param(deck => $element->get_value('deck'));
# setup the page break variable
$template->param(page_break => $burner->page_break);
The title variable assignment should be fairly self-explanatory -- it gets the
story's title and makes it available to the template. Next the deck field
is retrieved from $element using the get_value() method. Since there can only
be one deck field -- it's not marked as repeatable in the element tree --
it's safe to assign it to a single variable. Finally, a special variable is
setup to paginate the story; C<< $burner->page_break >> returns a value that
can be inserted into the output to break pages.
The next step should look very familiar if you've ever setup a nested loop in
HTML::Template. If you haven't then it probably looks frightening. I'll try to
ease you in slow:
# loop through pages building up @page_loop
my @page_loop;
for my $page ($element->get_elements('page')) {
These lines setup the variables we'll need to build the page_loop. We need to
use a loop for pages since there can be more than one inside the story
element.
The call to C<< $element->get_elements('page') >> returns container elements
of the 'page' type. What's a container element? Well, unfortunately Bricolage
is a bit confused about what to call things internally -- what the external
system refers to simply as an "element" the guts refer to as "container
elements." To make matters worse, fields are internally referred to as "field
elements." That said, calling elements "container elements" is nicely
descriptive since only container elements can I other elements.
Now that the loop is setup it's time to extract the page data:
# build per-page element loop
my @element_loop;
foreach my $e ($page->get_elements) {
# push on a row for this element
push @element_loop, { $e->get_key_name => $e->get_value };
}
First the code creates a new array to hold the element variables from this
page. Next we loop through all the elements in the page with the
get_elements() call -- these elements will be paragraphs and pull quotes. Each
element gets turned into a single row in the C containing a
single variable with the same key name as the element.
For example, let's say we have a page with three paragraphs and a pull quote.
After this loop is finished @element_loop will look something like:
@element_loop = (
{ "paragraph" => "text of paragraph one..." },
{ "pull_quote" => "text of pull quote one..." },
{ "paragraph" => "text of paragraph two..." },
{ "paragraph" => "text of paragraph three..." },
);
As you know from your knowledge of HTML::Template, this is the structure for a
C. Once we've got this structure, we push it onto the outer
C along with the object count.
# push element_loop and a page_count on this loop
push @page_loop, {
element_loop => \@element_loop,
page_count => $e->get_object_order,
};
}
A completed @page_loop for a two-page story might look something like:
@page_loop = (
{
element_loop => [
{ "paragraph" => "text of paragraph one..." },
{ "pull quote" => "text of pull quote one..." },
{ "paragraph" => "text of paragraph two..." },
{ "paragraph" => "text of paragraph three..." },
],
page_count => 1
},
{
element_loop => [
{ "paragraph" => "text of paragraph one..." },
{ "paragraph" => "text of paragraph two..." },
],
page_count => 2
}
);
Which, as you might know, is just the array of hashes of arrays of hashes
structure that HTML::Template expects for nested loops.
# finish the page_loop
$template->param(page_loop => \@page_loop);
# call output and return the results
return $template->output;
Finally, we send the @page_loop data to the template and return the results of
running the template.
=head2 THE TEMPLATE: F
The template for our script matches the variables and loops setup in the
script. It adds a very small amount of HTML formatting just so you can see
where formatting might be added:
>Previous Page>Next Page
Most of this should be pretty self-explanatory but I'll highlight some of the
more interesting bits. First, the template makes use of HTML::Template's
"loop_context_vars" option which is on by default in Bricolage. This allows
the template to make decisions based on the automatic loop variables
C<__first__> and C<__last__>:
This snippet is used to put the title line and deck on the first page only.
This mysterious section sets up the next and previous links:
>Previous Page>Next Page
The use of C<__first__> and C<__last__> should be obvious enough: the first
page doesn't get a previous page link and the last page doesn't get a next
page link. This section also makes use of some helper functions provided to
make linking between pages easier. We could do this without them though;
something like this would produce equivalent results:
Previous Page.html">
Previous Page
Next Page
Although that would only work if your output channel was setup to output files
with names like F and F. The next_page_link() and
prev_page_link() functions will work with any output channel settings.
The final bit of mystery in this template is the use of the magic page_break
variable:
If you remember back in the script this was setup with a call to
C<< $burner->page_break >>. Inserting this value in your output will tell
Bricolage to insert a page break. Also, Bricolage is smart enough not to
output a trailing blank page so you don't have to worry about the spacing
after C in the loop.
=head2 CONCLUSION
This first example has shown how a simple story type can be formatted using a
single script and a single template. The script is responsible for setting up
the variables and loops that the template uses to format the story.
Here's an analysis of this approach:
=over 4
=item Advantages
=over 4
=item *
Everything is in one place. This gives the HTML designer one-stop-shopping for
making changes to the way a story looks. Also, the programmer doesn't have to
hunt around for the right place to add some code for a new feature.
=item *
The script is explicit about what variables and loops are being setup in the
template. This can aid in maintenance of the scripts and templates.
=back
=item Disadvantages
=over 4
=item *
The template is quite complex -- loops within loops can be difficult For less
experienced designers to understand.
=item *
The script is fairly long considering how little work it is actually doing.
=item *
The individual elements are treated directly and thus do not have any
independent formatting associated with them. If another story type element
wants to use Page elements then the same work will need to be duplicated.
=back
=back
=head1 STRATEGY 2: NO SCRIPT, ONE TEMPLATE
As I hinted at above, C's C parameter can do a lot
of work for you. Combined with the default script creation you can often get
away with creating no scripts at all.
=head2 THE DEFAULT SCRIPT
The default script is used if Bricolage needs to publish an element for which
no script file (F<.pl>) exists but for which there is a template file
(F<.tmpl>). It consists of:
return $burner->new_template->output;
Since no options are specified to new_template(), the C parameter
defaults to on. In autofill mode, new_template() automatically fills in
variables and loops for your element tree.
Several types of variables and loops are created by autofill:
=over 4
=item *
A single variable is created for every element with the same key name as the
element. For fields, this variable contains the value of the field.
For container elements, the variable contains the output of the execution of
the script and/or template for that element (more on this in strategy 3).
The C<< >> variable in the previous example is an example of this
type of variable.
=item *
A loop is created for every element with the key name of the element followed
by "_loop". The rows of the loop contain instances of the variables described
above and a "_count" variable for each.
The C<< >> loop is an example of this type of loop.
=item *
A loop called C is created with a row for every subelement. The
values are the same as for the loop above with the addition of a boolean "is_"
variable.
The C<< >> loop used within the
C<< >> loop is this type of loop.
=item *
A variable for the total number of elements with the element key name and a
trailing "_total".
=item *
A variable named for each attribute of the story, including "title", "uri",
description", and so on.
=item *
A variable called "page_break" containing the return value of
C<< $burner->page_break >>.
=back
=head2 THE TEMPLATE: F
The template for use with this strategy is almost exactly the same as for
strategy 1 (sneaky, huh?). The only change is that the autofill code provides
"is_$key_name" variables inside the element_loops to make testing for the type
of the row more obvious and more fool-proof. In STRATEGY 1 a paragraph with
the sole contents "0" wouldn't have been printed! The horror!
>Previous Page>Next Page
=head2 CONCLUSION
This example demonstrates the real power of Bricolage's HTML::Template system.
Here's a breakdown of this strategy:
=over 4
=item Advantages
=over 4
=item *
No code required! You can take the day off if all the designer needs is access
to the data in the story and the elements.
=item *
Consistency. The template designer always gets access to the variables and
loops in the same way. Once they've learned the setup they can create
templates just by looking at the element tree.
=back
=item Disadvantages
=over 4
=item *
Understanding how all this works requires a good understanding of autofill.
Hopefully this document will get you there but sometimes its still nice to be
able to see the code that's really executing.
=item *
The template is still pretty complicated.
=back
=back
=head1 STRATEGY THREE: NO SCRIPTS, MANY TEMPLATES
Sometimes a little extra work can go a long way. If you're building an element
that will be used as a sub-element in a number of trees, then it pays to split
out the functionality into separate pieces. Bricolage supports this by
allowing you to create a script (F<.pl>) and a template (F<.tmpl>) for every
element.
This strategy will deal with just templates, relying on autofill to setup
variables and loops. The next strategy will deal with customizing the scripts
for multiple elements.
=head2 TEMPLATE: F
Here's a revised F to makes a call to the page element
script/template:
>Previous Page>Next Page
Notice that instead of the inner C there's a single C
called "page". This tells autofill to make a call to the element script for
the page element -- F. Of course, as we saw earlier, if this script
doesn't exist then the default script is used:
return $burner->new_template->output;
=head2 TEMPLATE: page.tmpl
Here's the page template that outputs the body of the page:
This should look pretty familiar -- it's exactly the same markup that was in
the original F! Autofill sets up the same loops and variables
whether you're in an original template or a sub-template.
One thing to note is that you can't just move the header- and
footer-generating code into the page template. Since the C<__first__> and
C<__last__> variables are only valid inside the loop in F, they
can't be used in C. This might be addressed in the future but until
then see the next strategy for a solution.
=head2 CONCLUSION
This strategy is a good one when you have elements that will be shared between
template trees. Here's a breakdown:
=over 4
=item Advantages
=over 4
=item *
Element templates can be reused across template trees.
=item *
Element template complexity is reduced -- only a single loop is used in each.
=item *
No code required! (just like Strategy 2)
=back
=item Disadvantages
=over
=item *
The formatting for a story is spread across multiple templates which may make
it harder for designers to make changes.
=item *
Autofill behavior may be hard for programmers to understand. (just like
Strategy 2)
=back
=back
=head1 STRATEGY 4: SCRIPTS AND TEMPLATES
The Bricolage system is all about flexibility. In Strategy 1 you got an
up-close look at a script that handles the entire template setup process.
Fortunately you don't need to do all that work just to add a small
enhancement. For an example, let's fix the problem I mentioned at the end of
Strategy 3 -- the header and footer for the Page element were stuck in
F by their reliance on C<__first__> and C<__last__>.
=head2 TEMPLATE: F
Here's the desired F:
=head2 TEMPLATE: F
And the new Page template:
>Previous Page>Next Page
You'll notice that the element loop is unchanged. The header and footer
expressions are the same except that C<__first__> and C<__last__> are now just
plain C and C. This was done to emphasize that we're not using
HTML::Template's automatic loop variables here.
=head2 SCRIPT: F
The problem here is simple -- we've got some variables in the Story that need
to be made available to the Page. Also, we'd like to do this without having to
do all the work of Strategy 1. Here's the first half of the solution in
F:
my $template = $burner->new_template;
my @pages = $element->get_elements('page');
my $total = @pages;
# build @page_loop by calling run_script with page_count and
# page_total arguments.
my @page_loop;
foreach my $page (@pages) {
push @page_loop, { page => $burner->run_script(
$page,
$page->get_object_order,
$total)
};
}
# replace autofilled page_loop with new one
$template->param(page_loop => \@page_loop);
# return the output
return $template->output;
Basically this script does the same thing that autofill does but only for a
single loop -- C. Additionally, instead of calling C
with just the element parameter it also supplies two arguments, the object
order, which corresponds to a page count, and the total number of pages
(computed from @pages).
=head2 SCRIPT: F
Now that we've setup F to pass parameters to the Page element, we'll
need a script that does something with them.
my ($page_count, $page_total) = @_;
my $template = $burner->new_template;
# setup params
$template->param(first => 1) if $page_count == 1;
$template->param(last => 1) if $page_count == $page_total;
$template->param(page_count => $page_count);
# return output
return $template->output;
As you can see, arguments are passed to scripts just as they are to Perl
subroutines -- through @_. The script uses these parameters to setup the
template params it needs.
=head2 CONCLUSION
This Strategy uses the full set of Bricolage HTML::Template tools we've seen
so far -- scripts, templates, autofill, and run_script().
=over 4
=item Advantages
=over 4
=item *
This style is very flexible -- the programmer can add functionality to the
autofilled content without having to re-invent the wheel.
=item *
Elements that are broken out into discrete scripts and templates can be reused
between element trees.
=item *
In this particular case, bending the rules a bit allows the page formatting to
be more logically grouped and easier to edit.
=back
=item Disadvantages
=over 4
=item *
Requires coding.
=item *
Requires communication between template programmer and template designer since
the variables and loops are somewhat different from the normal autofill setup.
=back
=back
=head1 STRATEGY 5: RELATED MEDIA
So far things have been kept pretty simple; our example story type contains
only text. Now let's add the possibility of including images in our story. The
new tree will look like:
Story
- Deck (textbox field)
+ Page (repeatable element)
- Paragraph (repeatable textbox field)
- Pull Quote (repeatable textbox field)
+ Image (repeatable related media element)
- Caption (textbox field)
The Image element is of the type "Related Media" and has one non-repeatable
field called "Caption". Since it's a related media element it also has the
ability to point to a media document. In this case the template will assume
that referenced media document is an image.
=head2 TEMPLATE: story.tmpl
To keep things simple, we'll start with the template used to format the story
in Strategy 2 with a small addition: