Skip to content

EPBDS-17018: an Add or a Remove keeps what was typed in the cells of a useForm table row - #253

Merged
AlexSamBY merged 3 commits into
masterfrom
claude/use-form-rows-add-remove
Oct 28, 2026
Merged

AlexSamBY merged 3 commits into
masterfrom
claude/use-form-rows-add-remove

Conversation

@AlexSamBY

@AlexSamBY AlexSamBY commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

An Add and a Remove keep what was typed in the cells of a table row that useForm gives a form of its own. #251 named both limitations in its Not in this change; this closes them as agreed there (option A): the fields each such row's form changed are written into the document's form before a row is added or removed. The branch merges master with #254, whose forms that hold the whole document the change now meets: After #254 says what that changed here.

The defect

pushDataKindRow and removeDataKindRow (engine/dataKindPush.ts) rebuild the document's data from a copy of its form's values, which hold none of what a useForm row's fields hold, and WithForm starts the form of a row whose data that changes again from it, as it does for a host's new data. Measured on master with #251, React 18.3; in each, getFormData and a submit had what the fields showed:

  • phases whose cells are a title, with lines below each: Design 2 typed in a title went back to Design when a line was removed or added; with lines that have forms of their own as well, a line's edit went too;
  • typed in a row's cell and new in a field below it, in the document's form: an Add showed a in the cell again;
  • rows a, b, c edited to a-edited, b-edited, c-edited: removing the second showed a-edited, c;
  • a row whose form starts from the whole document, an empty (null) row's or one whose cells read the document (relativeData: false), lost its edit on any Add or Remove: the document changes, and the form starts again from it.

Rows that share the document's form keep their edits in all of them. #251 measured the removal the same on 0.35.0.

