Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,24 @@ whose `showIf` reads the form hears the other forms of its document tree too, wh
what the form held before, and the tree keeps its latest tells, so a node catches up as it commits with those made
after it rendered.

Every such decision reads the merge of its tree's forms, which the tree's nodes share while what the page's forms hold
stays the same (`keptRawFormsData`, since 2026-10-26): `formsStorage` counts each form that registers, goes or
registers again, and a form a document renders counts its new values before the first of its subscribers hears of them
(`createBatchedForm`). A tree's tells come too late for that: the form layer subscribes as the form commits, after the
nodes under it. A change written into a form's values in place, which final-form tells nobody of, must be counted
where it is made (`countFormsChange`), as the `submit` action's is. A `useForm` row's form holds its row at the row's
place, as its fields read it (`engine/Data.tsx`), and every merge of the forms takes from it the values of the fields
it changed and nothing else, each read and written at the field's name (`rowPlaceOf`, `changedInside`, `writeField`,
`engine/formData.ts`). It changed a field whose value is not its start: its `initialValues`, moved, field by field, to
what the forms holding the document from its root hold of its values after a submit (`restartWrittenRows`, which the
`submit` action calls as it writes; a submit a row's own document makes writes into the row's form, and moves no
start). The rest of the row, a field its cells only show included, is what the document's form holds: what another
form, such as the row's expanded content's, has edited there, and what a submit has written of the row's form. A
new write of a row's values into the document's form must move the start the same way. Its fields are every one it
has registered, the ones a `showIf` hid included (`fieldNamesOf` in `createBatchedForm`). A name that comes is no
change of what the forms hold: the kept merge is checked against the values of the names that came
(`fieldNamesCount`, `holdsFields`), so a burst of registrations, a mount's among them, merges nothing more.

A mount has a cost besides renders: every field registers with final-form. Until 2026-10-13, each registration
published the state of every field before it and, once validation ran, validated them all, and each unregistration
did the same. A burst of n fields cost about n²/2 of each, on the first render, a tab switch, a `showIf` that flips
Expand Down
24 changes: 20 additions & 4 deletions src/core/engine/Data.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react'
import React, { useMemo } from 'react'
import { setIn } from 'final-form'
import { Active } from '../utils'
import { isObject } from '../utils/object'
import DocumentWithOwnForm from './rules'
import { rowPlaceOf } from './formData'
import LocalDraftTableRow from './components/LocalDraftTableRow'
import type { LocalDraftTableRowProps } from './components/LocalDraftTableRow'

Expand All @@ -24,8 +27,11 @@ export type DataMeta = { view?: string, meta?: DataMeta, [key: string]: unknown
export type DataProps = {
/** Data of the same `kind` are grouped into an array, and validated together as a group. */
kind: string
/** The UI Render instance containing this Data component; a draft row reads its data kinds */
instance: NonNullable<LocalDraftTableRowProps['parentInstance']>
/**
* The UI Render instance containing this Data component; a draft row reads its data kinds. None for a node rendered
* outside a document's render: a table's `extraItems`, a tooltip's `children`, an input's `icon`, a `values` renderer.
*/
instance?: LocalDraftTableRowProps['parentInstance']
/** The index of this Data component in the array of rendered data, for removing itself */
index?: number | string
/** Data.json to use */
Expand Down Expand Up @@ -91,6 +97,16 @@ export default function Data ({
? {...metaIn, relativePath, relativeIndex}
: metaIn

// A table row that `useForm` gives a form of its own names its fields by the row's place, `dataKind.items[1].kind`
// (`mapper.tsx`), so its form starts from the row's values there, placed as final-form places a field's value. Until
// 2026-10-26 it started from them at the root of its values, where none of its fields reads: every field of the row
// started empty. A table's add row (`draftRow`) starts from the `initialValues` its meta declares, as before, and so
// does a row that holds no object: for a `null` row, `data` is the document's (`mapper.tsx`), which placed there put a
// copy of the document inside the row.
const rowPlace = useForm && !draftRow ? rowPlaceOf(meta) : undefined
const place = rowPlace && isObject(initialValues) && initialValues !== (instance && instance.data) ? rowPlace : undefined
const formValues = useMemo(() => place ? setIn({}, place, initialValues) : initialValues, [place, initialValues])

// TableCells only, or Data wrapping TableCells (e.g. renderExtraItem). Draft values stay in React state
// until Add — no final-form fields at dataKind.*[nextIndex], so an empty `{}` is not materialized for the draft row.
if (localDraft) {
Expand All @@ -117,7 +133,7 @@ export default function Data ({
return <UIRenderWithUISetup
data={data}
meta={meta}
initialValues={initialValues}
initialValues={formValues}
form={{kind}}
parent={instance}
index={rowIndex}
Expand Down
5 changes: 3 additions & 2 deletions src/core/engine/__tests__/formData.select-order.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {
getRawFormsData,
} from '../formData'

const makeForm = values => ({
const makeForm = (values, fields = []) => ({
getState: () => ({values}),
getRegisteredFields: () => fields,
})

const indexMeta = (mapOptions = {text: 'code', value: '{index}'}) => ({
Expand Down Expand Up @@ -190,7 +191,7 @@ describe('raw and structured forms data boundaries', () => {
}],
['nested-row', {
// Its fields are named by its row's place, `root.rows[1].value`.
form: makeForm({root: {rows: [undefined, {value: 'nested'}]}}),
form: makeForm({root: {rows: [undefined, {value: 'nested'}]}}, ['root.rows[1].value']),
meta: {relativePath: 'root.rows', relativeIndex: 1},
}],
])
Expand Down
Loading
Loading