Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling
 
PerlMonks  

Abstracting away layout details when using large HTML::Template-based sites?

by skx (Parson)
on Dec 08, 2007 at 12:48 UTC ( #655823=perlquestion: print w/ replies, xml ) Need Help??
skx has asked for the wisdom of the Perl Monks concerning the following question:

I've got a large CGI-based application which is in need of a graphical overhaul, only I've run into a snag working out how to do that.

Right now I've got a large collection of HTML::Template files which make up the site, with each "page" of the site coming from a distinct template. Each template contains basically the page-specific stuff and the actual layout code. The latter I'd like to abstract away to make a site redesign more feasible.

(I do use file inclusion to include a common header and footer, but that is about the extent of the consistency and abstraction.)

My initial plan was to split up the site such that each template would only contain the "body" of the output - and have a global template which would define the layout information. To that end I've written a simple test program:

#!/usr/bin/perl -w use strict; use warnings; use HTML::Template; # # Load the layout and a stub page of "content" # my $layout = HTML::Template->new( filename => "./layout.template" ); my $page = HTML::Template->new( filename => "./page.inc" ); # # Insert the body into the layout template # $layout->param( page => $page->output() ); # # Now setup the title # $layout->param( title => "STEVE" ); print $layout->output();

Unfortunately this doesn't work as expected because the nested expansion of the $title in the body template fails to occur.

Does anybody have suggestions on how I could fix this? Or a better idea on how to keep a consistent and maintainable site design over a number of templates?

For reference here are the templates - as you can see I'm trying to only need to setup the $title expansion once, and have it apply to both the "layout" and the "body":

layout.tmpl

<html> <head> <title><!-- tmpl_var name='title' --></title> </head> <body> <!-- tmpl_var name='page' --> </body> </html>

page.inc:

<h2><!-- tmpl_var name='title' --></h2> <p>Imagine content here ..</p>
Steve
--

Comment on Abstracting away layout details when using large HTML::Template-based sites?
Select or Download Code
Re: Abstracting away layout details when using large HTML::Template-based sites?
by ww (Bishop) on Dec 08, 2007 at 16:06 UTC

    I may be missing something here, but this seems to work, at the price of (partially) re-mixing content and layout. Further, from the description of your project, I'm guessing that you will be taking the title from the command line or yet another file, hence the new var, $input.

    #!/usr/bin/perl -w # 655823 use strict; use warnings; use HTML::Template; # new; replace to use from ARGV or some other project file my $input = "STEVE"; my $layout = HTML::Template->new( filename => "layout.tmpl" ); my $page = HTML::Template->new( filename => "page.inc" ); $layout->param( page => $page->output() ); # Now setup title with $input, rather than hardcoding $layout->param( title => "$input" ); # spit out the page print $layout->output();

    When layout.tmpl is modified by moving the line <h2>...</h2> from page.inc, to line 6 here:

    <html> <head> <title><!-- tmpl_var name='title' --></title> </head> <body> <h2><!-- tmpl_var name='title' --></h2> <!-- tmpl_var name='page' --> </body> </html>

    after which, page.inc becomes simply

    <p>Imagine content here ..</p>

    and the output is:

    <html> <head> <title>STEVE</title> </head> <body> <h2>STEVE</h2> <p>Imagine content here ..</p> </body> </html>

    This leaves various issues, however; the most obvious being any case in which your page.inc was more complex -- eg, has body content intended to appear before the <h2...> pair. That's because this writer tackled the question only to begin acquiring more than my current, mimimal knowledge of HTML::Template.

      Thanks for the reply, as you suggest my content will be coming from an external source, along with all other page data. In this case it will be coming from a database.

      Your solution of moving things around doesn't really apply terribly well when I consider how I would be using this in practise I'm afraid.

      For the simple case which I've presented it does work fine, but the general problem of recursively expanding templates doesn't.

      One thing that I notice is if you have a layout.tmpl like this:

      <html> <head> <title><!-- tmpl_var name='title' --></title> </head> <body> <!-- tmpl_include name='page.inc' --> </body> </html>

      Things work! The page.inc containing:

      <h2><!-- tmpl_var name='title' --></h2>

      Is correctly processed via this code:

      my $template = HTML::Template->new( filename => 'layout.tmpl' ); $template->param( title => "Steve" ); print $template->output();

      So suddenly my problem is reduced to including variable files! Unfortunately a similar lack of recusive support means this doesn't work:

      <!-- tmpl_include name='<!-- tmpl_var name='filename' -->'>

      But via a filter I can get this same aim:

      # # Replace ### with environmental variable variable 'page' # sub filter { my ($text_ref ) = shift; my $val = $ENV{'page'}; $$text_ref =~ s/###/$val/g; }; # # Load 'layout.tmpl' - and have that include 'page.inc'. # # THis will expand the following in *both* files! # # <!-- tmpl_var name='title' --> # # $ENV{'page'} = 'page.inc'; my $template = HTML::Template->new(filename => 'layout.tmpl', filter => \&filter ); $template->param( title => "Something here" ); print $template->output();

      I don't know whether to feel pleased or dirty ..

      Steve
      --
        So suddenly my problem is reduced to including variable files! Unfortunately a similar lack of recusive support means this doesn't work: <!-- tmpl_include name='<!-- tmpl_var name='filename' -->'>
        try HTML::Template::Compiled*) - it offers TMPL_INCLUDE_VAR which includes the template file given (it also offers TMPL_INCLUDE_STRING, so that you can pass a template string into another template, but that might cost a bit of performance)
        *) disclaimer: i'm the author.
        I would also suggest HTML::Template::Compiled, but your filter solution is pretty nice too. You can work around the ENV variable by building a filter based on the variable, using an embedded sub:
        sub mk_include_filter { my $page = shift; return sub { my $text_ref = shift; $$text_ref =~ s/###/$page/g; }; } my $template = HTML::Template->new(filename => 'layout.tmpl', filter => mk_include_filter('page.i +nc') );
Re: Abstracting away layout details when using large HTML::Template-based sites?
by sundialsvc4 (Abbot) on Dec 08, 2007 at 23:50 UTC
    Although you might be fumbling a bit getting out of the starting-gate on this one, I do agree with your basic plan.

      Thanks for the comment. It looks like with the environmental variable trick posted above I've got this working nicely.

      I'd rather pass the filename directly to the filter subroutine, but I can't quite get that working properly..

      Steve
      --
Re: Abstracting away layout details when using large HTML::Template-based sites?
by wfsp (Abbot) on Dec 09, 2007 at 08:37 UTC
    Load the title param into $page before you load $page->output into $layout

    #!/usr/bin/perl use strict; use warnings; use Data::Dumper; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; use HTML::Template; # # Load the layout and a stub page of "content" # my $layout = HTML::Template->new( filename => "layout.template" ); my $page = HTML::Template->new( filename => "page.inc" ); $page->param( title => "STEVE"); # <-- new line # # Insert the body into the layout template # $layout->param( page => $page->output() ); # # Now setup the title # $layout->param( title => "STEVE" ); print $layout->output();
    output:
    <html> <head> <title>STEVE</title> </head> <body> <h2>STEVE</h2> <p>Imagine content here ..</p> </body> </html>
    update:
    Add a little more abstraction and feel even more pleased (possibly). :-)
    #!/usr/bin/perl use strict; use warnings; use HTML::Template; my $page_params = { title => "STEVE" }; my $layout_params = { title => "STEVE", page => page($page_params), }; my $layout = HTML::Template->new( filename => "layout.template" ); $layout->param($layout_params); print $layout->output(); sub page { my ($args) = @_; my $page = HTML::Template->new( filename => "page.inc" ); $page->param($args); return $page->output; }

      For my use case I don't know in advance which items to set in the header and which in the page, so this solution doesn't really scale past the small example. (Much like moving the title as suggested by ww initially failed to solve the general problem).

      Steve
      --
        HTML::Template puts together html very nicely. Everything else should be done by your script. I think you are trying to get the template to do too much.

        Your "content will be coming from... a database" so you could add a field which specifies "which items to set in the header and which in the page" or alternatively put that information in your script/config file i.e. one way or another, make a table.

        #!/usr/bin/perl use strict; use warnings; my %config = ( page => { tmpl => q{page.tmpl}, params => [qw{title param1 param2}], }, layout => { tmpl => q{layout.tmpl}, params => [qw{title param3 param4}], }, ); my %all_params = ( title => q{title}, param1 => q{param1}, param2 => q{param2}, param3 => q{param3}, param4 => q{param4}, ); my %page_params = map {$_ => $all_params{$_}} @{$config{page}->{para +ms}}; my %layout_params = map {$_ => $all_params{$_}} @{$config{layout}->{p +arams}}; print Dumper \%page_params; print Dumper \%layout_params;
        output:
        $VAR1 = { 'param2' => 'param2', 'title' => 'title', 'param1' => 'param1' }; $VAR1 = { 'param4' => 'param4', 'param3' => 'param3', 'title' => 'title' };
        Everything is in your code where, imo, it should be and then let HTML::Template do what it is very good at, whacking out nice HTML.

        Using a filter on a template is, imo, rather missing the point but ymmv.

        Perl data structures are the best thing since sliced bread, let Perl take the load. :-)