The fix

  • engine/formData.ts, changedRowsFor(form, forms): each row whose own form holds what the document's form does not, as the document form's copy of it with the fields the row's form changed of its start written at their names (changedInside, startOf, writeField), as the merges write them, shallowest first, an inner row written into the outer one. It leaves out a row its array in the document's form does not hold (one just removed, or a hole in a row form's sparse values), a row whose place there has another shape, which final-form would throw for, and the document's form itself.
  • engine/dataKindPush.ts, keepRowEdits: writes those rows into the document's form with final-form's change, in one batch, right before the push or remove mutator, while each row is still where its form has it. A removal moves an edit with its row, and the data rebuilt from the form holds it; a row the operation moves or changes starts again from that data with its edits, as the document's form does.
  • engine/formData.ts, settleWrittenRows, once the row is added or removed: puts back the rows the operation left as they were (below), and moves the start of every other row written to what the document's form held of it once written, before the row was added or removed (restartWrittenRows, as EPBDS-17018: a useForm table row starts with its values, and a submit sends every row with its edits #251's submit does, handed the document's form as it was then).

Nothing is written in place: final-form makes new values and tells the form's subscribers, and createBatchedForm counts the change for the merge a showIf reads.

After #254

What it does to the rows' forms

Written into the rebuilt data, the edits of a row the Add or the Remove leaves where it was, and as it was, would start its form again too, which nothing did before. Measured with that first version, in the commonest case, an edit and then an Add in the same table: the row's form was no longer dirty (canSave false), and a Required one of its fields showed was gone from the field and from what getValidationErrors told the host, until the field was touched again. The document's submit does not touch a row form's fields, so it would not have come back there either.

So such a row goes back into the rebuilt data as its form's initialValues hold it: its form is handed what it started from, and keeps its edits, its touched fields and its errors, as before this change, and its start, since the data holds none of its edits. A row goes back only where all of these hold:

  • the rebuilt data holds, at its place, exactly the row that was written: the operation neither moved it (a removal, or the compaction a removal does) nor changed it (a line added or removed in its table);
  • the document's form held the row just as the row's form started from it, an object: no edit of the document's form is in it;
  • the row's form holds just that row at its place, as a useForm row's does (engine/Data.tsx): a form that starts from the whole document starts again whatever happens to its row, so its edits must be in the data;
  • every row written inside it can go back as well. Found in review: put back, an outer row took away the value typed in a null line inside it.

Every other row written keeps its edits in the data, and its form's start moves to them (restartWrittenRows). Most such forms start again from the data anyway. One handed a moved row that reads as the one it started from does not: rows x, a, a, the second edited to a1, the first removed. Without the move it wrote its edit over the row that came to its place: measured, getFormData had a1, a1; with it, a1, a. Moving the start of a row put back as well lost its edit from the data, measured: its form gave nothing more, and the data held its start.

Checked as well:

  • rows that share the document's form: unchanged, their values are in its form already; every data test below runs on both kinds;
  • a host's new data still resets the rows, after an Add that put the edits into the data;
  • a table's add row (renderExtraItem) is not data (dataFormsOf), and is not written.

Tests

  • rules.row-form-values.test.js, each on rows with forms of their own and on rows that share the document's form: a line removed and added below a phase whose title was edited; an Add after an edit in a row and below it; an Add after an edit below a row of a field its cells only show; a removal of the middle of three edited rows; a removal that moves a row reading as the edited one into its place; a host's new data after an Add. Lines with forms of their own below phases with forms of their own: the row's and the line's edits through a removal.
  • rules.row-form-values.test.js, each on both kinds of row, in a table beside a useForm block that holds the whole document: a row's edit through an Add and then a Remove of the row before it; a removal that moves a row reading as the edited one into its place.
  • rules.row-form-values.test.js, a row whose form starts from the whole document: a value typed in a null line of a phase that goes back, as a line is added below another phase; an edit in a row whose cells read the document, as a row is added.
  • rules.row-form-values.test.js, a useForm row an Add or a Remove leaves where it was: its edit, in the field and in getFormData, its form still dirty, a Required it shows and the host told of it, through an Add, and the edit set back after it; its form still dirty through a Remove of a later row.
  • formData.test.js, changedRowsFor: the fields the row's form changed written over the document's copy, which it leaves as it was; a row whose edits the document's form holds already, a row past its array, at a hole, at a place of another shape, and the form itself, left out; initial only for an object row that the form holds just so and whose form holds just it; none for a row with a written row inside it that has none; an inner row after the outer one, in it, whichever registered first.
  • formData.test.js, settleWrittenRows: a row left as it was put back, its form's edit still given; a moved row left, its form's start moved; another form that holds the document from its root read as it is, and no start moved for a row not written; an outer row put back with the rows inside it, whose starts stay.

Measured on this branch, with master merged: without the edits written (changedRowsFor giving no row, as on master), eleven of the data tests on rows with forms of their own fail, the two beside a block among them, and the rest pass. Without the move of the starts, the two removals that move a row reading as the edited one into its place fail, in a table alone and beside a block. With the starts of the rows put back moved as well, four fail: the two of a row left where it was, the removal of the middle of three edited rows, whose first row goes back, and the Add and then Remove beside a block, whose edited row the Add puts back. No budget test drives an Add or a Remove, and no budget number moves.

Gates

On Node 24.14.0, at a5e1a06, master merged, each exits 0:

  • lint:js, lint:css, typecheck, typecheck:contract, docs:views:check, docs:props:check, docs:phrases:check, css:fixture:check, build-css:check, sync-version -- --check;
  • npm test and test:react16, test:react17, test:react19 (255 suites, 3503 tests each; one suite skipped on 16 and 17, as designed), and test:coverage with every threshold held (formData.ts 100/99.59/100/100, its one branch left the Select reordering's, as on master; dataKindPush.ts 100%);
  • build-lib, test:css:built, test:types:consumer, test:pack:budget, test:pack:consumer, test:pack:peers, test:env-flags, build;
  • CI=1 test:e2e and CI=1 test:e2e:react19 (77 each), test:pack:browser and test:pack:browser:peers.

npm test and test:react16 failed their first run in later-mount-budget.test.js, which drives no Add or Remove: one test ran past jest's 5 s timeout while the machine's load average stood at 126 beside a virtual machine, and on React 18 the next test failed in its wake (overlapping act calls). Rerun, both exited 0, and the suite alone takes 3.5 s. Every other leg, coverage's run on React 18 among them, passed it the first time. The jest legs ran with <rootDir>/\.claude/ ignored as well, for a stray, gitignored probe in this checkout. Not run here: test:e2e:engines, test:perf and audit:bundled, which CI runs as advisory or weekly jobs; this change adds no dependency.

Not in this change

  • A Remove still compacts the document's top-level tables (compactDataKindArrays): a row with no content goes, wherever the Remove was. A useForm row whose every value was emptied now has that in the data, so a Remove of another row takes it out of its table, as it takes such a row of the document's form. Measured, rows a, b, c with b emptied and c removed: one row left, where master with EPBDS-17018: a useForm table row starts with its values, and a submit sends every row with its edits #251 kept the emptied one and sent it as { kind: '' }. Rows that share the document's form left one row before and after.
  • A row's form handed a row that reads as the one it started from is not started again (WithForm compares by value), so after a removal that moves such a row into its place, it still shows its own edit, as on master with EPBDS-17018: a useForm table row starts with its values, and a submit sends every row with its edits #251; the data now holds the moved row as it is.
  • A useForm block that holds the whole document (A useForm block that holds the whole document is read with its edits #254) starts again from the data an Add or a Remove in a table beside it rebuilds, and loses its own edits: measured, U typed in its title went back to T, in the field, in getFormData and in a submit, on rows of either kind, the same with this change and without it. Written into the document's form as a row's are, its changed fields would stay; not done here.

CLAUDE.md gets one sentence on the new step, beside #251's paragraph on useForm rows.

🤖 Generated with Claude Code

…a `useForm` table row

An Add and a Remove rebuild the document's data from its form, which held none of what a `useForm`
row's fields hold, and a row whose data that changed started again from the document's copy, its
cells' edits gone: a row whose expanded table gained or lost a line, a row whose expanded content had
an edit, every row after a removed one, which moves up a row, and a row whose form starts from the
whole document, an empty row's or one whose cells read the document, on any Add or Remove. Rows that
share the document's form kept their edits.

The fields each such row's form changed are now written into the document's form before the row is
added or removed, while each row is still where its form has it (`changedRowsFor`, `keepRowEdits`).
A row the operation leaves where it was, and as it was, goes back into the rebuilt data as its form
started from it, so that its form keeps its edits, its touched fields and its errors; every other row
written measures its changes from what the document's form held of it once written
(`settleWrittenRows`, `restartWrittenRows`).
…ment

Conflicts, each resolved by keeping both sides:
- `engine/formData.ts`: the imports, `FormState` from #254 and `isEqualJSON` from this branch.
- `formData.test.js`: both appended a `describe`; both are kept.
- `changelog.md`: #251's caveat about an Add and a Remove stays removed; this branch's
  bullet comes before #254's.

`documentValuesOf` is gone: `restartWrittenRows` reads the forms merged from the root
through `documentFormsOf`, their fields and starts too where more than one holds the whole
document (`mergeDocuments`). `settleWrittenRows` hands it those forms by #254's
`mergedFromTheRoot`, the one written replaced by its values right after the write, as
before; that copy has no fields and no start, so it writes no field over the others and its
start never moves, and the Add or the Remove starts the form written again anyway.
…the whole document

Since #254 the merges take, from each form that holds the whole document, the fields it
changed (`mergeDocuments`), and `restartWrittenRows` reads such forms that way. Two tests,
each on rows with forms of their own and on rows that share the document's form, in a table
beside such a block: a row's edit through an Add and then a Remove of the row before it, and
an edit kept in its row as a row that reads the same moves into its place, in the field,
`getFormData` and a submit.

Measured on this branch: without the edits written, both fail on rows with forms of their
own; without the starts moved, the second fails; with the starts of the rows put back moved
too, the first fails.
@AlexSamBY
AlexSamBY merged commit 1895624 into master Oct 28, 2026
8 checks passed
@AlexSamBY
AlexSamBY deleted the claude/use-form-rows-add-remove branch October 28, 2026 11:01
@AlexSamBY AlexSamBY mentioned this pull request Oct 28, 2026
AlexSamBY added a commit that referenced this pull request Oct 28, 2026
Release 0.35.1: the version, and the changelog's Unreleased section as
`v0.35.1`.

## What changes

- `package.json` and `package-lock.json` read 0.35.1, set by `npm
version 0.35.1 --no-git-tag-version`. Its `version` script set the
`data-version` of `src/library/AppWrapper.tsx` and `public/index.html`.
- `src/demo/markdowns/changelog.md`: `### Unreleased` is `### v0.35.1`,
with a paragraph on what the release is.

## What the release holds

The three changes merged since 0.35.0 (8e76778), each with its own
changelog entries:

- #251 (EPBDS-17018): a table row that `useForm` gives a form of its own
starts with its values, and a submit sends every row with its edits. A
`showIf` that reads the form merges the document's forms once, not once
per node.
- #254: an edit in a `useForm` block that holds the whole document is in
what the document reads.
- #253 (EPBDS-17018): an Add or a Remove keeps what was typed in the
cells of such a row.

None of them changes what a host builds against:
`src/library/contract.ts`, `src/library/main.tsx`, the peer range and
the locked dependencies are as in 0.35.0. The build bundles the same 19
packages, at the same versions, as 0.35.0's (`dist/sbom.cdx.json`), and
`audit:bundled` finds nothing in them.

## How the changelog was audited

Each of the three commits since 0.35.0 touched the changelog (`git
diff-tree --name-only`), and nothing in the section reads wrong once
released. The section states no package sizes.

## Checked

On this branch's commit df8734c, over master's 1895624, with Node
24.14.0, 27 gates exit 0:

- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`, the three
docs checks, `css:fixture:check`, `build-css:check`, `sync-version
--check`;
- `npm test`, `test:react16`, `test:react17` and `test:react19`: 255
suites and 3503 tests each, one suite skipped on 16 and 17; coverage
with its thresholds;
- `build-lib`, `test:css:built`, `test:types:consumer`,
`test:pack:budget`, `test:pack:consumer`, `test:pack:peers`,
`test:env-flags`, the demo build;
- `CI=1 test:e2e` and `CI=1 test:e2e:react19`, 77 each;
`test:pack:browser` and `test:pack:browser:peers`.

Notes on these runs:

- The first run of `test:react17` lost a jest worker to a SIGSEGV, in
`rules.popup-content.test.js`. Its crash report has the stack of the V8
bug `nodejs/node#62393`, which Node 24.14.0 has. The rerun exited 0.
- The jest legs ran with `<rootDir>/\.claude/` ignored as well, for a
stray, gitignored probe in this checkout.
- Not run here: `test:e2e:engines` and `test:perf`, which CI runs as
advisory jobs.

`npm pack --dry-run` on this branch: 26 files, 3.8 MB unpacked, 1.0 MB
packed, `dist/index.js` 338.5 KB (0.35.0: 332 KB), shasum
`5c1e5655763232d79a83f2ffb7c1e014e88d7fea`.

## After it merges

What publishes the release, in this order:
1. Tag the squash commit `v0.35.1`, and push the tag.
2. `npm publish`. Its `prepack` checks the version sites and the
generated docs, and builds the library.
3. `npm run build && npm run deploy`. The changelog reaches hosts
through the demo site, so the deploy is part of the release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant