Edit a record and its related rows as one, 1.107
An order and its lines usually live on two screens. The header is a form with a save button. The lines are a grid with another save button. Change the customer and a quantity, press one, forget the other, and the order that reaches the database is one nobody typed.
The example above is that order in a single record editor. The form on top holds the order, the grid below holds its lines, and both sit on the same rows. Change the status and a quantity, then watch the “Unsaved changes” badge: it covers both. This is the first of four things in 1.107, and the other three follow the same idea. The rows you already have can serve a form, a grid and an explanation, so you stop copying them into something else to get an answer.
Edit a record and its related rows as one
createRecordEditor binds a form to one row and any number of child views to that row’s related rows. A child can be a grid, a list, a tree, a board, a data view or a chart. The form and every child share three things:
- One dirty state.
isDirty()is true if the form or any child has an unsaved edit, and adirtyevent tells you when it changes. Wire it to a leave-this-page guard and you are done. - One validation pass.
validate()checks the form, then each child, and reports the first failure by view and field, such as the lines grid and its Qty column. - One batched save.
save()hands your store every changed row in a single call.
Tick “Reject the next save” in the example and press Save order. The store throws, the whole batch rolls back and the editor stays dirty. A half-saved record cannot happen, because there is no half.
const editor = createRecordEditor(el, {
source, key: 'SO-4471',
form: { rows: [{ cols: [{ type: 'input', name: 'customer', required: true }] }] },
children: [{ id: 'lines', type: 'grid', relation: { from: 'id', to: 'orderId' }, config: { columns } }],
});
editor.on('dirty', ({ dirty }) => saveButton.disabled = !dirty);
if (!editor.validate()) await editor.save();
Explain a number from the totals row
“Why is this total up?” gets answered today by exporting the rows to a spreadsheet, and the answer is stale by the time anyone reads it. Open Explain on a total, a group total, a pivot cell or a KPI tile and a panel ranks what the figure is made of, with each contributor’s share. Give it a comparison and it ranks the drivers of the change instead.
In the example below, press “Why is September up on August?”. The panel names the product that moved the most, and the line above the grid adds the drivers up and checks them against the totals row. Every figure matches it exactly, because the panel reads the same rows as the total.
You can also reach it from the grid itself: right-click a total and choose Explain this number. In code, grid.explain({ column: 'revenue', group: 'region' }) returns the contributors, and adding compare returns the drivers.
Test whether a difference is real
“Is this difference real?” gets answered by a notebook. Someone exports two groups, remembers which test applies, runs it and pastes back a p-value that means little to the person who asked.
statistics.compare does the choosing. Hand it a measured column and a grouping column and it picks the test for two or more groups, says why, and returns the statistic, the p-value, a confidence interval, an effect size with its label and a one-sentence verdict. It never returns a bare “significant” flag, because a p-value is something to read, not a ruling.
In the example below, night-shift tickets take longer to handle than day-shift ones. The card prints the verdict, the test and the reason it was chosen. Switch to the three regions and the grid chooses a different test, and tells you so.
Your users do not need the code. A Compare groups item in the column menu opens the same result beside the column. No correction is applied when you run several comparisons, so the statistics reference says what that means before you run twenty of them.
Write xlsx without a library
Getting a workbook out of an app usually means adding a spreadsheet library, and often a grid on the page to feed it. LatticeGrid.xlsx writes a multi-sheet workbook from plain rows or from a grid, reads every sheet back as rows, and turns a sheet into CSV. It needs no grid on the page and no third-party code.
const book = await LatticeGrid.xlsx.write({
filename: 'orders.xlsx',
sheets: [
{ name: 'Summary', rows: [['Order', 'Total'], ['SO-4471', 3280]] },
{ name: 'Lines', rows: grid },
],
});
const { sheets } = await LatticeGrid.xlsx.read(book);
const csv = LatticeGrid.xlsx.toCsv(sheets[1]);
The export to Excel guide covers formats and column widths.
Upgrading from 1.106
There is nothing to change. The record editor, Explain, Compare groups and the xlsx writer are new surface and opt in by use.
The changelog has the full list. The grid engine page covers the core all four build on. Open the order above, change a quantity and a status, and press Save once.