Skip to content

Commit 9f5ccca

Browse files
authored
[7.x] [Form lib] Allow new "defaultValue" to be provided when resetting the… (#75302) (#75455)
1 parent e0e50ed commit 9f5ccca

File tree

16 files changed

+325
-144
lines changed

16 files changed

+325
-144
lines changed

src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export const FormWizardProvider = WithMultiContent<Props<any>>(function FormWiza
147147
return nextState;
148148
});
149149
},
150-
[getStepIndex, validate, onSave, getData]
150+
[getStepIndex, validate, onSave, getData, lastStep]
151151
);
152152

153153
const value: Context = {

src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('<UseField />', () => {
9898

9999
useEffect(() => {
100100
onForm(form);
101-
}, [form]);
101+
}, [onForm, form]);
102102

103103
return (
104104
<Form form={form}>

src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,24 @@ function UseFieldComp<T = unknown>(props: Props<T>) {
5454
const propsToForward =
5555
componentProps !== undefined ? { ...componentProps, ...rest } : { ...rest };
5656

57-
const fieldConfig =
57+
const fieldConfig: FieldConfig<any, T> & { initialValue?: T } =
5858
config !== undefined
5959
? { ...config }
6060
: ({
6161
...form.__readFieldConfigFromSchema(path),
6262
} as Partial<FieldConfig<any, T>>);
6363

64-
if (defaultValue === undefined && readDefaultValueOnForm) {
65-
// Read the field default value from the "defaultValue" object passed to the form
66-
(fieldConfig.defaultValue as any) = form.getFieldDefaultValue(path) ?? fieldConfig.defaultValue;
67-
} else if (defaultValue !== undefined) {
68-
// Read the field default value from the propvided prop
69-
(fieldConfig.defaultValue as any) = defaultValue;
64+
if (defaultValue !== undefined) {
65+
// update the form "defaultValue" ref object so when/if we reset the form we can go back to this value
66+
form.__updateDefaultValueAt(path, defaultValue);
67+
68+
// Use the defaultValue prop as initial value
69+
fieldConfig.initialValue = defaultValue;
70+
} else {
71+
if (readDefaultValueOnForm) {
72+
// Read the field initial value from the "defaultValue" object passed to the form
73+
fieldConfig.initialValue = (form.getFieldDefaultValue(path) as T) ?? fieldConfig.defaultValue;
74+
}
7075
}
7176

7277
if (!fieldConfig.path) {

src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ import { FIELD_TYPES, VALIDATION_TYPES } from '../constants';
2525
export const useField = <T>(
2626
form: FormHook,
2727
path: string,
28-
config: FieldConfig<any, T> = {},
28+
config: FieldConfig<any, T> & { initialValue?: T } = {},
2929
valueChangeListener?: (value: T) => void
3030
) => {
3131
const {
3232
type = FIELD_TYPES.TEXT,
33-
defaultValue = '',
33+
defaultValue = '', // The value to use a fallback mecanism when no initial value is passed
34+
initialValue = config.defaultValue ?? '', // The value explicitly passed
3435
label = '',
3536
labelAppend = '',
3637
helpText = '',
@@ -44,14 +45,22 @@ export const useField = <T>(
4445

4546
const { getFormData, __removeField, __updateFormDataAt, __validateFields } = form;
4647

47-
const initialValue = useMemo(() => {
48-
if (typeof defaultValue === 'function') {
49-
return deserializer ? deserializer(defaultValue()) : defaultValue();
50-
}
51-
return deserializer ? deserializer(defaultValue) : defaultValue;
52-
}, [defaultValue, deserializer]) as T;
48+
/**
49+
* This callback is both used as the initial "value" state getter, **and** for when we reset the form
50+
* (and thus reset the field value). When we reset the form, we can provide a new default value (which will be
51+
* passed through this "initialValueGetter" handler).
52+
*/
53+
const initialValueGetter = useCallback(
54+
(updatedDefaultValue = initialValue) => {
55+
if (typeof updatedDefaultValue === 'function') {
56+
return deserializer ? deserializer(updatedDefaultValue()) : updatedDefaultValue();
57+
}
58+
return deserializer ? deserializer(updatedDefaultValue) : updatedDefaultValue;
59+
},
60+
[initialValue, deserializer]
61+
);
5362

54-
const [value, setStateValue] = useState<T>(initialValue);
63+
const [value, setStateValue] = useState<T>(initialValueGetter);
5564
const [errors, setErrors] = useState<ValidationError[]>([]);
5665
const [isPristine, setPristine] = useState(true);
5766
const [isValidating, setValidating] = useState(false);
@@ -429,7 +438,7 @@ export const useField = <T>(
429438

430439
const reset: FieldHook<T>['reset'] = useCallback(
431440
(resetOptions = { resetValue: true }) => {
432-
const { resetValue = true } = resetOptions;
441+
const { resetValue = true, defaultValue: updatedDefaultValue } = resetOptions;
433442

434443
setPristine(true);
435444
setValidating(false);
@@ -438,11 +447,12 @@ export const useField = <T>(
438447
setErrors([]);
439448

440449
if (resetValue) {
441-
setValue(initialValue);
442-
return initialValue;
450+
const newValue = initialValueGetter(updatedDefaultValue ?? defaultValue);
451+
setValue(newValue);
452+
return newValue;
443453
}
444454
},
445-
[setValue, serializeOutput, initialValue]
455+
[setValue, initialValueGetter, defaultValue]
446456
);
447457

448458
// -- EFFECTS

src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,17 @@ interface Props {
3333
onData: FormSubmitHandler<MyForm>;
3434
}
3535

36+
let formHook: FormHook<any> | null = null;
37+
38+
const onFormHook = (_form: FormHook<any>) => {
39+
formHook = _form;
40+
};
41+
3642
describe('use_form() hook', () => {
43+
beforeEach(() => {
44+
formHook = null;
45+
});
46+
3747
describe('form.submit() & config.onSubmit()', () => {
3848
const onFormData = jest.fn();
3949

@@ -125,12 +135,6 @@ describe('use_form() hook', () => {
125135
});
126136

127137
test('should not build the object if the form is not valid', async () => {
128-
let formHook: FormHook<MyForm> | null = null;
129-
130-
const onFormHook = (_form: FormHook<MyForm>) => {
131-
formHook = _form;
132-
};
133-
134138
const TestComp = ({ onForm }: { onForm: (form: FormHook<MyForm>) => void }) => {
135139
const { form } = useForm<MyForm>({ defaultValue: { username: 'initialValue' } });
136140
const validator: ValidationFunc = ({ value }) => {
@@ -141,7 +145,7 @@ describe('use_form() hook', () => {
141145

142146
useEffect(() => {
143147
onForm(form);
144-
}, [form]);
148+
}, [onForm, form]);
145149

146150
return (
147151
<Form form={form}>
@@ -297,4 +301,145 @@ describe('use_form() hook', () => {
297301
});
298302
});
299303
});
304+
305+
describe('form.reset()', () => {
306+
const defaultValue = {
307+
username: 'defaultValue',
308+
deeply: { nested: { value: 'defaultValue' } },
309+
};
310+
311+
type RestFormTest = typeof defaultValue;
312+
313+
const TestComp = ({ onForm }: { onForm: (form: FormHook<any>) => void }) => {
314+
const { form } = useForm<RestFormTest>({
315+
defaultValue,
316+
options: { stripEmptyFields: false },
317+
});
318+
319+
useEffect(() => {
320+
onForm(form);
321+
}, [onForm, form]);
322+
323+
return (
324+
<Form form={form}>
325+
<UseField
326+
path="username"
327+
config={{ defaultValue: 'configDefaultValue' }}
328+
data-test-subj="userNameField"
329+
/>
330+
<UseField
331+
path="city"
332+
config={{ defaultValue: 'configDefaultValue' }}
333+
defaultValue="inlineDefaultValue"
334+
data-test-subj="cityField"
335+
/>
336+
<UseField path="deeply.nested.value" data-test-subj="deeplyNestedField" />
337+
</Form>
338+
);
339+
};
340+
341+
const setup = registerTestBed(TestComp, {
342+
defaultProps: { onForm: onFormHook },
343+
memoryRouter: { wrapComponent: false },
344+
});
345+
346+
test('should put back the defaultValue for each field', async () => {
347+
const {
348+
form: { setInputValue },
349+
} = setup() as TestBed;
350+
351+
if (!formHook) {
352+
throw new Error(
353+
`formHook is not defined. Use the onForm() prop to update the reference to the form hook.`
354+
);
355+
}
356+
357+
let formData: Partial<RestFormTest> = {};
358+
359+
await act(async () => {
360+
formData = formHook!.getFormData();
361+
});
362+
expect(formData).toEqual({
363+
username: 'defaultValue',
364+
city: 'inlineDefaultValue',
365+
deeply: { nested: { value: 'defaultValue' } },
366+
});
367+
368+
setInputValue('userNameField', 'changedValue');
369+
setInputValue('cityField', 'changedValue');
370+
setInputValue('deeplyNestedField', 'changedValue');
371+
372+
await act(async () => {
373+
formData = formHook!.getFormData();
374+
});
375+
expect(formData).toEqual({
376+
username: 'changedValue',
377+
city: 'changedValue',
378+
deeply: { nested: { value: 'changedValue' } },
379+
});
380+
381+
await act(async () => {
382+
formHook!.reset();
383+
});
384+
385+
await act(async () => {
386+
formData = formHook!.getFormData();
387+
});
388+
expect(formData).toEqual({
389+
username: 'defaultValue',
390+
city: 'inlineDefaultValue', // Inline default value is correctly kept after resetting
391+
deeply: { nested: { value: 'defaultValue' } },
392+
});
393+
});
394+
395+
test('should allow to pass a new "defaultValue" object for the fields', async () => {
396+
const {
397+
form: { setInputValue },
398+
} = setup() as TestBed;
399+
400+
if (!formHook) {
401+
throw new Error(
402+
`formHook is not defined. Use the onForm() prop to update the reference to the form hook.`
403+
);
404+
}
405+
406+
setInputValue('userNameField', 'changedValue');
407+
setInputValue('cityField', 'changedValue');
408+
setInputValue('deeplyNestedField', 'changedValue');
409+
410+
let formData: Partial<RestFormTest> = {};
411+
412+
await act(async () => {
413+
formHook!.reset({
414+
defaultValue: {
415+
city: () => 'newDefaultValue', // A function can also be passed
416+
deeply: { nested: { value: 'newDefaultValue' } },
417+
},
418+
});
419+
});
420+
await act(async () => {
421+
formData = formHook!.getFormData();
422+
});
423+
expect(formData).toEqual({
424+
username: 'configDefaultValue', // Back to the config defaultValue as no value was provided when resetting
425+
city: 'newDefaultValue',
426+
deeply: { nested: { value: 'newDefaultValue' } },
427+
});
428+
429+
// Make sure all field are back to the config defautlValue, even when we have a UseField with inline prop "defaultValue"
430+
await act(async () => {
431+
formHook!.reset({
432+
defaultValue: {},
433+
});
434+
});
435+
await act(async () => {
436+
formData = formHook!.getFormData();
437+
});
438+
expect(formData).toEqual({
439+
username: 'configDefaultValue',
440+
city: 'configDefaultValue', // Inline default value **is not** kept after resetting with undefined "city" value
441+
deeply: { nested: { value: '' } }, // Fallback to empty string as no config was provided
442+
});
443+
});
444+
});
300445
});

0 commit comments

Comments
 (0)