Lattice Grid Buy a licence

blog

Totals for Selected Rows: a Derived Grid, Not a Filter

Building…
Loading a live grid…

A collections call runs on specifics. The customer agrees these three invoices, not the whole account, not last quarter, just the ones named one at a time as the call goes on. The rep ticks each order as it comes up and needs the total of exactly those rows before the call ends, and it has to stay right if a fourth order gets added to the list halfway through. Nothing about “just the ones mentioned on this call” is a region, a date range or a status. A filter has no field to grab, because the only thing those rows share is that someone picked them.

A derived grid can still answer it, by reading a different signal than a filter leaves. Earlier posts in this series point a summary grid at whatever rows a filter leaves in the source; this one points it at whatever rows are ticked instead. Turn on the orders grid’s own checkbox column, follow it with follow: 'selected', and the totals panel reduces exactly those rows, live, on every tick and untick, with no field to filter on and no second query to keep in step.

That removes the mental subtotal, and the habit of pasting the ticked rows into a spreadsheet to add them up while the customer waits. The total is on the page the moment the last row is ticked, and it is still correct if the list changes under it, because the derived grid rereads the selection rather than a snapshot of it. The page below opens with order 3 already ticked, so the panel never sits empty waiting for the rep’s first tap.

Plain JavaScript

import { createGrid } from '@toclocoinc/lattice-grid';

const orders = [
  { id: 1,  amount: 180,  flag: '' },
  { id: 2,  amount: 940,  flag: '' },
  { id: 3,  amount: 265,  flag: 'Disputed' },
  { id: 4,  amount: 410,  flag: '' },
  { id: 5,  amount: 75,   flag: '' },
  { id: 6,  amount: 1200, flag: '' },
  { id: 7,  amount: 330,  flag: 'Disputed' },
  { id: 8,  amount: 560,  flag: '' },
  { id: 9,  amount: 95,   flag: '' },
  { id: 10, amount: 720,  flag: '' },
  { id: 11, amount: 410,  flag: '' },
  { id: 12, amount: 290,  flag: 'Disputed' },
  { id: 13, amount: 860,  flag: '' },
  { id: 14, amount: 150,  flag: '' },
  { id: 15, amount: 500,  flag: '' },
  { id: 16, amount: 980,  flag: '' },
  { id: 17, amount: 215,  flag: '' },
  { id: 18, amount: 640,  flag: 'Disputed' },
  { id: 19, amount: 305,  flag: '' },
  { id: 20, amount: 455,  flag: '' },
];

const book = createGrid(document.querySelector('#orders'), {
  rowKey: 'id',
  selection: { mode: 'multiple', checkbox: true, headerCheckbox: true },
  columns: [
    { field: 'id',     title: 'Order' },
    { field: 'amount', title: 'Amount', type: 'number' },
    { field: 'flag',   title: 'Status' },
  ],
  rows: orders,
});

// A second grid whose rows are the total for whatever is ticked above.
const totals = createGrid(document.querySelector('#totals'), {
  source: {
    mode: 'derived',
    from: book,
    follow: 'selected',
    groupBy: [],       // no dimension to group by, so the ticked rows reduce to one row
    select: {
      revenue: { of: 'amount', fn: 'sum' },
      orders:  { fn: 'count' },
    },
  },
  columns: [
    { field: 'revenue', title: 'Revenue', type: 'number' },
    { field: 'orders',  title: 'Orders',  type: 'number' },
  ],
});

// Order 3 is already named on the call by the time the page opens, so the
// panel starts with its total rather than sitting empty. Selection keys are
// strings, so the row key goes in as '3', not the number.
book.selection.set(['3']);

With order 3 ticked on load, totals already holds revenue 265, orders 1. Tick the other three orders flagged Disputed (7, 12 and 18) in the checkbox column and totals moves to revenue 1,525, orders 4. Untick one of them and it drops to 885 and 3 on the next frame, with nothing wired between the two grids beyond the from above.

React

import React from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';

// Build the component once, at module scope, not on every render.
const LatticeGrid = createLatticeGrid({ React, createGrid });

function SelectedTotals({ orders }) {
  const bookRef = React.useRef(null);
  const [book, setBook] = React.useState(null);

  const orderColumns = [
    { field: 'id',     title: 'Order' },
    { field: 'amount', title: 'Amount', type: 'number' },
    { field: 'flag',   title: 'Status' },
  ];

  const totalColumns = [
    { field: 'revenue', title: 'Revenue', type: 'number' },
    { field: 'orders',  title: 'Orders',  type: 'number' },
  ];

  React.useEffect(() => {
    const grid = bookRef.current.grid;
    // Order 3 is already named on the call by the time the page opens, so
    // the panel starts with its total rather than sitting empty. Selection
    // keys are strings, so the row key goes in as '3', not the number.
    grid.selection.set(['3']);
    setBook(grid);
  }, []);

  return (
    <>
      <LatticeGrid
        ref={bookRef}
        rowKey="id"
        columns={orderColumns}
        rows={orders}
        selection={{ mode: 'multiple', checkbox: true, headerCheckbox: true }}
        style={{ height: 320 }}
      />
      {book && (
        <LatticeGrid
          columns={totalColumns}
          source={{
            mode: 'derived',
            from: book,
            follow: 'selected',
            groupBy: [],
            select: {
              revenue: { of: 'amount', fn: 'sum' },
              orders:  { fn: 'count' },
            },
          }}
          style={{ height: 120 }}
        />
      )}
    </>
  );
}

The adapter forwards a ref whose .grid is the real grid instance; the effect reads it once the orders grid has mounted, calls selection.set on it the same way the plain example does, and only then renders the totals panel. Ticking a row in the checkbox column moves the totals panel the same way it does in the plain example, because both read the orders grid’s own selection, not a copy of it.

The options used

Option What it does
mode: 'derived' This grid’s rows are computed from another grid’s, not loaded from an array or a server.
from The grid to read.
follow: 'selected' Read whichever rows are currently selected on the source grid, rather than the filtered set.
groupBy: [] No dimension to group by, so every selected row reduces to a single row, the total for all of them, instead of passing the rows through unchanged.
select The reduced columns, by output id: of names the field to reduce, fn is any totals function: sum, count, avg, median, p95 and more.
selection On the source grid: mode: 'multiple' allows ticking more than one row; checkbox adds the pinned column that ticks them; headerCheckbox adds the tri-state select-all above it.

What the example actually produced

Running the plain JavaScript example above with createHeadlessGrid from the published package, setting the selection and printing totals’ row, gives:

Selected orders Revenue Orders
3 (page load) 265 1
3, 7, 12, 18 1,525 4
3, 7, 12 885 3

Untick every order and totals holds no rows at all rather than a row of zeroes, but that is not the state the page opens in: order 3 is ticked before the rep touches anything, so the panel always has a figure on it.

The derived and chained grids guide covers groups, joins and chaining a derived grid from a derived grid in full. For the checkbox column and the select-all box this post’s selection runs on, see how to select rows with checkboxes.

Read next

All posts RSS