Skip to content

Commit

Permalink
feat: auto field value update (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung authored Sep 12, 2024
1 parent 1b7e3b2 commit b5b0920
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 43 deletions.
32 changes: 15 additions & 17 deletions packages/conform-dom/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,7 @@ function createFormMeta<Schema, FormError, FormValue>(
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<string, FormError>) ?? {},
Expand All @@ -300,15 +295,20 @@ function getDefaultKey(
): Record<string, string> {
return Object.entries(flatten(defaultValue, { prefix })).reduce<
Record<string, string>
>((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<Error>(
Expand Down Expand Up @@ -440,10 +440,8 @@ function updateValue<Error>(
if (name === '') {
meta.initialValue = value as Record<string, unknown>;
meta.value = value as Record<string, unknown>;
meta.key = {
...getDefaultKey(value as Record<string, unknown>),
'': generateId(),
};
meta.key = getDefaultKey(value as Record<string, unknown>);

return;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/conform-react/helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -214,7 +214,6 @@ export function getFormControlProps<Schema>(
options?: FormControlOptions,
): FormControlProps {
return simplify({
key: metadata.key,
required: metadata.required || undefined,
...getFieldsetProps(metadata, options),
});
Expand Down
68 changes: 67 additions & 1 deletion packages/conform-react/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,77 @@ export function useForm<
context.onUpdate({ ...formConfig, formId });
});

const subjectRef = useSubjectRef();
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 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') {
// Skip 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()];
}

Expand Down
28 changes: 5 additions & 23 deletions packages/conform-react/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
(
Expand Down Expand Up @@ -71,7 +71,7 @@ export function createDummySelect(

select.name = name;
select.multiple = true;
select.dataset.conform = 'true';
select.dataset.conform = 'managed';

// To make sure the input is hidden but still focusable
select.setAttribute('aria-hidden', 'true');
Expand All @@ -98,7 +98,7 @@ export function createDummySelect(
export function isDummySelect(
element: HTMLElement,
): element is HTMLSelectElement {
return element.dataset.conform === 'true';
return element.dataset.conform === 'managed';
}

export function updateFieldValue(
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit b5b0920

Please sign in to comment.