In an earlier node I discussed the fact that HTML is a tree and showed how an if-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; }

In reply to HTATR II: HTML table generation via DWIM tree rewriting by princepawn

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post, it's "PerlMonks-approved HTML":



  • Posts are HTML formatted. Put <p> </p> tags around your paragraphs. Put <code> </code> tags around your code and data!
  • Titles consisting of a single word are discouraged, and in most cases are disallowed outright.
  • Read Where should I post X? if you're not absolutely sure you're posting in the right place.
  • Please read these before you post! —
  • Posts may use any of the Perl Monks Approved HTML tags:
    a, abbr, b, big, blockquote, br, caption, center, col, colgroup, dd, del, details, div, dl, dt, em, font, h1, h2, h3, h4, h5, h6, hr, i, ins, li, ol, p, pre, readmore, small, span, spoiler, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, tt, u, ul, wbr
  • You may need to use entities for some characters, as follows. (Exception: Within code tags, you can put the characters literally.)
            For:     Use:
    & &amp;
    < &lt;
    > &gt;
    [ &#91;
    ] &#93;
  • Link using PerlMonks shortcuts! What shortcuts can I use for linking?
  • See Writeup Formatting Tips and other pages linked from there for more info.