Seaside provides WATableReport as a simple but
useable component for presenting tabular data. While it lacks
some flexibility in terms of rendering (particularly style tagging
to support justification and the ability to render arbitrary
components into the table), it is often good enough for simple
applications and makes a great starting place for more complicated
HTML reports. WATableReport is directly useable for
building sortable tables containing text or links. With
subclassing it can be extended to include images or input
components in your tables as well. In this tutorial we will look
first at building a simple sortable tablular report and then at
including web drill-down capabilities to reports. We finish by
showing some useful extensions.
We begin by creating a component showing a "table of factorials". First the component class:
WAComponent subclass: #FactorialTableComponent
instanceVariableNames: 'report'
classVariableNames: ''
poolDictionaries: ''
category: 'WATableReportExample'
next we add a canBeRoot class side method
canBeRoot ^trueNow our component will be visible in the Seaside config tool so we can make it the root of an application. Go ahead and create an application which has this component as a root so we can test it. Now we wish to create an instance of
WATableReport
(which we will store in our report instance
variable). WATableReport instances need a data set
(the "rows") and a collection of columns which
essentially tell how to produce the text for that column's items.
In our case the data set are simply the numbers 1 to 6. We want
to have three columns, one which displays the number itself, one
which sends it factorial and displays the result and one which
displays "even" if the number is even and
"odd" of the number is odd. We build up the table in
the initialize method below.
initialize
| rows columns |
rows := #(1 2 3 4 5 6).
columns := OrderedCollection new
add: (WAReportColumn selector: #yourself title: 'original'); "selector gets perform:-ed on your data item"
add: (WAReportColumn selector: #factorial title: 'factorial');
add: (WAReportColumn new "or if you want more control"
valueBlock: [:rowItem | rowItem even ifTrue: ['even'] ifFalse: ['odd']];
title: 'odd/even');
yourself.
report := WATableReport new rows: rows; columns: columns; yourself.
Notice that WAReportColumn can either use a selector
directly to get a column's data from your row data or it can use a
block. Now let's arrange for this component to be displayed by
implementing children and
renderContentOn: as follows:
children ^Array with: report renderContentOn: html html render: reportYou should now be able to view your component. Notice that every three rows alternate between a white and a yellow background (actually, on my monitor the yellow is barely noticeable). If you want to change the period of the alternation send
rowPeriod: to the report instance (in
initialize) and if you want to change the colors send
rowColors: with an array of string colors. For
example, adding
report rowPeriod: 2.
report rowColors: {'white'. 'lightgreen'}.
makes a greenbar style report but with only two text lines per
region. If you supply more colors then they will be included in
the alternation. If you want the whole report white, just sent
report rowColors: {'white'}. If you view your
application now it should look like
this.
Notice that each column can be sorted by clicking on the column
heading. Only one active "sort" column is supported and no visual
indication is given showing which column is controlling the sort
order. In order for sorting to work, your row objects must
understand <= or you must specify a different
sortBlock for that column.
Now, all of us with bosses (pointy-haired or not!) know that numerical columns must be right aligned. We'll just right align all of the cells for now.
style
^ 'td {text-align: right;}'
The final result should look something like
WAReportColumn by specifying a
"click block" for that column.
WATableReport will display items in that column as
anchors allowing the user to click to presumably see more detail.
In our example we add a click block to the number column. Modify
the first column created in initialize to look as
follows:
add: ((WAReportColumn selector: #yourself title: 'original')
clickBlock: [:row | self tellMeMoreAbout: row];
yourself);
and implement
tellMeMoreAbout: aNumber self inform: 'The number ' , aNumber printString , ' is a very pretty number.'OK, not very interesting but you get the idea. In the callback we could display more details about the row in question. In this case our row data are numbers so we could display more information about the number. If you view the report you will see that the first column data are presented as anchors which invoke our callback block when clicked. Note that no matter which column you add the click block to, the row data is passed as the argument to the block (not the value of that specific column). This is normally what you want.
[:x | x
asString] so by default your cell's value is sent
asString and the result is displayed as text. (Note
this method should return a String or something which
produces the desired response to asString. This method should not
try to produce html since you will not get the desired result.)
The format block for a column can be changed using
formatBlock:. Suppose, for example, that we want the
factorial data displayed in binary. We would modify the line
which adds the factorial column in initialize to:
add: ((WAReportColumn selector: #factorial title: 'factorial')
formatBlock: [:item | item printStringBase: 2];
yourself);
Probably the most common use of this is to format dates. For
example the format block [:item | item mmddyyyy]
displays dates in the format 'month/day/year'. Look at the
Date class for other formats. We have no particular
use for dates in our example.
WAReportColumn has a simple but generally useful
mechanism for providing a summary row at the end of a table. If a
column is flagged with hasTotal: true then the sum of
all the values in the row will be displayed. This typically only
works for numeric columns since it uses
Collection>>detectSum:. Suppose we want to total the
number and factorial values (no longer in base 2), change the
lines above to:
add: ((WAReportColumn selector: #yourself title: 'original') hasTotal: true; yourself); add: ((WAReportColumn selector: #factorial title: 'factorial') hasTotal: true; yourself);The summary row is displayed using HTML table header
tags. This doesn't produce a very nice looking
table in this example but this can be solved with some CSS by
adding the following method:
style
^ 'td {text-align: right;}
tr+tr>th {text-align: right; border-top: 1px dashed blue;}'
The footer and header both use th tags so our CSS
selector to pick out the footer is a little longer than you
might have expected. Our report now looks like this:

Enhancements that I find useful
Here I present extensions (through subclassing only!) to
WATableReport and WAReportColumn which I
think maintain the spirit of this framework but make it easier to
produce better looking reports.
The two changes that I'd like to affect are: more useful style
information in the table and more control over what is rendered in
a cell (to permit images etc). In the current design it seems
that too much of the rendering responsibility falls on
WATableReport rather than on
WAReportColumn (*). So, I
subclass each:
WATableReport subclass: #SCTableReport
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SC-Components'
WAReportColumn subclass: #SCReportColumn
instanceVariableNames: 'cssClass '
classVariableNames: ''
poolDictionaries: ''
category: 'SC-Components'
Now in SCTableReport I override the following method
renderColumn: aColumn row: aRow on: html
aColumn renderCellForRow: aRow on: html
and in SCReportColumn I implement it as follows:
renderCellForRow: aRow on: html
| text |
text _ self textForRow: aRow.
text isEmpty
ifTrue: [text _ ' '].
html
tableData: [self canChoose
ifTrue: [html
anchorWithAction: [self chooseRow: aRow]
text: text]
ifFalse: [html text: text]]
This is essentially the original implementation of
WATableReport>>renderColumn:row:on: but now rendering
is the responsibility of the column. At this point I've made no
changes in functionality and the "SC" components should work just
like the "WA" components, that is, the new "SC" components work
exactly like the "WA" components. Now we start to make changes.
First accessor/mutator for the cssClass i-var:
cssClass: anObject
cssClass _ anObject
cssClass
^ cssClass
Now we modify our rendering method to use this class:
renderCellForRow: aRow on: html
| text |
text _ self textForRow: aRow.
text isEmpty
ifTrue: [text _ ' '].
cssClass ifNotNil: [html cssClass: cssClass].
html
tableData: [self canChoose
ifTrue: [html
anchorWithAction: [self chooseRow: aRow]
text: text]
ifFalse: [html text: text]]
Now we can change our table so that the parity column is left
justified (it text, after all). There are two changes, the first
is to add a call to cssClass: in our initialize
method:
initialize
| rows columns |
rows := #(1 2 3 4 5 6).
columns := OrderedCollection new
add: ((SCReportColumn selector: #yourself title: 'original') hasTotal: true;
yourself);
add: ((SCReportColumn selector: #factorial title: 'factorial') hasTotal: true;
yourself);
add: (SCReportColumn new "or if you want more control"
valueBlock: [:rowItem | rowItem even ifTrue: ['even'] ifFalse: ['odd']];
title: 'odd/even';
cssClass: 'parity');
yourself.
report := SCTableReport new rows: rows; columns: columns; yourself.
report rowPeriod: 2.
report rowColors: {'white'. 'lightgreen'}.
and change our style method to left justify that column:
style
^ 'td {text-align: right;}
tr+tr>th {text-align: right; border-top: 1px dashed blue;}
td.parity {text-align: left;}'
So now our report looks like:

Similarly we should move rendering of the header and footer over
to the column class. Add the following to
SCTableReport:
renderTableHeaderOn: html
html cssClass: 'header'.
super renderTableHeaderOn: html
renderHeaderForColumn: aColumn on: html
aColumn
renderHeaderOn: html
sortBlock: [self sortColumn: aColumn]
renderTableFooterOn: html
html cssClass: 'footer'.
super renderTableFooterOn: html
renderFooterForColumn: aColumn on: html
aColumn renderFooterOn: html forRows: rows
Now we put the header and footer code in (adding the CSS classes)
in SCReportColumn:
renderHeaderOn: html sortBlock: aBlock
cssClass
ifNotNil: [html cssClass: cssClass].
html
tableHeading: [self canSort
ifTrue: [html anchorWithAction: aBlock text: self title]
ifFalse: [html text: self title]]
renderFooterOn: html forRows: rows
cssClass
ifNotNil: [html cssClass: cssClass].
html
tableHeading: [html
text: (self totalForRows: rows)]
Now we have a lot easier time assigning styles to a specific
header or footer cell. In this particular example this allows us
to change our simple style method to
style
^ 'td {text-align: right;}
tr.footer th {text-align: right; border-top: 1px dashed blue;}
td.parity {text-align: left;}'
Really not much of a technical simplification but it does make our
intentions more clear.
While we're thinking about styles...what should we do about the
sort column? Personally I like some visual indicator of which
column is controling the sort but just giving it a CSS class
doesn't sound like enough. What if we want to decorate it with a
graphical arrow...I guess that's a style decision but I don't like
injecting content using CSS. Rather than produce the most
flexible solution here I opted for the one I wanted for my
particular application. I simply added a "+" (plus) or
"-" (or minus) sign in parentheses to the column label
if it is controlling the sort. Here's the change to SCTableReport
renderHeaderForColumn: aColumn on: html
aColumn
renderHeaderOn: html
sortBlock: [self sortColumn: aColumn]
sorted: aColumn == self sortColumn
reversed: self isReversed
and for SCReportColumn
renderHeaderOn: html sortBlock: aBlock sorted: sorted reversed: reversed
| sortedString |
sortedString _ sorted
ifTrue: [reversed
ifTrue: ['(-)']
ifFalse: ['(+)']]
ifFalse: [''].
cssClass
ifNotNil: [html cssClass: cssClass].
html
tableHeading: [self canSort
ifTrue: [html anchorWithAction: aBlock text: self title , sortedString]
ifFalse: [html text: self title , sortedString]]
You can get rid of
SCReportColumn>>renderHeaderOn:sortBlock: since it is
no longer used. Here's what the report looks like when reverse
sorted by the first column:

One last improvement: images in cells. At first I thought I
needed to be able to render any kind of HTML content into a cell.
In practice the only non-text content I've needed was images but
but I have used the rendering control to color cells etc.
Basically we add a render block to SCReportColumn.
This three argument block is responsible for rendering the
td element completely. The one provided by
initialize (below) shows the default text rendering
mechanism.
WAReportColumn subclass: #SCReportColumn
instanceVariableNames: 'cssClass renderBlock '
classVariableNames: ''
poolDictionaries: ''
category: 'SC-Components'
initialize
super initialize.
renderBlock _ [:row :col :html | html
tableData: [self canChoose
ifTrue: [html
anchorWithAction: [self chooseRow: row]
text: (self textForRow: row)]
ifFalse: [html
text: (self textForRow: row)]]]
renderBlock
^ renderBlock
renderBlock: anObject
renderBlock _ anObject
textForRow: row
| text |
text _ formatBlock
value: (self valueForRow: row).
text
ifEmpty: [^ ' '].
^ text
renderCellForRow: aRow on: html
self cssClass
ifNotNil: [html cssClass: self cssClass].
self renderBlock
value: aRow
value: self
value: html
Now if all went well your report should look exactly the same.
Now add a custom render block to the "even/odd" column
that paints odd cells red. Modify
FactorialTableComponent>>initialize so that this
column gets added as below.
add: (SCReportColumn new
valueBlock: [:rowItem | rowItem even
ifTrue: ['even']
ifFalse: ['odd']];
title: 'odd/even';
renderBlock: [:row :col :html |
| text |
text _ col textForRow: row.
text = 'odd'
ifTrue: [html attributeAt: 'bgcolor' put: 'red'].
html tableData: text];
cssClass: 'parity');
Notice that I ignore canChoose since I know that
there is no click block for this column. Render blocks can be
quite messy and I don't recommend the "one huge initialize method"
approach I've used here. The final hideous result looks like

The complete source for my extensions can be found in SCTableReport.st and SCReportColumn.st and the final
FactorialTableComponent source code is here.
(*) Avi Bryant offers a quite reasonable
explanation for the existing design here.
After using this class quite a bit I would choose a diffent model
class (something like the name TableModel would imply) if
a strict MVC design is desired.