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>
Re: Abstracting away layout details when using large HTML::Template-based sites?
by ww (Archbishop) 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.
| [reply] [d/l] [select] |
|
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 ..
| [reply] [d/l] [select] |
|
| [reply] |
|
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.
| [reply] [d/l] |
|
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') );
| [reply] [d/l] |
|
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;
}
| [reply] [d/l] [select] |
|
| [reply] |
|
#!/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. :-)
| [reply] [d/l] [select] |
Re: Abstracting away layout details when using large HTML::Template-based sites?
by pKai (Priest) on Dec 09, 2007 at 14:16 UTC
|
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. | [reply] [d/l] [select] |
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..
| [reply] |
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)];
| [reply] [d/l] |
|
|