|Keep It Simple, Stupid|
The MotivationMy first real job assignment out of college was to deliver a suped up front-end for an Access database file implemented in Cold Fusion. That was when a very important lesson that I had been taught reared its ugly head at me: "seperate your interface from your implementation". This mantra has more than one meaning - in this particular scenario it meant don't mix the business rules with the presentation documents.
What did I do wrong? I used ColdFusion to generate key HTML elements - I painted myself in a corner. When the person who wrote the HTML needed to change something, I was the one who did the changing. I had just assigned myself a new job on top of the one I had with no extra pay!
That's what HTML::Template is all about - the ability to keep your Perl code decoupled from your HTML pages. Instead of serving up HTML files or generating HTML inside your Perl code, you create templates - text files that contain HTML and special tags that will be substituted with dynamic data by a Perl script.
If you find yourself lacking as a web designer or generally couldn't care less, you can give your templates to a web designer who can spice them up. As long as they do not mangle the special tags or change form variable names, your code will still work, providing your code worked in the first place. :P
The TagsHTML::Template provides 3 kinds of tags:
and it's corresponding template file:
(p.s. this tutorial discusses CGI below - if you really want to try this out as a CGI script then don't forget to print out a content header. I recommend the ubiquitous header method avaible from using CGI.pm)
A conditional is simply a boolean value - no predicates. These tags require closing tags. For example, if you only wanted to display a table that contained a secret message to certain priviledged viewers, you could use something like:
Notice the quotes around the name attribute for a conditional, these are the only tags that use quotes, and the quotes are necessary.
Also, something very important - that last bit of perl code was not very smart. In his documentation, the author mentions that a maintenance problem can be created by thinking like this. Don't write matching conditionals inside your Perl code. My example is very simple, so it is hard to see how this could get out of hand. Example 2 would be better as:
Example 2 (revised)
Now only one parameter is needed instead of two. $message will be undefined if is_member() returns false, and since an undefined value is false, the TMPL_IF for 'SECRET' will be false and the message will not be displayed.
By using the same attribute name in the TMPL_IF tag and the TMPL_VAR tag, decoupling has been achieved. The conditional in the code is for the message, not the conditional in the template. The presence of the secret message triggers the TMPL_IF. This becomes more apparent when using data from a database - I find the best practice is to place template conditionals on table column names, not a boolean value that will be calculated in the Perl script. I will discuss using a database shortly.
Now, you may be tempted to simply use one TMPL_VAR tag and use a variable in your Perl script to hold the HTML code. Now you don't need a TMPL_IF tag, right? Yes, but that is wrong. The whole point of HTML::Template is to keep the HTML out of your Perl code, and to have fun doing it!
Loops are more tricky than variables or conditionals, even more so if you do not grok Perl's anonymous data structures. HTML::Template's param method will only accept a reference to an array that contains references to hashes. Here is an example:
This might seem a bit cludgy at first, but it is actually quite handy. As you will soon see, the complexity of the data structure can actually make your code simpler.
A Concrete Example part 1So far, my examples have not been very pratical - the real power of HTML::Template does not kick in until you bring DBI and CGI along for the ride.
To demonstrate, suppose you have information about your mp3 files stored in a database table - no need for worrying about normalization, keep it simple. All you need to do is display the information to a web browser. The table (named songs) has these 4 fields:
We will need to display similar information repeatly, sounds like a loop will be needed - one that displays 4 variables. And this time, just because it is possible, the HTML::Template tags are in the form of HTML comments, which is good for HTML syntax validation and editor syntax highlighting.
And that's it. Notice what I passed to the HTML::Template object's param method: one variable that took care of that entire loop. Now, how does it work? Everything should be obvious except this little oddity:
fetchrow_hashref returns a hash reference like so:
This hash reference describes one row. The line of code takes each row-as-a-hash_ref and pushes it to an array reference - which is exactly what param() wants for a Template Loop: "a list (an array ref) of parameter assignments (hash refs)". (ref: HTML::Template docs)
Some of the older versions of DBI allowed you to utitize an undocumented feature. dkubb presented it here. The result was being able to call the DBI selectall_arrayref() method and be returned a data structure that was somehow magically suited for HTML::Template loops, but this feature did not survive subsequent revisions.
Bag of TricksThe new() method has quite a few heplful attributes that you can set. One of them is die_on_bad_params, which defaults to true. By utilizing this, you can get real lazy:
It is not good to blindly rely on die_on_bad_params, but sometimes it is necessary. Just be carefull to note that if someone changes the name of a column, the script will not report an error, and you might let the problem go unoticed for a longer period of time than if you had used die_on_bad_params.
Another extremely useful attribute is associate. When I wrote my first project with HTML::Template, I ran into a problem: if the users of my application submitted bad form data, I needed to show them the errors and allow them to correct them, without having to fill in the ENTIRE form again.
In my templates I used variables like so:
But when the user had invalid data, they would loose what they just typed in - either to the old data or to blank form fields. Annoying!
That's where associate saves the day. It allows you to inherit paramter values from other objects that have a param() method that work like HTML::Template's param() - objects like CGI.pm!
Problem solved! The parameters are magically set, and you can override them with your own values if need be. No need for those nasty and cumbersome hidden tags. :)
loop_context_vars allows you to access 4 variables that control loop output: first, last, inner, and odd. They can be used in conditionals to vary your table output:
No need to keep track of a counter in your Perl code, the conditions take care of it for you. Remember, if you use a conditional in a template, you should not have to test for that condition in your code. Code smart.
A Concrete Example part 2Let's supe up our previous song displayer to allow sorting by the column names. And while we are at it, why bother hard coding the names of the database fields in the template. Let's set a goal to store the database field names in one list and one list only.
Of course, this means that we will have to design a new data structure, because the only way to accomplish our lofty goal cleanly is to use two template loops: one for each row of data, and one for the indidividual fields themselves.
As for sorting - let's just use plain old anchor tags instead of a full blown form. We can make the headers links back to the script with a parameter set to sort by the name of the header: <a href="mp3.cgi?sort=title">Title</a>. Also, let's get rid of the hard coded script name, in case we decide to change the extension from .cgi to .asp, because we can. CGI.pm provides a method, script_name which returns the name of the script, relative to the web server's root.
Here is the final example. If you think the Perl code is a bit convoluted, well you are right, it is. But it is also flexible enough to allow you add or remove database fields simply by changing the @COLS list. This makes it trivial to allow the user to choose which fields she or he sees, an exercise I leave to the reader, as well as adding the ability to sort fields in descending or ascending order.
Last note, notice the use of the built-in <DATA> filehandle to store the template in this script. This allows you to contain your code and template in one text document, but still fully seperated. You can specify a scalar reference in the constructor like so:
The Last Example
See also: HTML::Template and 'perldoc HTML::Template' after you install it.