Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

CGI.pm HTML-Generation Methods Considered Useful

by friedo (Prior)
on Jul 10, 2004 at 04:40 UTC ( #373307=perlmeditation: print w/ replies, xml ) Need Help??

Dear Monks,

I would like to take some time to address an issue dear to my heart: CGI.pm's HTML-generation methods. Lest you rise up in righteous indignation, let me be clear that I in no way advocate implimenting an entire web application with them. However, it should be pointed out that in certain limited circumstances, they can be exceptionally useful. I wish to present a few tips and tricks for quickly and painlessly generating large amounts of HTML using CGI.pm.

Have you ever had a large data structure (maybe coming from a database, or maybe not) which needed to be converted to HTML quickly? CGI's table methods have a few handy features which can be exploited mercilessly to massage almost any data into a table with a single statement.

  1. A list passed to a table(), Tr() or td() method will be surrounded by the corresponding HTML element.
  2. An array-ref passed to a table(), Tr() or td() method will have every element surrounded by the corresponding HTML element.
  3. A hash-ref passed as the first argument to any method will be used as the HTML attributes for that element.

But you probably knew that already. Let's see how we can take advantage of it.

$html .= $q->table( { -bgcolor => '#ffffff', -border => 1 }, $q->caption('Status Summary'), # beware beautiful nested maps ahead $q->Tr( [ map { my $k = $_; $q->td( [ $k, map { $data->{$k}{$_} } sort keys %{ $data->{$k} } ] ) } sort keys %{ $data } ] ) );

This example comes from a simple status monitor application I wrote. In the example, $data is a hash-reference which points to a two-level hash consisting of a machine name and a series of tests run on that machine. For example, $data->{machine1}{pload} contains the status of the processor load on machine1.

The code generates a simple table with the machines as rows and the tests as columns, and the status data in each table cell. The inner map fills an array reference with items to put in <td> tags. The first item is the outer key, the machine name, in $k The ramaining items are the status of each test for that machine. The outer map then creates an array reference of those rows to pass back to Tr(), which builds the rows and is then passed to table(), completing our HTML. This code will work regardless of the number of machines or tests, as long as the form of the data structure is kept the same.

But we can do better. Suppose we want to highlight a cell if a machine is reporting dangerous processor load? By adding an inline conditional we can optionally pass style elements to the cell based dynamically on the data therein.

$html .= $q->table( { -bgcolor => '#ffffff', -border => 1 }, $q->caption('Status Summary'), # beware beautiful nested maps ahead map { my $k = $_; $q->Tr( $q->td( $k ), map { $q->td( $data->{$k}{$_} eq 'bad' ? { -bgcolor => '#ff0000' } : { -bgcolor => '#ffffff' }, $data->{$k}{$_} ) } sort keys %{ $data->{$k} } ) } sort keys %{ $data } );

Here we have switched from passing array references of items to surround with an HTML element to passing lists of individual elements. This is so we can pass individual hash-refs with style elements to each table cell. The conditional at the beginning of our calls to $q->td() checks to see if the status is 'bad'. If it is, we make the cell background red, if not, we make it white.

These two examples just scratch the surface of what can be done quickly and easily with CGI.pm's nested method calls. The above two examples took less than ten minutes (most of the time spent fixing errant braces and parantheses.) Each example can be expanded; for instance, another conditional could be added to allow a cell datum to be a hyperlink to additional data in certain circumstances. Figuring out a cute way to include the table headers is left as an exercise for the reader.

Caveats: These methods are great for quickly and painlessly generating tables from arbitrary data, using Perl's infinitely powerful list handling capabilities. You should not rely on them to develop a complete web or database application, for which you really should use a templating system of some sort. Also, depending on how complex you get, the code can be very difficult to read, especially for novices who are not familiar with map and/or references.

Code in peace. May the source be with you.
--Mike Friedman

Comment on CGI.pm HTML-Generation Methods Considered Useful
Select or Download Code
Re: CGI.pm HTML-Generation Methods Considered Useful
by toma (Vicar) on Jul 10, 2004 at 08:35 UTC
    I have been working on coding standards, trying to figure out how to reconcile the need to not limit creative programmers, while not over-burdening maintainers with overly creative code.

    I have had trouble figuring out where to draw the line on cleverness, and I think your example is very helpful in this respect.

    It's really great code. It is useful, powerful, and I like it. I think that it would be really helpful to translate it into more mundane code to compare the ease of understanding and maintainance. Perhaps someone could even compare the difference in the difficulty of adding table headers, which you mentioned as a possible enhancement.

    I have noticed that both the quest and the map operations are associated with much unfathomable code, and I now I suspect that that the combination is multiplicative.

    I hope you don't mind if your code shows up in our coding standard!

    It should work perfectly the first time! - toma
      Thanks for the compliment. :)

      Feel free to use the code for whatever you like.

      much unfathomable code

      The coding conventions are not exactly what I would do, but the code itself is fine and very understandable. Definately easier to understand with map than with lots of little loops.

      What line do you consider problematic?

Re: CGI.pm HTML-Generation Methods Considered Useful
by tachyon (Chancellor) on Jul 10, 2004 at 10:52 UTC

    Personally we have a pretty table function that takes an array of array refs (as well as width and centre args). It does the alternate the colours thing (stylesheet based) and as it takes an array of array refs all you need to do is unshift a header into the return from fetchall_arrayref and there is your table. This lives in our UI.pm and using it is a single call $html = pretty_table( $ary_ref ) +/- unshifting headers. Benefits include a single simple call and stylsheeting so the theme remains consistent. Oh and the sum total code volume of our CGI parser, HTML::Template, UI.pm.... is still less than CGI.pm alone, and a lot faster (not that code volume or size make much difference to most users). Here is the code FWIW.

    sub pretty_table { my ($ary, $width, $no_center) = @_; my $header = $ary->[0]; $width = $width ? qq!width="$width"! : ''; my $html = qq!<table border="0" cellpadding="2" cellspacing="1" $w +idth>\n!; my $cols = scalar @$header; $html .= get_row( $header, $cols, 'header' ); my $flip_flop = 1; for my $row_ref ( @$ary[1..$#{$ary}] ) { my $class = $flip_flop ? 'light' : 'dark'; $html .= get_row( $row_ref, $cols, $class ); $flip_flop ^= 1; } $html .= qq!<tr><td colspan="$cols" class="header">&nbsp;</td></tr +>\n!; $html .= "</table>\n"; $html = qq!<div align="center"><center>\n$html</center></div>\n! u +nless $no_center; return $html } sub get_row { my ( $row_ref, $cols, $class ) = @_; my $html = qq!<tr>!; for my $td ( 0.. $cols-1 ) { my $data = defined $row_ref->[$td] ? $row_ref->[$td] : '&nbsp +;'; $html .= qq!<td class="$class">$data</td>!; } $html .= "</tr>\n"; return $html }

    cheers

    tachyon

      That reminds me; I forgot to include an evil method for alternating row colors:

      my @colors = ( '#dddddd', '#eeeeee' ); $html .= $q->table( { -bgcolor => '#ffffff', -border => 1 }, $q->caption('Status Summary'), # beware beautiful nested maps ahead map { my $k = $_; $q->Tr( { -bgcolor => (push @colors, shift @colors) && $colors[0] }, $q->td( $k ), map { $q->td( $data->{$k}{$_} eq 'bad' ? { -bgcolor => '#ff0000' } : { -bgcolor => '#ffffff' }, $data->{$k}{$_} ) } sort keys %{ $data->{$k} } ) } sort keys %{ $data } );

      There is no particularly good reason to do this. After this, it's just for fun. :)

        Surely you meant to map to stylesheet classes? Does anyone still hardcode colors into their code these days? It is 2004 afterall....

        Do you find it vaguely inefficient to use 3000+ lines of code + your 18 to do what <30 lines of code (or one line as a call when you modularise it) will do just as well, faster and in a more transparent maintainable and consistent manner?

        Hmm does that sound biased? OK I admit it. I think CGI.pm has no place in generating HTML in the first place, and don't like the interface. You either love it or you hate it. I have used it. I do hate it. Ovid likes it and so does merlyn. What can I say? They are obviously misguided, or I have no idea what I am talking about. Or perhaps they use what works for them as I use what works for me. Use what works for you. TIMTOWDI.

        cheers

        tachyon

Re: CGI.pm HTML-Generation Methods Considered Useful
by jonadab (Parson) on Jul 10, 2004 at 19:24 UTC
    $html .= $q->table( { -bgcolor => '#ffffff', -border => 1 }, $q->caption('Status Summary'), # beware beautiful nested maps ahead $q->Tr( [ map { my $k = $_; $q->td( [ $k, map { $data->{$k}{$_} } sort keys %{ $data->{$k} } ] ) } sort keys %{ $data } ] ) );

    I fail to see how this is preferable to the normal way of doing it:

    $html .= "<table class=\"foo\"><thead><tr>" . (join "\n", map {"<th>$_</th>"} @field) . "</tr></thead><tbody>\n" . (join "\n", map { my %record = %$_; "<tr class=\"".(somecondition($_))."\">" .(join "\n", map {"<td>$record{$_}</td>"} @field)."</tr>" } @record) . "</tbody></table>\n";

    ;$;=sub{$/};@;=map{my($a,$b)=($_,$;);$;=sub{$a.$b->()}} split//,".rekcah lreP rehtona tsuJ";$\=$;[-1]->();print
Re: CGI.pm HTML-Generation Methods Considered Useful
by dragonchild (Archbishop) on Jul 11, 2004 at 01:56 UTC
    I'm curious - you mention this is for a status-monitoring application-ette. Why don't you take a little bit of time and convert it to HTML::Template or TT? You already have it in the right data format - AoH. TT can take HoH, even, and sort them.

    Here's the big thing for me - CGI has nifty time-savers for generating HTML. Yet, I have NEVER been left with just generating HTML. I have always had some suit come up to me and say "Could you just export this to Excel for me?" or "Can I get a nice printout of that?" (implying PDF). Or, even worse, "Can you graph that for me?". Which is why I wrote PDF::Template, Excel::Template, and Graph::Template - to go with HTML::Template. Same data structure, but it takes me 5 minutes to convert any webpage into a PDF, XLS, or PNG. I'm not knocking CGI as a quick-hit, but I think to use its HTML generation in anything but a one-off is just begging to be hit by a rewrite-by-four.

    ------
    We are the carpenters and bricklayers of the Information Age.

    Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

    I shouldn't have to say this, but any code, unless otherwise stated, is untested

      I want to see a demo of this. Please?
        I started discussion of this concept in Re: Re: Re: Why CGI::Application. Basically, you have a single function in your CGI::Application baseclass which does all your output. The function accepts a template name, an output type, and parameters. Something like:
        my %types = ( pdf => { extension => '.pdf.xml', module => 'PDF::Template', }, xls => { extension => '.xls.xml', module => 'Excel::Template', }, html => { extension => '.tmpl', module => 'HTML::Template', }, ); sub print { my $self = shift; my ($tmpl_name, $type, @parms) = @_; my $module = $types{$type}{module}; my $template = $module->new( filename => $tmpl_name . '.' . $types{$type}{extension}; ); $template->param( @parms ); return $template->output; }

        That's the basic skeleton. Obviously, you'll want to extend that a bit, add some error-handling and defaults. But, Excel::Template and PDF::Template support the exact same API as HTML::Template, by design.

        ------
        We are the carpenters and bricklayers of the Information Age.

        Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

        I shouldn't have to say this, but any code, unless otherwise stated, is untested

Re: CGI.pm HTML-Generation Methods Considered Useful
by Your Mother (Canon) on Jul 12, 2004 at 08:10 UTC

    Another little example you've given me an excuse to haul out. Contains a stylesheet trick I like for standalone CGIs.

    use strict; use CGI qw(:standard); use Date::Calc qw(:all); print header(), start_html( -title => "My Little Calendar", -head => style({ -type => 'text/css' }, join('', <DATA>),) +, ); my ($year, $month, $day) = Today(); my @month = (1 .. Days_in_Month($year, $month)); my $offset = Day_of_Week($year, $month, 1) - 1; unshift @month, map { undef } 1 .. $offset; my $end_of_month_offset = 7 - (@month % 7); push @month, (undef) x $end_of_month_offset; my @Cmonth = (); while (@month) { push @Cmonth, [ splice @month, 0, 7 ] } print table( Tr( td( { -class => 'month', -colspan => 7 }, uc(Month_to_Text($month)) ), ), Tr( map { td({ -class => 'day' }, Day_of_Week_Abbreviation($_)) } (1 .. 7) ), map { Tr( map { td({ -class => $_ == $day ? 'today' : 'date' }, $_) } @$_ ) } @Cmonth ); __DATA__ table, tr { border:0; padding:0; margin:0; } tr { text-align:center; } td { font-family:optima,helvetica,sans-serif; font-size:60%; margin:2px; padding:1px; } .day { background-color:#9ca; width: 2.2em; font-weight:bold; } .date { background-color:#9cf; height: 2em; } .today { background-color:#cef; height: 2em; } .month { text-align:center; background-color:#68b; font-size:11pt; letter-spacing:.2ex; font-weight:bold; }
Re: CGI.pm HTML-Generation Methods Considered Useful
by Juerd (Abbot) on Jul 12, 2004 at 11:59 UTC

    CGI.pm considered painful and ugly.

    HTML generating methods considered very slow.

    td and Tr considered hard to read.

    Nested maps considered harmful for maintainer's brain.

    Considering considered a waste of time :)

    But I think you're not thinking of ever maintaining this code anyway. The one-letter variables (typing "key" instead of "k" will not give you RSI), big indentation levels, hardcoded colour codes and sequential building of HTML give that away. I'm guessing that somewhere in your code is something like $html = read_file('header.html');, or worse: $html = $q->start_html; (if it is a string literal, please let someone strangle you).

    Just store your data as an array of arrays, or something alike, and use a simple module to write the table for you. Or at least move the html-generating things out of the way of anything that deals with raw data.

    Templating modules considered mandatory.

    Juerd # { site => 'juerd.nl', plp_site => 'plp.juerd.nl', do_not_use => 'spamtrap' }

Log In?
Username:
Password:

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

How do I use this? | Other CB clients
Other Users?
Others having an uproarious good time at the Monastery: (5)
As of 2014-12-28 10:20 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    Is guessing a good strategy for surviving in the IT business?





    Results (180 votes), past polls