Chapter 16 of 34

Footer models: totals and key figures

A footer that sums up exactly the rows you see – and follows filter and sorting.

After this chapter you can

  • write a footer area model
  • sum the filtered rows only once
  • find columns by property

A footer is an area model

footerData (chapter 14 (Array of arrays)) is static. A footer that shows totals of exactly the rows the user sees is a small class of its own: it extends AreaModel and reads the displayed rows of the body with AreaModelObjectArray.getFilteredRows() – after filter and sorting.

All rows pass the external filter and the sort order; the result feeds the body, the footer totals, copy and the Excel export of the view all rows getAllRows() filter externalFilterFunction sort comparator / sortByColumn displayed rows getFilteredRows() body (visible part) footer totals copy · Excel view externalFilterChanged() runs the filter again and keeps the last sort order. ExcelService with getAllRows() ignores filter and sorting.

The footer model

src/model/order-totals-footer.ts
import { AreaModel, type AreaModelObjectArray, type CellRendererIf, type ColumnDefIf } from "@guiexpert/table";
import type { Order } from "./orders";
// Episode 016 – a footer that follows filter and sorting: it reads the rows the body currently shows.
export class OrderTotalsFooterModel extends AreaModel {
constructor(
private readonly getBody: () => AreaModelObjectArray<Order>,
private readonly columnDefs: ColumnDefIf[],
private readonly rowHeight = 34) {
super();
this.areaIdent = "footer";
}
override getRowCount(): number { return 1; }
override getRowHeight(_rowIndex: number): number { return this.rowHeight; }
// Episode 025 – an own area model chooses its renderers itself: here the footer renderer of the column
override getCellRenderer(_rowIndex: number, columnIndex: number): CellRendererIf | undefined {
return this.columnDefs[columnIndex]?.rendererMap?.footer;
}
private cache?: { rows: Order[]; length: number; qty: number; total: number };
override getValueAt(_rowIndex: number, columnIndex: number): any {
const t = this.totals();
switch (this.columnDefs[columnIndex]?.property) { // by property, not by position
case "customer": return `${t.length.toLocaleString("en")} orders`;
case "quantity": return t.qty;
case "total": return Math.round(t.total * 100) / 100;
}
return "";
}
/** Episode 031 – values changed (editing, live data): sum again on the next repaint. */
invalidate(): void {
this.cache = undefined;
}
// sum once per filter result, not once per repaint
private totals() {
const rows = this.getBody().getFilteredRows();
if (this.cache?.rows !== rows || this.cache.length !== rows.length) {
let qty = 0, total = 0;
for (const o of rows) { qty += o.quantity; total += o.total; }
this.cache = { rows, length: rows.length, qty, total };
}
return this.cache;
}
}
  • One row: getRowCount() returns 1, getRowHeight() its height.
  • By property, not by position: the switch looks at columnDefs[columnIndex].property. Hidden or moved columns (chapter 18 (Fixed columns)) don't break it.
  • Sum once per filter result: getValueAt() is called for every visible footer cell on every repaint – the cache keeps that cheap even for 100,000 rows.
  • getCellRenderer() lets the footer render the total as euro (chapter 25 (Cell renderers)).

Plug it in

const tableModel = TableFactory.createTableModel({
rows: orders,
columnDefs: columns,
footerAreaModel: new OrderTotalsFooterModel(
() => tableModel.getBodyModel() as AreaModelObjectArray<Order>, columns)
});

The footer needs the body model, which only exists after createTableModel – so it gets a function (() => tableModel.getBodyModel()) instead of the model itself. Pass it as footerAreaModel (CreateTableModelPara).

Live Totals that follow the filter
Screenshot: Totals that follow the filter Screenshot: Totals that follow the filter
Type “berlin” – the footer shows the number of orders and the sums of only these rows.

Next

A model without any array behind it – two million rows computed on demand: chapter 17 (Own AreaModel).

API used in this chapter