-like programming contruct could be effected
by simple tree operations.
The current node continues the exploration of tree-based HTML generation and
simultaneously provides a discussion of the merits of DWIM-programming with
2 relevant examples.
Unrolling a table: the Seamstress way
The very first thing I did was simply go at this problem in a direct manner.
I had a model and I had some sample HTML as the view. So I just coded up
some
HTML::TreeBuilder subs to unroll the model based on the view.
Here is the model
package Simple::Class;
my @name = qw(bob bill brian babette bobo bix);
my @age = qw(99 12 44 52 12 43);
my @weight = qw(99 52 80 124 120 230);
sub new {
my $this = shift;
bless {}, ref($this) || $this;
}
sub load_data {
my @data;
for (0 .. 5) {
push @data, {
age => $age[rand $#age] + int rand 20,
name => shift @name,
weight => $weight[rand $#weight] + int rand 40
}
}
\@data;
}
Very straightforward: just five rows of tabular data
Here is the view
<table id="load_data">
<tr> <th>name</th><th>age</th><th>weight</th> </tr>
<tr id="iterate">
<td id="name"> NATURE BOY RIC FLAIR </td>
<td id="age"> 35 </td>
<td id="weight"> 220 </td>
</tr>
</table>
Again, pretty simple, we just want to replace our sample table with real rows
of table data.
A program to weave the sample HTML view with the model
use HTML::Seamstress;
# load the view
my $seamstress = HTML::Seamstress->new_from_file('simple.html');
# load the model
my $o = Simple::Class->new;
my $data = $o->load_data;
# find the <table> and <tr>
my $table_node = $seamstress->look_down('id', 'load_data');
my $iter_node = $table_node->look_down('id', 'iterate');
my $table_parent = $table_node->parent;
# drop the sample <table> and <tr> from the HTML
# only add them in if there is data in the model
# this is achieved via the $add_table flag
$table_node->detach;
$iter_node->detach;
my $add_table;
# Get a row of model data
while (my $row = shift @$data) {
++$add_table;
# clone the sample <tr>
my $new_iter_node = $iter_node->clone;
# find the tags labeled name age and weight and
# set their content to the row data
$new_iter_node->content_handler($_ => $row->{$_})
for qw(name age weight);
# push this table row onto the table
$table_node->push_content($new_iter_node);
}
# reattach the table to the HTML tree if we loaded data into some tabl
+e rows
$table_parent->push_content($table_node) if $add_table;
print $seamstress->as_HTML;
So that was a job done the seamstress way. Then I thought that I didnt want
to handcode all of those stereotyped tree operations each time I wanted to
unroll tabular data into a tabular view so I needed an API call that
would do it for me.
Creating a Flexible Subroutine to Abstract the Process of Unrolling Tables
In DWIM programming, one specifies the desired
end and hides
oneself from the work in handling the possibly contorted logic of the
means.
The most common example of this is polymorphic calculation of area:
use Inline::Java; # grin ;-)
Shape[] shapes = new Shape[100];
shapes[0] = new Rectange(45,27);
shapes[1] = new Circle(10);
shapes[2] = new Ellipse(25,13);
shapes[3] = new Quadrilateral(15,20,50,14);
...
double area = 0;
for (int i=0; i < shapes.length; i++)
area += shapes[i].computeArea();
Note how the programmer simply said
computeArea without having
to figure out what type of shape he was dealing with: he simply told the
program to do what he wanted and hid/saved himself from the details.
I will discuss how I made use of DWIM techniques to simplify the a subroutine
to unroll a model and view into a concrete table.
My first cut at this function was OK. Here
is the view (again):
<table id="load_data">
<tr> <th>name</th><th>age</th><th>weight</th> </tr>
<tr id="iterate">
<td id="name"> NATURE BOY RIC FLAIR </td>
<td id="age"> 35 </td>
<td id="weight"> 220 </td>
</tr>
</table>
and here is
is how it
was called:
use HTML::Seamstress;
# load the view
my $seamstress = HTML::Seamstress->new_from_file('simple.html');
# load the model
my $o = Simple::Class->new;
$seamstress->table
(
# tell seamstress where to find <table>, via the method call
# ->look_down('id', $gi_table). Seamstress detaches the table from
+the
# HTML tree automatically if no table rows can be built
# "gi" stands for generic identifier. Most people call "gi"s tags,
+but
# mirod calls them "gi"s so I will too :)
gi_table => 'load_data',
# tell seamstress where to find the <tr>.
# this is the major place where DWIM will come in!
gi_tr => 'iterate',
# the model data to be pushed into the table, row by row
table_data => $o->load_data,
# The way to take the model data and obtain one row.
# If the table data were a hashref, we would do:
# my $key = (keys %$data)[0]; my $val = $data->{$key}; delete $data
+->{$key}
tr_data => sub { my ($self, $data) = @_;
shift(@{$data}) ;
},
# the way to take a row of data and fill the <td> tags
# content_handler() is a Seamstress function which takes
# $id_val and $content as args. It does a ->look_down('id', $id_val
+)
# and sets the content section of the found node (a <td>) to $conte
+nt
td_data => sub { my ($tr_node, $tr_data) = @_;
$tr_node->content_handler($_ => $tr_data->{$_})
for qw(name age weight) }
);
print $seamstress->as_HTML;
So, I wrote a ->table() function which received all the
information
on how to find and iterate the view and model and turned the dummy table in
the HTML into a real table, filled out with model data.
Then I decided that this function should be flexible as a function of whether
the gi_tr argument was a simple scalar or an array ref.
If a simple scalar
was passed in, then that <tr> would serve as the model for all
<tr> in the
table. If an array ref was passed in then each element of the array ref would
serve as a model <tr> in sequence. Practically speaking, this is
the way to
get alternating table rows with different background colors.
Here is the sample HTML that we would use to model alternating table rows:
<table id="load_data" CELLPADDING=8 BORDER=2>
<tr> <th>name</th><th>age</th><th>weight</th> </tr>
<tr id="iterate1" BGCOLOR="white" > #### ONE BGCOLOR
<td id="name"> NATURE BOY RIC FLAIR </td>
<td id="age"> 35 </td>
<td id="weight"> 220 </td>
</tr>
<tr id="iterate2" BGCOLOR="#CCCC99"> ### ANOTHER BGCOLOR
<td id="name"> NATURE BOY RIC FLAIR </td>
<td id="age"> 35 </td>
<td id="weight"> 220 </td>
</tr>
</table>
The only difference in the API call is that
gi_tr => 'iterate',
will have to be changed to
gi_tr => [qw(iterate1 iterate2)],
So that Seamstress can find the list of dummy rows instead of just one row.
but within my code, I have a lot to do to handle the scalar versus array ref
case... unless I can fold both the scalar and arrayref into the same data
structure and treat them the same regardless of which comes in.
My first task was to promote the scalar to a list
and fold the array ref to a list:
sub table {
my ($s, %table) = @_;
my @table_gi_tr = listify $table{gi_tr} ;
...
Voila. Now, regardless of whether the input scalar is a simple scalar or an
array ref, I have an array ref. If I did not have the DWIM-ease of
Scalar::Listify here is what I would have had to write:
my @table_gi_tr = (ref $table{gi_tr} eq 'ARRAY')
? (@$table{gi_tr})
: ( $table{gi_tr})
...err, 3 Perl expressions with more brackets and colons and questions marks
than the Sunday comic versus one succinct DWIM API call?
I will take the Scalar::Listify DWIM version anyday, thank you.
So, now it does not matter to the rest of the subroutine whether a scalar
(one tr) or
array ref (of several tr)
was passed in, because we have turned either into an array.
Now comes potential stumbling block #2: how to cycle through a list of
table rows, repeating the list when we come to the end? The answer:
Tie::Cycle by brian d. foy. We cycle blindly in a
DWIM fashion. We cycle over our 1-element list just as gleefully as we
do our 2-element list of <tr>. Hell, make it 3 or 4 rows, it just don't
matter. cycle, cycle, cycle:
# fold the scalar and aref into an array
my @table_gi_tr = listify $table{gi_tr} ;
# create an array containing HTML::Element pointers to the <tr> or <tr
+>s
my @iter_node = map
{
$table->{table_node}->look_down($ID, $_)
} @table_gi_tr;
# tie a scalar to the list of <tr> so that we can cycle thru 'em
tie $table->{iter_node}, 'Tie::Cycle', \@iter_node;
# iterate through the model data
while (my $row = $table{tr_data}->($table, $table{table_data}))
{
# pick out a <tr> to display the row of model data with:
my $I = $table->{iter_node}; # force the tied data to FETCH in
+to $I
my $new_iter_node = $I->clone; # clone the dummy <tr> for templa
+ting
# wont work: my $new_iter_node = $table->{iter_node}->clone
+;
...
}
And so, while we iterate through the row data of the model,
Tie::Cycle keeps feeding us the proper row of the sample view
<tr>s for Seamstress to rip through and load up the row with
model data.
The final subroutine for your DWIM-viewing pleasure: handles simple tables
and those with multiple sample-tr rows with equal poise:
our ($table_data, $tr_data, $gi_td);
sub table {
my ($s, %table) = @_;
my $table = {};
$table->{table_node} = $s->look_down($ID, $table{gi_table});
my @table_gi_tr = listify $table{gi_tr} ;
my @iter_node = map
{
$table->{table_node}->look_down($ID, $_)
} @table_gi_tr;
tie $table->{iter_node}, 'Tie::Cycle', \@iter_node;
$table->{content} = $table{content};
$table->{parent} = $table->{table_node}->parent;
$table->{table_node}->detach;
$_->detach for @iter_node;
my $add_table;
while (my $row = $table{tr_data}->($table, $table{table_data}))
{
++$add_table;
# wont work: my $new_iter_node = $table->{iter_node}->clone
+;
my $I = $table->{iter_node};
my $new_iter_node = $I->clone;
$table{td_data}->($new_iter_node, $row);
$table->{table_node}->push_content($new_iter_node);
}
$table->{parent}->push_content($table->{table_node}) if $add_table;
}