Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

HTATR II: HTML table generation via DWIM tree rewriting

by princepawn (Parson)
on Oct 30, 2003 at 03:28 UTC ( [id://303188]=perlmeditation: print w/replies, xml ) Need Help??

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; }

Replies are listed 'Best First'.
Re: HTATR II: HTML table generation via DWIM tree rewriting
by jeffa (Bishop) on Oct 30, 2003 at 14:55 UTC
    No offense, but i'd rather just learn a mini-language like Template:
    package Simple::Class; use strict; use warnings; use Class::MethodMaker new_hash_init => 'new', get_set => [qw(age name weight)], ; sub load_data { return [ map { Simple::Class->new({ name => $_, age => int(rand(50))+20, weight => int(rand(150))+20, }) } qw(bob bill brian babette bobo bix) ]; } package main; use strict; use warnings; use Data::Dumper; use Template; my $template = Template->new; $template->process(\*DATA,{people => Simple::Class::load_data}) || die $template->error; __DATA__ [% colors = ['white','gray'] %] <table> <tr><th>name</th><th>age</th><th>weight</th></tr> [% FOREACH person = people %] [% PROCESS row person = person %] [% END %] </table> [% BLOCK row %] [% color = colors.shift %] <tr> <td bgcolor="[% color %]">[% person.name %]</td> <td bgcolor="[% color %]">[% person.age %]</td> <td bgcolor="[% color %]">[% person.weight %]</td> </tr> [% colors.push(color) %] [% END %]
    I will agree that i like your template better when it comes to alternating colors. I probably should have handled the color picking inside the Perl code and not the template, but this is a trivial move. As it stands, i am really starting to like the Template mini-language.

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    
      Whether you choose a mini-language over something like Seamstress just depends on who is available to do what. In the second and third companies I worked at, we had some top-flight graphic designers who were also very good at HTML, but they knew nothing about programming.

      They build full pages and handle all elements of consistent look and feel with their HTML tool, usually Dreamweaver, but sometimes Frontpage.

      Now for other shops, Mason and Template are perfect. I don't know what type of shop this might be. I would like to hear about such shops. I think the web designers at etoys got used to Template somehow.

        On my way to work this morning (a good 30 minute commute), i pondered your idea some more. I see what you are doing now and i think it's a pretty cool idea. I guess i had to bang out a reply before i could fully grasp what was going on. However, i wonder if you could utilize XML::Twig instead of rolling your own "rewiter"?

        UPDATE:
        Correction, you aren't rolling your own, you are using HTML::TreeBuilder instead. Sorry about that. :)

        jeffa

        L-LL-L--L-LL-L--L-LL-L--
        -R--R-RR-R--R-RR-R--R-RR
        B--B--B--B--B--B--B--B--
        H---H---H---H---H---H---
        (the triplet paradiddle with high-hat)
        
      I suppose colours are best handled with CSS.

      ESC[78;89;13p ESC[110;121;13p

        I agree:
        [% classes = ['odd','even'] %] [% BLOCK row %] [% class = classes.shift %] <tr> <td class="[% class %]">[% person.name %]</td> <td class="[% class %]">[% person.age %]</td> <td class="[% class %]">[% person.weight %]</td> </tr> [% classes.push(class) %] [% END %]
        But you, the programmer, still have to manage which class to use for which row.

        jeffa

        L-LL-L--L-LL-L--L-LL-L--
        -R--R-RR-R--R-RR-R--R-RR
        B--B--B--B--B--B--B--B--
        H---H---H---H---H---H---
        (the triplet paradiddle with high-hat)
        
Re: HTATR II: HTML table generation via DWIM tree rewriting
by Anonymous Monk on Oct 30, 2003 at 22:25 UTC

    Your template HTML has id attributes in the <tr> elements which you use to find the appropriate node in the template.

    When you unroll the tree, do you remove this attribute? This would be necessary to get valid XHTML, as no two nodes in an XHTML document can have the same id.

    This leads to a more general question of attribute rewriting with this tree-based approach. I'm thinking specifically in terms of using a class attribute instead of putting in HTML-presentation based attributes like BGCOLOR.

    I assume it is done in a similar way to the content, in that you just repeat the row with another 'iteraten' id for each new attribute (easier for page designers, perhaps). The alternative is to have a single sample line, and programmatically replace the class value, as per the element content for each node. This approach has the obvious disadvantage that a designer would only see a table with one row.

    One final thing to consider with the DWIM unrolling function: we often like to fill-out a table with a single row saying "No records returned" or similar when there are no records. This might be a useful extra argument to the DWIM function.

    enough rambling... interesting article

      When you unroll the tree, do you remove this attribute? This would be necessary to get valid XHTML, as no two nodes in an XHTML document can have the same id.
      Wow, I know nothing about XHTML, but you are right: this would be invalid XHTML for that reason... it's easy enough to tack on a counter to the id as I unroll it, if XHTML ever becomes pervasive enough for me to do so.

      The aggravating thing is that I did not see any examples in the XMLC docs for how they uniquify rows when unrolling tables.

      This leads to a more general question of attribute rewriting with this tree-based approach. I'm thinking specifically in terms of using a class attribute instead of putting in HTML-presentation based attributes like BGCOLOR.
      I'm not sure what you mean by "class attribute". I assume this has something to do with CSS? My HTML skillz are rather light --- just enough for rudimentary pages, nothing spectacular.

      I assume it is done in a similar way to the content, in that you just repeat the row with another 'iteraten' id for each new attribute (easier for page designers, perhaps). The alternative is to have a single sample line, and programmatically replace the class value, as per the element content for each node. This approach has the obvious disadvantage that a designer would only see a table with one row.
      Because this paragraph follows of the discussion of "class attributes" from the previous paragraph, I am completely lost in this paragraph and cannot comment.
      One final thing to consider with the DWIM unrolling function: we often like to fill-out a table with a single row saying "No records returned" or similar when there are no records. This might be a useful extra argument to the DWIM function.
      That is true. In more user-friendly setups, where is not just dumping out data this would be a necessary thing. Now the implementation could follow two paths. The superduper table unroller come become even more flexible and accomodate such a message/table row OR we could have this:
      <span id=no_data> <table> <tr> <td>no data</td> </tr> </table> </span> <span id=has_data> <table> ... the real table </table> <span>

      I think just having a message row in the sample table and either detaching or displaying it is a more compact solution though.

      Thank you for the good ideas and discussion.

      DBSchema::Sample

Re: HTATR II: HTML table generation via DWIM tree rewriting
by cbraga (Pilgrim) on Oct 30, 2003 at 16:51 UTC
    And now I'll introduce you to the <readmore> tag. I also should post a constructive comment on your new article so I'll read it now.

    ESC[78;89;13p ESC[110;121;13p

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://303188]
Approved by davido
Front-paged by antirice
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others exploiting the Monastery: (7)
As of 2024-04-23 13:28 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found