Skip to content

Commit 116ee74

Browse files
author
Soare Robert-Daniel
committed
dev: use tanstack form
1 parent 9f9b6ed commit 116ee74

13 files changed

Lines changed: 705 additions & 39 deletions

package-lock.json

Lines changed: 104 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@chakra-ui/react": "^2.10.9",
6060
"@emotion/react": "^11.14.0",
6161
"@emotion/styled": "^11.14.1",
62+
"@tanstack/react-form": "^1.29.0",
6263
"framer-motion": "^12.38.0"
6364
}
6465
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* Bridges TanStack Form state to legacy editor props (values / onChange) while syncing editDraft upward.
3+
*
4+
* ## Do not “fix” by always passing `defaultValues: editDraft`
5+
*
6+
* `useForm` calls `formApi.update(opts)` on every render. If `opts` includes `defaultValues` taken from
7+
* the parent `editDraft` prop, that object can lag the TanStack store by one React frame: after
8+
* `form.reset(next)` from a Chakra `onChange`, `useStore` re-renders this component before
9+
* `setEditDraft` from `listeners.onChange` has committed. `FormApi.update` may then treat parent
10+
* `defaultValues` as the source of truth and overwrite `values` (see `@tanstack/form-core` `FormApi.update`:
11+
* `shouldUpdateValues` when `!isTouched` — legacy inputs are not TanStack `Field`s, so the form often
12+
* stays “untouched”). Result: inputs look broken (e.g. empty “Post ID” / `default_value`).
13+
*
14+
* We pass `defaultValues` only on the **first** options object, then drop it via
15+
* `includeDefaultValuesInOpts` after `useLayoutEffect`, so subsequent `update({ listeners })` calls
16+
* never push a stale `editDraft` snapshot into the store. Remount per field via `key={clientId}` still
17+
* gets a fresh initial `defaultValues` when switching rows.
18+
*
19+
* ## Parent `editDraft` replaced in place (same `clientId`)
20+
*
21+
* After mount we no longer pass `defaultValues` from props, so if the parent replaces `editDraft`
22+
* (undo, server refresh) without remounting, we `form.reset(editDraft)` when a stable serialization of
23+
* the incoming prop differs from the form store. User edits stay in sync because the listener updates
24+
* the parent before the serialized snapshot diverges.
25+
*/
26+
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from '@wordpress/element';
27+
import { useStore } from '@tanstack/react-form';
28+
import { Alert, AlertIcon, Skeleton, VStack } from '@chakra-ui/react';
29+
import { FieldSettingsForm } from '../FieldSettingsForm';
30+
import { usePpomAppForm } from '../form/ppomForm';
31+
import type { FieldRow } from '../types/fieldModal';
32+
import type { FieldModalManageStepProps } from '../types/fieldModal';
33+
34+
export type FieldManageEditorBridgeProps = FieldModalManageStepProps;
35+
36+
type FieldManageEditorBridgeInnerProps = Omit<
37+
FieldModalManageStepProps,
38+
'editDraft' | 'fields' | 'onOpenPicker'
39+
> & {
40+
editDraft: FieldRow;
41+
};
42+
43+
/** Stable JSON for comparing field rows regardless of key insertion order. */
44+
function stableStringifyFieldRow( value: unknown ): string {
45+
if ( value === null || typeof value !== 'object' ) {
46+
return JSON.stringify( value );
47+
}
48+
if ( Array.isArray( value ) ) {
49+
return (
50+
'[' +
51+
value.map( ( item ) => stableStringifyFieldRow( item ) ).join( ',' ) +
52+
']'
53+
);
54+
}
55+
const obj = value as Record<string, unknown>;
56+
const keys = Object.keys( obj ).sort();
57+
return (
58+
'{' +
59+
keys
60+
.map(
61+
( k ) =>
62+
JSON.stringify( k ) + ':' + stableStringifyFieldRow( obj[ k ] )
63+
)
64+
.join( ',' ) +
65+
'}'
66+
);
67+
}
68+
69+
export function FieldManageEditorBridge( props: FieldManageEditorBridgeProps ) {
70+
if ( ! props.editDraft ) {
71+
return null;
72+
}
73+
const { fields: _fields, onOpenPicker: _onOpenPicker, ...rest } = props;
74+
return (
75+
<FieldManageEditorBridgeInner
76+
key={ props.editDraft.clientId }
77+
{ ...rest }
78+
editDraft={ props.editDraft }
79+
/>
80+
);
81+
}
82+
83+
function FieldManageEditorBridgeInner( {
84+
i18n,
85+
selectedId: _selectedId,
86+
editDraft,
87+
schemaLoading,
88+
activeSchema,
89+
TypedEditor,
90+
onEditDraftChange,
91+
ppomFieldIndex,
92+
modalContext,
93+
}: FieldManageEditorBridgeInnerProps ) {
94+
/** See file docblock — must flip to false after mount so `usePpomAppForm` stops receiving `defaultValues: editDraft` every render. */
95+
const [ includeDefaultValuesInOpts, setIncludeDefaultValuesInOpts ] =
96+
useState( true );
97+
98+
const listeners = useMemo(
99+
() => ( {
100+
onChange: ( {
101+
formApi,
102+
}: {
103+
formApi: { state: { values: unknown } };
104+
} ) => {
105+
onEditDraftChange( formApi.state.values as FieldRow );
106+
},
107+
} ),
108+
[ onEditDraftChange ]
109+
);
110+
111+
// After first paint, only pass `listeners` into `usePpomAppForm` (see file docblock).
112+
useLayoutEffect( () => {
113+
setIncludeDefaultValuesInOpts( false );
114+
}, [] );
115+
116+
const form = usePpomAppForm(
117+
includeDefaultValuesInOpts
118+
? {
119+
defaultValues: editDraft,
120+
listeners,
121+
}
122+
: { listeners }
123+
);
124+
125+
useEffect( () => {
126+
const incoming = stableStringifyFieldRow( editDraft );
127+
const current = stableStringifyFieldRow( form.state.values );
128+
if ( incoming !== current ) {
129+
form.reset( editDraft );
130+
}
131+
}, [ editDraft, form ] );
132+
133+
const values = useStore( form.store, ( s ) => s.values ) as FieldRow;
134+
135+
const bridgeOnChange = useCallback(
136+
( action: Parameters< typeof onEditDraftChange >[ 0 ] ) => {
137+
const prev = form.state.values as FieldRow;
138+
const next =
139+
typeof action === 'function'
140+
? ( action as ( p: FieldRow | null ) => FieldRow | null )(
141+
prev
142+
)
143+
: action;
144+
if ( next === null || next === undefined ) {
145+
return;
146+
}
147+
form.reset( next );
148+
},
149+
[ form ]
150+
);
151+
152+
return (
153+
<VStack align="stretch" spacing={ 3 }>
154+
{ schemaLoading && ! activeSchema && (
155+
<VStack spacing={ 2 } align="stretch">
156+
<Skeleton height="36px" />
157+
<Skeleton height="36px" />
158+
<Skeleton height="72px" />
159+
</VStack>
160+
) }
161+
{ activeSchema && TypedEditor && (
162+
<TypedEditor
163+
schema={ activeSchema }
164+
values={ values }
165+
onChange={ bridgeOnChange }
166+
i18n={ i18n }
167+
ppomFieldIndex={ ppomFieldIndex }
168+
modalContext={ modalContext }
169+
/>
170+
) }
171+
{ activeSchema && ! TypedEditor && (
172+
<FieldSettingsForm
173+
schema={ activeSchema }
174+
values={ values }
175+
onChange={ bridgeOnChange }
176+
fieldType={ editDraft.type || '' }
177+
i18n={ i18n }
178+
ppomFieldIndex={ ppomFieldIndex }
179+
modalContext={ modalContext }
180+
isFallback
181+
/>
182+
) }
183+
{ ! schemaLoading && ! activeSchema && editDraft.type && (
184+
<Alert status="info">
185+
<AlertIcon />
186+
{ i18n.unsupportedControl }
187+
</Alert>
188+
) }
189+
</VStack>
190+
);
191+
}

