Data grid / foldkit-data-grid

Editing

A column with edit is editable. The grid owns the edit session (which cell, the draft, an error); the application owns the value, and hears of a commit as the grid's OutMessage. The column's schema says what its text means, from the text typed to the value: a draft is committed only when it decodes, and the failure's message is the cell's error. A schema that decodes to a date (Schema.DateFromString) is CellEditor.Date and edits in a date field. The draft the field opens on is the local calendar day, YYYY-MM-DD. toISOString is the UTC day, so a local midnight used to open the field on the wrong one. A column draft that returns a string still wins. Committing decodes the field's text through the schema.

ts
// Dollars as typed, with at most two decimals, and the number they mean.
const Price = Schema.String.check(
  Schema.isPattern(/^\d+(\.\d{1,2})?$/, { message: 'A price, like 4.99' }),
).pipe(
  Schema.decodeTo(Schema.Number, {
    decode: SchemaGetter.transform(text => Number(text)),
    encode: SchemaGetter.transform(price => price.toFixed(2)),
  }),
)
const priced = Columns.define<Product>()({
  sku: { header: 'SKU', value: product => product.sku },
  price: {
    header: 'Price',
    value: product => product.price,
    edit: { schema: Price, draft: product => Schema.encodeSync(Price)(product.price) },
  },
})
const Prices = DataGrid.make({ id: 'prices', columns: priced })
// Edited({ row, column, text }) | Pasted({ accepted, refused }) | Filled({ source, to })
// | UndoRequested | RedoRequested
type Out = typeof Prices.Out.Type

// In `onOut`: the committed text read back as its column's value, a number here.
const repriced = (cell: Extract<Out, { _tag: 'Edited' }>) =>
  Prices.matchEdit(cell, { price: ({ row, value }) => ({ row, price: value }) })
  • EditStarted({ address, draft }) opens an edit on an editable cell; EditChanged keeps the draft; EditCancelled drops it.
  • EditCommitted({ next, reveal }) decodes the draft with the column's schema (any text, without one). A draft it refuses keeps the edit with the error, and nothing is reported. One it accepts ends the edit, moves focus to next, and the update returns the OutMessage Out.Edited({ row, column, text }), unless it is unchanged: the text the edit began from, or text that decodes to the same value (09.0 for 9.00). An unchanged edit closes as a changed one does and reports nothing. It is judged against the text the edit began from, not the cell as it is now, so leaving an editor as it opened never writes over a change another device made meanwhile.
  • Grid.matchEdit(cell, handlers) reads a reported cell's value back: one handler per editable column, each given its column's decoded type, so the application parses nothing itself. The OutMessage stays text, which a parent can store or send; a cell that does not decode, or names a column that does not edit, was not the grid's, and throws.
  • A key typed on the grid is EditTyped({ address, text, from }): it starts the edit from from, the text the cell showed, or adds to the one open on that cell, so keys faster than the editor opens all count. On a choice column it finds the next option the key starts instead. EditStepped({ by }) moves a choice's draft through its options (next, previous, first, last, pageNext, pagePrevious), and EditChosen({ draft }) keeps one a pointer chose. The grid's Messages carry what the user did and update works out what it means, for the reason in Messages carry intent.
  • Place the grid with onOut. Every placement handles the OutMessage, as any Bundle's must; a grid that edits nothing passes onOut: Bundle.ignore.
  • A click on another cell commits first; a refused draft keeps the edit and the click waits. Hiding the edited column ends the edit.