diff --git a/packages/conform-dom/form.ts b/packages/conform-dom/form.ts index 06320bb5..9ef1648e 100644 --- a/packages/conform-dom/form.ts +++ b/packages/conform-dom/form.ts @@ -279,12 +279,7 @@ function createFormMeta( value: initialValue, constraint: options.constraint ?? {}, validated: lastResult?.state?.validated ?? {}, - key: !initialized - ? getDefaultKey(defaultValue) - : { - '': generateId(), - ...getDefaultKey(defaultValue), - }, + key: getDefaultKey(defaultValue), // The `lastResult` should comes from the server which we won't expect the error to be null // We can consider adding a warning if it happens error: (lastResult?.error as Record) ?? {}, @@ -301,15 +296,20 @@ function getDefaultKey( ): Record { return Object.entries(flatten(defaultValue, { prefix })).reduce< Record - >((result, [key, value]) => { - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - result[formatName(key, i)] = generateId(); + >( + (result, [key, value]) => { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + result[formatName(key, i)] = generateId(); + } } - } - return result; - }, {}); + return result; + }, + { + [prefix ?? '']: generateId(), + }, + ); } function setFieldsValidated( @@ -441,10 +441,8 @@ function updateValue( if (name === '') { meta.initialValue = value as Record; meta.value = value as Record; - meta.key = { - ...getDefaultKey(value as Record), - '': generateId(), - }; + meta.key = getDefaultKey(value as Record); + return; } diff --git a/packages/conform-react/context.tsx b/packages/conform-react/context.tsx index 69ece9c4..a73aac07 100644 --- a/packages/conform-react/context.tsx +++ b/packages/conform-react/context.tsx @@ -28,6 +28,7 @@ import { useContext, useSyncExternalStore, useRef, + useEffect, } from 'react'; export type Pretty = { [K in keyof T]: T[K] } & {}; @@ -152,7 +153,67 @@ export function useFormState( [form, subjectRef], ); - return useSyncExternalStore(subscribe, form.getState, form.getState); + const state = useSyncExternalStore(subscribe, form.getState, form.getState); + + useEffect(() => { + const formId = form.getFormId(); + const formElement = document.forms.namedItem(formId); + const scope = subjectRef?.current.key; + + if (!formElement || !scope) { + return; + } + + const getAll = (value: unknown) => { + if (typeof value === 'string') { + return [value]; + } + + if ( + Array.isArray(value) && + value.every((item) => typeof item === 'string') + ) { + return value; + } + + return undefined; + }; + const get = (value: unknown) => getAll(value)?.[0]; + + for (const element of formElement.elements) { + if ( + (element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element instanceof HTMLSelectElement) && + scope.name?.includes(element.name) + ) { + const prev = element.dataset.conform; + const next = state.key[element.name]; + const defaultValue = state.initialValue[element.name]; + + if (typeof prev === 'undefined' || prev !== next) { + element.dataset.conform = next; + + if ('options' in element) { + const value = getAll(defaultValue) ?? []; + + for (const option of element.options) { + option.selected = value.includes(option.value); + } + } else if ( + 'checked' in element && + (element.type === 'checkbox' || element.type === 'radio') + ) { + element.checked = get(defaultValue) === element.value; + } else { + element.value = get(defaultValue) ?? ''; + } + } + } + } + }, [form, state, subjectRef]); + + return state; } export function FormProvider(props: { @@ -307,9 +368,14 @@ export function getMetadata< case 'initialValue': case 'value': case 'valid': - case 'dirty': + case 'dirty': { + if (key === 'initialValue') { + // If `initialValue` is subscribed, it's likely that we will want the field to be updated + updateSubjectRef(subjectRef, 'key', 'name', name); + } updateSubjectRef(subjectRef, key, 'name', name); break; + } case 'errors': case 'allErrors': updateSubjectRef( diff --git a/packages/conform-react/helpers.ts b/packages/conform-react/helpers.ts index bebe699a..bfc668b9 100644 --- a/packages/conform-react/helpers.ts +++ b/packages/conform-react/helpers.ts @@ -1,7 +1,7 @@ import type { FormMetadata, FieldMetadata, Metadata, Pretty } from './context'; type FormControlProps = { - key: string | undefined; + key?: string; id: string; name: string; form: string; @@ -214,7 +214,6 @@ export function getFormControlProps( options?: FormControlOptions, ): FormControlProps { return simplify({ - key: metadata.key, required: metadata.required || undefined, ...getFieldsetProps(metadata, options), }); diff --git a/packages/conform-react/integrations.ts b/packages/conform-react/integrations.ts index cb5c58b6..df326640 100644 --- a/packages/conform-react/integrations.ts +++ b/packages/conform-react/integrations.ts @@ -19,8 +19,8 @@ export function getFieldElements( const elements = !field ? [] : field instanceof Element - ? [field] - : Array.from(field.values()); + ? [field] + : Array.from(field.values()); return elements.filter( ( @@ -306,26 +306,8 @@ export function useControl< change(value); }; - const refCallback: RefCallback< - HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | undefined - > = (element) => { - register(element); - - if (!element) { - return; - } - - const prevKey = element.dataset.conform; - const nextKey = `${meta.key ?? ''}`; - - if (prevKey !== nextKey) { - element.dataset.conform = nextKey; - updateFieldValue(element, value ?? ''); - } - }; - return { - register: refCallback, + register, value, change: handleChange, focus,