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.
The footer model
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).
Next
A model without any array behind it – two million rows computed on demand: chapter 17 (Own AreaModel).