Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/components/Form/DynamicField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export const DynamicField = <TData extends FormDataType>({
const [dependentValues, setDependentValues] = useState(pick(values, field.deps));

const staticField = useMemo(() => {
return field.render.call(undefined, values);
// only the values named in deps are provided, since a change to any other value does not trigger a rerender
return field.render.call(undefined, dependentValues as PartialFormDataType<TData>);
}, [dependentValues, field.render]);

useEffect(() => {
Expand Down
27 changes: 27 additions & 0 deletions src/components/Form/Form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,31 @@ describe('Form', () => {
expect(onSubmit).toHaveBeenCalledWith({ symptoms: new Set(['none']) });
});
});

describe('dynamic field deps', () => {
it('should pass only the values named in deps to the render function', async () => {
const renderField = vi.fn((data: { a?: boolean; b?: string; c?: string }) => {
return data.a ? ({ kind: 'string', label: 'Field C', variant: 'input' } as const) : null;
});
render(
<Form
content={{
a: { kind: 'boolean', label: 'Field A', variant: 'checkbox' },
b: { kind: 'string', label: 'Field B', variant: 'input' },
c: { deps: ['a'], kind: 'dynamic', render: renderField }
}}
data-testid={testid}
validationSchema={z.object({ a: z.boolean().optional(), b: z.string().optional(), c: z.string().optional() })}
onSubmit={onSubmit}
/>
);
await userEvent.type(screen.getByLabelText('Field B'), 'abc');
fireEvent.click(screen.getByLabelText('Field A'));
await waitFor(() => expect(screen.getByLabelText('Field C')).toBeInTheDocument());
expect(renderField.mock.lastCall?.[0]).toStrictEqual({ a: true });
for (const [data] of renderField.mock.calls) {
expect(data).not.toHaveProperty('b');
}
});
});
});
5 changes: 3 additions & 2 deletions src/components/Form/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
StaticFormFields,
UnknownFormField
} from '@douglasneuroinformatics/libui-form-types';
import { pick } from 'lodash-es';

export function getInitialValues<T extends FormDataType>(values: PartialNullableFormDataType<T>) {
const initialValues: { [key: string]: unknown } = {};
Expand Down Expand Up @@ -34,15 +35,15 @@ export function getFormFields<T extends FormDataType>(content: FormContent<T>):

/**
* Given a set of data, resolve static content for form fields. Null values
* will be removed.
* will be removed. A dynamic field receives only the values named in its deps.
*/
export function resolveStaticFormFields<T extends FormDataType>(content: FormContent<T>, data: PartialFormDataType<T>) {
const staticFormFields: Partial<StaticFormFields<T>> = {};
const formFields = getFormFields(content);
for (const fieldName in formFields) {
const field: UnknownFormField<T, typeof fieldName> = formFields[fieldName];
if (field.kind === 'dynamic') {
const resolvedField = field.render.call(undefined, data);
const resolvedField = field.render.call(undefined, pick(data, field.deps) as PartialFormDataType<T>);
if (resolvedField) {
staticFormFields[fieldName] = resolvedField;
}
Expand Down
Loading