Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl: the Markov chain saw

RFC: CGI::Tables

by elusion (Curate)
on May 07, 2003 at 03:22 UTC ( [id://256105]=perlmeditation: print w/replies, xml ) Need Help??

Below is a module I've written to dynamically create HTML tables. I've never liked creating them by hand. Comments and suggestions are very welcome.
package CGI::Tables; use strict; use vars qw[@ISA @EXPORT $VERSION]; $VERSION = '0.9'; require Exporter; @ISA = qw(Exporter); @EXPORT = qw(table); #feedit: options, data #receive: html #effect: none sub table { my $data = pop; my $keyed = { @_ }; my $html; my %attrs = %{shift @$data} if "HASH" eq ref $data->[0]; $data = arrange($data, $keyed) if $keyed->{cols} or $keyed->{rows}; $data = add_titles($keyed, $data) if $keyed->{col_titles} or $keyed->{row_titles}; $html .= "<table" . attributes(%attrs) . ">\n"; $html .= row(@$_) for @$data; $html .= "</table>\n"; return $html; } #feedit: row data #receive: html #effect: none sub row { my (@cells) = @_; my %attrs = %{shift @cells} if "HASH" eq ref $cells[0]; my $html = "<tr" . attributes(%attrs) . ">"; $html .= cell($_) for @cells; $html .= "</tr>\n"; return $html; } #feedit: cell data #receive: html #effect: none sub cell { my ($data) = @_; return "<td>$data</td>" if not ref $data; my %attrs = %{shift @$data} if "HASH" eq ref $data->[0]; my $html = "<td" . attributes(%attrs) . ">"; $html .= $_ for @$data; $html .= "</td>"; $html =~ s/td/th/g if $attrs{_header}; return $html; } #feedit: keyed args, data #receive: data #effect: none sub add_titles { my ($keyed, $data) = @_; my @titled; if ($keyed->{col_titles}) { my @titles; push @titles, title($keyed->{col_titles}, $_) for 0..$#{$data->[0]}; push @titled, [@titles]; } if ($keyed->{row_titles}) { unshift @{$titled[0]}, "" if $keyed->{col_titles}; for (0..$#$data) { push @titled, [ title($keyed->{row_titles}, $_), @{$data->[$_]} ]; } } else { push @titled, @$data; } return \@titled; } #feedit: titles, col/row number #receive: title #effect: none sub title { my ($titles, $n) = @_; my $title; if ("ARRAY" eq ref $titles) { $title = $titles->[$n]; } elsif ("CODE" eq ref $titles) { $title = $titles->($n); } return [ {_header=>1}, $title ]; } #feedit: attribute hash #receive: code for inside html tags #effect: none sub attributes { my (%attrs) = @_; my $html = ""; for (keys %attrs) { next if $_ =~ /^_/; $html .= " $_=\"$attrs{$_}\""; } return $html; } #feedit: data, keyed #receive: arranged data #effect: none sub arrange { my ($data, $keyed) = @_; my ($rows, $cols) = size($#$data + 1, $keyed); # add extra cells that are needed push @$data, "" until $#$data + 1 == $rows * $cols; my @arranged; if ($keyed->{cols}) { for (1..$rows) { push @arranged, [ splice @$data, 0, $cols ]; } } else { push @arranged, [] for 1..$rows; while (@$data) { push @$_, shift @$data for @arranged; } } return \@arranged; } #feedit: number of data elements, keyed #receive: number of rows, cols #effect: none sub size { my ($n_data, $keyed) = @_; my ($rows, $cols, $ratio); if ($rows = $keyed->{rows}) { $cols = divisor($n_data, $rows); } elsif ($cols = $keyed->{cols}) { $rows = divisor($n_data, $cols); } return $rows, $cols; } #feedit: number of data elements, number of (rows, cols) #receive: number of other way #effect: none sub divisor { my ($n_data, $rows) = @_; my $cols = int $n_data / $rows; $cols++ if $n_data % $rows; return $cols; } 1; __END__ =head1 NAME CGI::Tables - An easy way to create HTML tables =head1 SYNOPSIS use CGI::Tables; print table cols => 2, [qw(Cell Cell Cell)]; =head1 DESCRIPTION Only one function is exported: C<table()>. You pass it the parameters +and the data, in that order. The data is passed as an array ref and the parameters as a list. In return, you get HTML for a table. =head2 Arranging Data There are three ways to set up the data. All of the examples in this section produce identical tables. =over =item Hard Coding This method is the old fashioned way. If you're lazy (it's a virtue), +use one of the other two. This I<is> the most efficient way though. Pass t +he rows as array refs and the cells inside that. table [ [ "Row 1 Cell 1", "Row 1 Cell 2", "Row 1 Cell 3" ], [ "Row 2 Cell 1", "Row 2 Cell 2", "Row 2 Cell 3" ] ]; =item Cols If you pass the number of columns you want and put the cells in the da +ta array ref, CGI::Tables will automagically figure out how many rows the +re are and place the data in the correct place. The cells go from left to + right, top to bottom. table cols => 3, [ "Row 1 Cell 1", "Row 1 Cell 2", "Row 1 Cell 3", "Row 2 Cell 1", "Row 2 Cell 2", "Row 2 Cell 3" ]; =item Rows This is similar to passing the number of columns. The only difference is that the data as arranged top to bottom, left to right. table rows => 2, [ "Row 1 Cell 1", "Row 2 Cell 1", "Row 1 Cell 2", "Row 2 Cell 2", "Row 1 Cell 3", "Row 2 Cell 3" ]; =back =head2 Cells When passing a cell, it is perfectly acceptable to pass a string. You +can pass an array ref if you so desire. The elements of ref will be concatenate +d and placed in the cell. =head2 Column and Row Headers Column and row headers are passed by C<col_titles> and C<row_titles>, respectively. You can either pass an array ref of titles or a code/sub +routine reference. Code refs are passed column/row indices, which start at zer +o. Examples: table rows => 2, col_titles => sub { return "Col " . (shift() + 1) } +, [ @data ]; table rows => 2, row_titles => ["Row 1", "Row 2"], [ @data ]; =head2 Specifying HTML Attributes HTML attributes can be specifed for the table, a row, or a cell. Stick + a hash ref as the first element in the appropriate array ref, and it wil +l be added. For cells, pass an array ref (which would otherwise be optional +) with a hash ref in front. Row attributes cannot be passed when the tab +le is created dynamically. The following example will print a table of width + 600 with one cell that has #660000 as the background color. table cols => 2, [ {width => 600}, [ {bgcolor => "#660000"}, "Cell 1 +" ], "Cell 2", "Cell 3", "Cell 4" ]; =head1 AUTHOR Matthew Diephouse =cut

elusion :

Replies are listed 'Best First'.
Re: RFC: CGI::Tables
by hossman (Prior) on May 07, 2003 at 07:07 UTC
    1. I have to be honest, while your API seems nice and simple and clean, I don't see any major advantage to using it over using HTML::AsSubs (or the table/Tr/th/td methods that CGI provides).
    2. You might want to read up on HTML::ElementTable, It seems to have a pretty powerful API for manipulating tables.
      One of the major advantages of seperating presentation from code is being able to change the presentation without changing the code. This allows people such as web designers to come in and redesign a website without the programmer having to change his code to fit that new design.

      I can personally say that HTML::Template works great for me and allows for easy design changes. As for being able to manipulate tables using HTML::ElementTable, you can't get much more powerful than changing the raw HTML.

      I think the area where my code would shine is when you have a table, and you want to insert data and shift everything else. Instead of moving braces and commas and rearranging, you just put the new cell in there.

      The same thing applies to changing the number of rows or columns. Change one number and you're done. If you want, you can even choose the size based on a user preference.

      elusion :

Re: RFC: CGI::Tables
by theorbtwo (Prior) on May 07, 2003 at 03:53 UTC

    The spec looks nice. I havn't looked through the code yet. OTOH, I'd name it HTML::Tables, as this has nothing to do with the CGI interface.

    Warning: Unless otherwise stated, code is untested. Do not use without understanding. Code is posted in the hopes it is useful, but without warranty. All copyrights are relinquished into the public domain unless otherwise stated. I am not an angel. I am capable of error, and err on a fairly regular basis. If I made a mistake, please let me know (such as by replying to this node).

      I'm horrible at picking names.

      I picked CGI because at least one well known CGI module does tables and other HTML. Now that you say that though, it makes sense. I see HTML::Table too now... though the interface is a bit different.

      elusion :

Re: RFC: CGI::Tables
by Heidegger (Hermit) on May 07, 2003 at 06:56 UTC
    When developing web apps I prefer to have the HTML code in a template. You can use HTML::Template for it. That way your tables are also created dynamically. What you have to do is put your data in the array+hash structure. Then you can change the appearance of the table by editing template or global CSS. That way the code and presentation are separate.
(jeffa) Re: RFC: CGI::Tables
by jeffa (Bishop) on May 07, 2003 at 22:47 UTC
    Did you not see my attempt at this problem: DBIx::XHTML_Table? You can even use that module to create a "Spreadsheet CGI". However, mine is more tied to DBI ... but you can always pass the constructor an AoA without needing a database.

    This does make me wonder if there should be a base class that both of our modules could use ... on another note, i would like to see your module replace HTML::Table, which i really don't care for. The author actually tries to handle each attribute individually instead of generically allowing attributes to 'handle themselves' ... shudder

    Anyhoo, check out my module ... there are some neat features you are welcome to steal. :) (like rotating attributes and callbacks for each cell)

    Oh yeah, i almost forgot -- if you want to investigate the HTML::Template solution, be sure and check out my Dynamic HTML::Template Database Template. Again, it was designed to be generic and to be used with a database, but you can always modify it to fit your specific needs.


    (the triplet paradiddle with high-hat)
      I hadn't seen DBIx::XHTML_Table before. It is the 96th result returned in a CPAN search for "html table". I rarely search to the 10th page. Truth be told, there are entirely too many results returned. Many of them seem to do the same thing, and most of them also seem poorly designed, IMOH.


      Some people will naturally like templates better. That's fine, but let's ignore that at the moment, because I personally don't care much for them. They have their place, but most of the time it's not with me.

      Where is the best place to put a module like this? I see them in all different namespaces: DBIx, HTML, Table, Text, CGI. Where would it actually belong? If it's made more generic with different output options? What if you decide you want to expand it with other HTML functions? This is a general problem with CPAN, I think. Reusable code doesn't fit well in a hierarchy.

      Now what if different people have different ideas of what a good interface would be? The first module claims a good name, and the second really has no place.

      Suppose, for instance that I decide I don't like the way works. I decide that I want to redesign it. Sure, reinventing the wheel can be bad, but there's always the possibility of improvement. What would I name my module?


      It's really hard to find the right module sometimes. Too hard. I suppose it's even harder to write one.

      elusion :

        It's curious that in the same burst you rant about too many modules being on CPAN and having them poorly designed, but you offer no feedback at all on DBIx::XHTML_Table, which apparently has support for using an AoA to build the table, which I think is supposed by be one of the neat features of your module-- the easy ability to add a column.

        My suggestion is that you if you are concerned about the proliferation of semi-useful modules on CPAN, look hard at all the alternatives to your own module that you could contribute to before starting yet-another.

        I get the sense that many modules are maintained by primarily one person who would be glad to have help, and we would all benefit by having fewer high quality modules to wade through.

        If you remain confident your module is filling a unique niche, I think the mailing list is the appropriate place to ask about potential names.


Re: RFC: CGI::Tables
by chip (Curate) on May 09, 2003 at 16:03 UTC
    Minor point, but your creates %attrs hashes by copying the incoming hash (if any), but it could just use its ref directly. So instead of:

    my %attrs = %{shift @$data}

    You could say:

    my $attrs = (ref($data->[0]) eq 'HASH') ? shift @$data : {}

    Then refer to $attrs->{FOO}.

        -- Chip Salzenberg, Free-Floating Agent of Chaos

      Or maybe
      my $attrs = ref eq 'HASH' ? $_ : {} for shift @$data;
      which I find a lot less clumsy. Update: was ref $_ before.

      Makeshifts last the longest.

        Absolutely; I often topicalize with for. But I'd also write "ref $_" as "ref" or "ref()" -- concision!

            -- Chip Salzenberg, Free-Floating Agent of Chaos

        Oops! Your substitution is actually really broken, because when $data[0] isn't a HASH, it gets thrown away. This is a Bad Thing. I didn't notice it at first, which is also a Bad Thing. :-,

            -- Chip Salzenberg, Free-Floating Agent of Chaos

Re: RFC: CGI::Tables
by halley (Prior) on May 07, 2003 at 23:40 UTC

    Before I noticed Text::Table was around, I made a similar module which I called, of all things, Text::Table. However, unlike the one on CPAN, mine supports outputting to plain text, CSV, or HTML. You have more support for coloring and style, etc., but we both seem to be reinventing wheels.

    [ e d @ h a l l e y . c c ]

Log In?

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

How do I use this?Last hourOther CB clients
Other Users?
Others rifling through the Monastery: (2)
As of 2024-04-25 06:05 GMT
Find Nodes?
    Voting Booth?

    No recent polls found