Re: Abstracting away layout details when using large HTML::Template-based sites?
by pKai (Priest) on Dec 09, 2007 at 14:16 UTC

    There is HTML::Template::Compiled which, while being compatible to HTML::Template, has a dynamic include feature, which does exactly what you expected above from HTML::Template at this point.

    Referring to your code above:

    my $filename = "./page.inc"; # or determine dynamically $layout->param( page => $filename );

    layout.tmpl

    <html> <head> <title><!-- tmpl_var name='title' --></title> </head> <body> <!-- tmpl_include_var name='page' --> </body> </html>

    and title will also be expanded in the page.inc part.

Re: Abstracting away layout details when using large HTML::Template-based sites?
by Rhandom (Curate) on Dec 10, 2007 at 16:24 UTC
    Template::Alloy also provides ways to do what you want. In addition to supporting all of HTML::Template and HTML::Template::Expr, it also allows for using TT tags in your templates as well - and as a plus Template::Alloy is faster than HTML::Template.

    The following code shows inserting dynamic page content in two ways: passing a variable to the PROCESS directive, and the second way is passing raw text to eval (eval simply uses the current template object and parses the text that is passed as a template - if normal cache options are set, this method can still be very fast).
    use Template::Alloy; use POSIX qw(tmpnam); my $file1 = tmpnam; my $file2 = tmpnam; END { unlink $file1 }; END { unlink $file2 }; if (open my $fh, ">", $file1) { print $fh "I am file1 ($file1). title = (<TMPL_VAR name=title>). page = (<TMPL_VAR name=page>). ------------------ <TMPL_PROCESS \$page> ------------------ <TMPL_GET page3.eval> "; close $fh; } if (open my $fh2, ">", $file2) { print $fh2 "I am file2 ($file1). title = (<TMPL_VAR name=title>). "; close $fh2; } my $ht = Template::Alloy->new(filename => $file1, absolute => 1); $ht->param(title => 'The title'); $ht->param(page => $file2); $ht->param(page3 => sub { "This is a dynamically included string.\ntit +le = (<TMPL_VAR name=title>)\n"}); print $ht->output; __END__ prints I am file1 (/tmp/filessyE4j). title = (The title). page = (/tmp/filez6O6TO). ------------------ I am file2 (/tmp/filessyE4j). title = (The title). ------------------ This is a dynamically included string. title = (The title)

    my @a=qw(random brilliant braindead); print $a[rand(@a)];

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://655823]
Approved by BrowserUk
Front-paged by ww
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (14)
As of 2014-10-01 12:00 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    What is your favourite meta-syntactic variable name?














    Results (10 votes), past polls