From 01130bdafe5c8b080a85b135bc7edec384906fa4 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 19 Sep 2024 23:10:20 +0100 Subject: [PATCH] fix: revert auto field value update (#778) --- .changeset/brown-bikes-raise.md | 10 ++++ packages/conform-dom/form.ts | 36 ++++++------ packages/conform-react/helpers.ts | 3 +- packages/conform-react/hooks.ts | 73 +------------------------ packages/conform-react/integrations.ts | 24 +++++++- playground/app/routes/form-control.tsx | 6 -- tests/integrations/form-control.spec.ts | 4 -- 7 files changed, 52 insertions(+), 104 deletions(-) create mode 100644 .changeset/brown-bikes-raise.md diff --git a/.changeset/brown-bikes-raise.md b/.changeset/brown-bikes-raise.md new file mode 100644 index 00000000..05e6c780 --- /dev/null +++ b/.changeset/brown-bikes-raise.md @@ -0,0 +1,10 @@ +--- +"@conform-to/dom": patch +"@conform-to/react": patch +--- + +fix: revert auto field value update + +Revert #729 and #766 + +The auto field value update feature introduced in v1.2.0 has caused several critical issues with significant user impact. While I appreciate what they accomplished, I’ve realized the current solution isn't robust enough to handle all potential use cases. To minimize the impact on everyone, I believe it's best to revert these changes for now. diff --git a/packages/conform-dom/form.ts b/packages/conform-dom/form.ts index 61e47baf..66717aa0 100644 --- a/packages/conform-dom/form.ts +++ b/packages/conform-dom/form.ts @@ -78,9 +78,7 @@ export type FormValue = Schema extends : Schema extends Array ? string | Array> | undefined : Schema extends Record - ? - | { [Key in keyof Schema]?: FormValue } - | undefined + ? { [Key in keyof Schema]?: FormValue } | undefined : unknown; const error = Symbol('error'); @@ -278,7 +276,12 @@ function createFormMeta( value: initialValue, constraint: options.constraint ?? {}, validated: lastResult?.state?.validated ?? {}, - key: getDefaultKey(defaultValue), + key: !initialized + ? getDefaultKey(defaultValue) + : { + '': generateId(), + ...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) ?? {}, @@ -295,20 +298,15 @@ 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; - }, - { - [prefix ?? '']: generateId(), - }, - ); + return result; + }, {}); } function setFieldsValidated( @@ -440,8 +438,10 @@ function updateValue( if (name === '') { meta.initialValue = value as Record; meta.value = value as Record; - meta.key = getDefaultKey(value as Record); - + meta.key = { + ...getDefaultKey(value as Record), + '': generateId(), + }; return; } diff --git a/packages/conform-react/helpers.ts b/packages/conform-react/helpers.ts index bfc668b9..bebe699a 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; + key: string | undefined; id: string; name: string; form: string; @@ -214,6 +214,7 @@ export function getFormControlProps( options?: FormControlOptions, ): FormControlProps { return simplify({ + key: metadata.key, required: metadata.required || undefined, ...getFieldsetProps(metadata, options), }); diff --git a/packages/conform-react/hooks.ts b/packages/conform-react/hooks.ts index 11c0f3c4..24161525 100644 --- a/packages/conform-react/hooks.ts +++ b/packages/conform-react/hooks.ts @@ -91,82 +91,11 @@ export function useForm< context.onUpdate({ ...formConfig, formId }); }); - const subjectRef = useSubjectRef({ - key: { - // Subscribe to all key changes so it will re-render and - // update the field value as soon as the DOM is updated - prefix: [''], - }, - }); + const subjectRef = useSubjectRef(); const stateSnapshot = useFormState(context, subjectRef); const noValidate = useNoValidate(options.defaultNoValidate); const form = getFormMetadata(context, subjectRef, stateSnapshot, noValidate); - useEffect(() => { - const formElement = document.forms.namedItem(formId); - - if (!formElement) { - 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 - ) { - const prev = element.dataset.conform; - const next = stateSnapshot.key[element.name]; - const defaultValue = stateSnapshot.initialValue[element.name]; - - if ( - prev === 'managed' || - element.type === 'submit' || - element.type === 'reset' || - element.type === 'button' - ) { - // Skip buttons and fields managed by useInputControl() - continue; - } - - 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) ?? ''; - } - } - } - } - }, [formId, stateSnapshot]); - return [form, form.getFieldset()]; } diff --git a/packages/conform-react/integrations.ts b/packages/conform-react/integrations.ts index 883222c9..1bdc9579 100644 --- a/packages/conform-react/integrations.ts +++ b/packages/conform-react/integrations.ts @@ -71,7 +71,7 @@ export function createDummySelect( select.name = name; select.multiple = true; - select.dataset.conform = 'managed'; + select.dataset.conform = 'true'; // To make sure the input is hidden but still focusable select.setAttribute('aria-hidden', 'true'); @@ -98,7 +98,7 @@ export function createDummySelect( export function isDummySelect( element: HTMLElement, ): element is HTMLSelectElement { - return element.dataset.conform === 'managed'; + return element.dataset.conform === 'true'; } export function updateFieldValue( @@ -306,8 +306,26 @@ 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, + register: refCallback, value, change: handleChange, focus, diff --git a/playground/app/routes/form-control.tsx b/playground/app/routes/form-control.tsx index 81fbfa85..dad98b68 100644 --- a/playground/app/routes/form-control.tsx +++ b/playground/app/routes/form-control.tsx @@ -128,12 +128,6 @@ export default function FormControl() { > Reset form - diff --git a/tests/integrations/form-control.spec.ts b/tests/integrations/form-control.spec.ts index 54c00cc8..039b3ea5 100644 --- a/tests/integrations/form-control.spec.ts +++ b/tests/integrations/form-control.spec.ts @@ -14,7 +14,6 @@ function getFieldset(form: Locator) { clearMessage: form.locator('button:text("Clear message")'), resetMessage: form.locator('button:text("Reset message")'), resetForm: form.locator('button:text("Reset form")'), - inputButton: form.locator('input[type="submit"]'), }; } @@ -22,9 +21,6 @@ async function runValidationScenario(page: Page) { const playground = getPlayground(page); const fieldset = getFieldset(playground.container); - // Conform should not overwrite the value of any input buttons - await expect(fieldset.inputButton).toHaveValue('Submit'); - await expect(playground.error).toHaveText(['', '', '']); await fieldset.validateMessage.click();