packages/admin/field-modal/src/components/FieldManagePanel.tsx

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Manage step: empty state or active field editor (typed, fallback, or unsupported).
33
*/
4-
import { Box, Button, VStack, Text, Alert, AlertIcon, Skeleton } from '@chakra-ui/react';
5-
import { FieldSettingsForm } from '../FieldSettingsForm';
4+
import { Box, Button, VStack, Text } from '@chakra-ui/react';
5+
import { FieldManageEditorBridge } from './FieldManageEditorBridge';
66
import type { FieldModalManageStepProps } from '../types/fieldModal';
77

88
export type FieldManagePanelProps = FieldModalManageStepProps;
@@ -44,43 +44,19 @@ export function FieldManagePanel( {
4444
</VStack>
4545
) }
4646
{ fields.length > 0 && selectedId && editDraft && (
47-
<VStack align="stretch" spacing={ 3 }>
48-
{ schemaLoading && ! activeSchema && (
49-
<VStack spacing={ 2 } align="stretch">
50-
<Skeleton height="36px" />
51-
<Skeleton height="36px" />
52-
<Skeleton height="72px" />
53-
</VStack>
54-
) }
55-
{ activeSchema && TypedEditor && (
56-
<TypedEditor
57-
schema={ activeSchema }
58-
values={ editDraft }
59-
onChange={ onEditDraftChange }
60-
i18n={ i18n }
61-
ppomFieldIndex={ ppomFieldIndex }
62-
modalContext={ modalContext }
63-
/>
64-
) }
65-
{ activeSchema && ! TypedEditor && (
66-
<FieldSettingsForm
67-
schema={ activeSchema }
68-
values={ editDraft }
69-
onChange={ onEditDraftChange }
70-
fieldType={ editDraft.type || '' }
71-
i18n={ i18n }
72-
ppomFieldIndex={ ppomFieldIndex }
73-
modalContext={ modalContext }
74-
isFallback
75-
/>
76-
) }
77-
{ ! schemaLoading && ! activeSchema && editDraft.type && (
78-
<Alert status="info">
79-
<AlertIcon />
80-
{ i18n.unsupportedControl }
81-
</Alert>
82-
) }
83-
</VStack>
47+
<FieldManageEditorBridge
48+
i18n={ i18n }
49+
fields={ fields }
50+
selectedId={ selectedId }
51+
editDraft={ editDraft }
52+
schemaLoading={ schemaLoading }
53+
activeSchema={ activeSchema }
54+
TypedEditor={ TypedEditor }
55+
onEditDraftChange={ onEditDraftChange }
56+
ppomFieldIndex={ ppomFieldIndex }
57+
modalContext={ modalContext }
58+
onOpenPicker={ onOpenPicker }
59+
/>
8460
) }
8561
</Box>
8662
);

0 commit comments

Comments
 (0)