HTML::Template's TMPL_LOOP tag is useful for displaying lists of things. But if the list becomes long, you might want to break it into columns. HTML::Template doesn't provide an easy way to do this. The columnize() method, below, will take your loop data (in the structure required by HTML::Template's param() method - making this easy to incorporate into your existing code) and create a new structure, reformatted into rows and columns. You specify both the number of columns as well as the ordering of your data - either across or down the columns.
(Your comments are invited.)
Sample simple usage:
...
my $flat_data =
[{data=>'A'} , {data=>'B'}, {data=>'C'}, {data=>'D'},{data=>'E'}];
my $columnized_data = columnize($flat_data, 3, "across");
my $tmpl_file = ...;
my $tmpl = HTML::Template->new(
filename => $tmpl_file,
loop_context_vars => 1
) or die "Creation of template object from $tmpl_file failed.";
$tmpl->param( {DATA_LOOP => $columnized_data});
print "Content-type: text/html\n\n", $tmpl->output();
The output will look like:
A B C
D E
In case you're wondering, the return value of columnize() was:
[
{
'COLUMN_LOOP' => [
{'data' => 'A'},
{'data' => 'B'},
{'data' => 'C'}
]
},
{
'COLUMN_LOOP' => [
{'data' => 'D'},
{'data' => 'E'}
]
}
];
Alternatively, had you called columnize($flat_data, 3, "down"), you'd get output:
A C E
B D
with columnize() returning:
[
{
'COLUMN_LOOP' => [
{'data' => 'A'},
{'data' => 'C'},
{'data' => 'E'}
]
},
{
'COLUMN_LOOP' => [
{'data' => 'B'},
{'data' => 'D'}
]
}
];
Once you've added this second dimension to your data, you'll need to to do the same to your template. This is easy, since HTML::Template allows for nesting TMPL_LOOP tags. Simply add a nested TMPL_LOOP tag named COLUMN_LOOP, to your existing TMPL_LOOP. (The outer loop represents the rows, the inner loop, the columns).
So if your existing TMPL_LOOP section looks like:
<tmpl_loop name="DATA_LOOP">
<tr>
<td>
<tmpl_var name="data">">
</td>
</tr>
</tmpl_loop>
Change it to:
<tmpl_loop name="DATA_LOOP">
<tr>
<tmpl_loop name="COLUMN_LOOP">
<td>
<tmpl_var name="data">">
</td>
</tmpl_loop>
</tr>
</tmpl_loop>
____
The code:
#!/usr/bin/perl -w
use strict;
use Carp;
sub columnize {
my $rows_ref = shift;
my $num_cols = shift;
my $order = shift;
if (!defined $rows_ref or !defined $num_cols or !defined $order) {
croak("Undefined params not permitted:\n");
}
if ($num_cols < 1) {croak "Bad column-number param: $num_cols\n";}
if ($order !~ /^(across|down)$/i) {croak "Bad order param: $order\n
+";}
use integer; # use integer division for all division operations
my $num_rows = ($#$rows_ref / $num_cols) + 1;
my @result_rows = ();
for (my $row = 0; $row < $num_rows; $row++) {
for (my $col = 0; $col < $num_cols; $col++) {
my $index;
if ($order eq "across") {
$index = ($row * $num_cols) + $col;
} else {
$index = ($col * $num_rows) + $row;
}
# Skip any empty spots in the last row (if "across")
# or in the last column (if "down"):
next if ($index > $#$rows_ref);
$result_rows[$row]->{COLUMN_LOOP}->[$col] = $rows_ref->[$inde
+x];
}
}
return \@result_rows;
}
____
Caveat: when calling columnize with a "down" ordering, the column parameter is used as a maximum suggested column count, rather than a fixed specification. It calculates the number of rows that would result from the given number of columns, and then chooses an actual number of columns that would leave the fewest empty slots in the last row. Consider:
columnize(<data set with 11 items>, 5, "down");
A literal interpretation would produce:
A D F H J
B E G I K
C
But if you're going to fill 3 rows anyway, and require the end-user to read vertically, then what you (or at least your reader) would probably want is:
A D G J
B E H K
C F I
If you look at the output of the 'ls' command, you'll see that it follows a similar algorithm. Of course, this will usually only make a difference when you specify a lot of columns, and your data set is relatively small.