Skip to content

Commit

Permalink
[7.x] [Form lib] Allow new "defaultValue" to be provided when resetti…
Browse files Browse the repository at this point in the history
…ng the… (#75302) (#75455)
  • Loading branch information
sebelga committed Aug 19, 2020
1 parent e0e50ed commit 9f5ccca
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export const FormWizardProvider = WithMultiContent<Props<any>>(function FormWiza
return nextState;
});
},
[getStepIndex, validate, onSave, getData]
[getStepIndex, validate, onSave, getData, lastStep]
);

const value: Context = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('<UseField />', () => {

useEffect(() => {
onForm(form);
}, [form]);
}, [onForm, form]);

return (
<Form form={form}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,24 @@ function UseFieldComp<T = unknown>(props: Props<T>) {
const propsToForward =
componentProps !== undefined ? { ...componentProps, ...rest } : { ...rest };

const fieldConfig =
const fieldConfig: FieldConfig<any, T> & { initialValue?: T } =
config !== undefined
? { ...config }
: ({
...form.__readFieldConfigFromSchema(path),
} as Partial<FieldConfig<any, T>>);

if (defaultValue === undefined && readDefaultValueOnForm) {
// Read the field default value from the "defaultValue" object passed to the form
(fieldConfig.defaultValue as any) = form.getFieldDefaultValue(path) ?? fieldConfig.defaultValue;
} else if (defaultValue !== undefined) {
// Read the field default value from the propvided prop
(fieldConfig.defaultValue as any) = defaultValue;
if (defaultValue !== undefined) {
// update the form "defaultValue" ref object so when/if we reset the form we can go back to this value
form.__updateDefaultValueAt(path, defaultValue);

// Use the defaultValue prop as initial value
fieldConfig.initialValue = defaultValue;
} else {
if (readDefaultValueOnForm) {
// Read the field initial value from the "defaultValue" object passed to the form
fieldConfig.initialValue = (form.getFieldDefaultValue(path) as T) ?? fieldConfig.defaultValue;
}
}

if (!fieldConfig.path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import { FIELD_TYPES, VALIDATION_TYPES } from '../constants';
export const useField = <T>(
form: FormHook,
path: string,
config: FieldConfig<any, T> = {},
config: FieldConfig<any, T> & { initialValue?: T } = {},
valueChangeListener?: (value: T) => void
) => {
const {
type = FIELD_TYPES.TEXT,
defaultValue = '',
defaultValue = '', // The value to use a fallback mecanism when no initial value is passed
initialValue = config.defaultValue ?? '', // The value explicitly passed
label = '',
labelAppend = '',
helpText = '',
Expand All @@ -44,14 +45,22 @@ export const useField = <T>(

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

const initialValue = useMemo(() => {
if (typeof defaultValue === 'function') {
return deserializer ? deserializer(defaultValue()) : defaultValue();
}
return deserializer ? deserializer(defaultValue) : defaultValue;
}, [defaultValue, deserializer]) as T;
/**
* This callback is both used as the initial "value" state getter, **and** for when we reset the form
* (and thus reset the field value). When we reset the form, we can provide a new default value (which will be
* passed through this "initialValueGetter" handler).
*/
const initialValueGetter = useCallback(
(updatedDefaultValue = initialValue) => {
if (typeof updatedDefaultValue === 'function') {
return deserializer ? deserializer(updatedDefaultValue()) : updatedDefaultValue();
}
return deserializer ? deserializer(updatedDefaultValue) : updatedDefaultValue;
},
[initialValue, deserializer]
);

const [value, setStateValue] = useState<T>(initialValue);
const [value, setStateValue] = useState<T>(initialValueGetter);
const [errors, setErrors] = useState<ValidationError[]>([]);
const [isPristine, setPristine] = useState(true);
const [isValidating, setValidating] = useState(false);
Expand Down Expand Up @@ -429,7 +438,7 @@ export const useField = <T>(

const reset: FieldHook<T>['reset'] = useCallback(
(resetOptions = { resetValue: true }) => {
const { resetValue = true } = resetOptions;
const { resetValue = true, defaultValue: updatedDefaultValue } = resetOptions;

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

if (resetValue) {
setValue(initialValue);
return initialValue;
const newValue = initialValueGetter(updatedDefaultValue ?? defaultValue);
setValue(newValue);
return newValue;
}
},
[setValue, serializeOutput, initialValue]
[setValue, initialValueGetter, defaultValue]
);

// -- EFFECTS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ interface Props {
onData: FormSubmitHandler<MyForm>;
}

let formHook: FormHook<any> | null = null;

const onFormHook = (_form: FormHook<any>) => {
formHook = _form;
};

describe('use_form() hook', () => {
beforeEach(() => {
formHook = null;
});

describe('form.submit() & config.onSubmit()', () => {
const onFormData = jest.fn();

Expand Down Expand Up @@ -125,12 +135,6 @@ describe('use_form() hook', () => {
});

test('should not build the object if the form is not valid', async () => {
let formHook: FormHook<MyForm> | null = null;

const onFormHook = (_form: FormHook<MyForm>) => {
formHook = _form;
};

const TestComp = ({ onForm }: { onForm: (form: FormHook<MyForm>) => void }) => {
const { form } = useForm<MyForm>({ defaultValue: { username: 'initialValue' } });
const validator: ValidationFunc = ({ value }) => {
Expand All @@ -141,7 +145,7 @@ describe('use_form() hook', () => {

useEffect(() => {
onForm(form);
}, [form]);
}, [onForm, form]);

return (
<Form form={form}>
Expand Down Expand Up @@ -297,4 +301,145 @@ describe('use_form() hook', () => {
});
});
});

describe('form.reset()', () => {
const defaultValue = {
username: 'defaultValue',
deeply: { nested: { value: 'defaultValue' } },
};

type RestFormTest = typeof defaultValue;

const TestComp = ({ onForm }: { onForm: (form: FormHook<any>) => void }) => {
const { form } = useForm<RestFormTest>({
defaultValue,
options: { stripEmptyFields: false },
});

useEffect(() => {
onForm(form);
}, [onForm, form]);

return (
<Form form={form}>
<UseField
path="username"
config={{ defaultValue: 'configDefaultValue' }}
data-test-subj="userNameField"
/>
<UseField
path="city"
config={{ defaultValue: 'configDefaultValue' }}
defaultValue="inlineDefaultValue"
data-test-subj="cityField"
/>
<UseField path="deeply.nested.value" data-test-subj="deeplyNestedField" />
</Form>
);
};

const setup = registerTestBed(TestComp, {
defaultProps: { onForm: onFormHook },
memoryRouter: { wrapComponent: false },
});

test('should put back the defaultValue for each field', async () => {
const {
form: { setInputValue },
} = setup() as TestBed;

if (!formHook) {
throw new Error(
`formHook is not defined. Use the onForm() prop to update the reference to the form hook.`
);
}

let formData: Partial<RestFormTest> = {};

await act(async () => {
formData = formHook!.getFormData();
});
expect(formData).toEqual({
username: 'defaultValue',
city: 'inlineDefaultValue',
deeply: { nested: { value: 'defaultValue' } },
});

setInputValue('userNameField', 'changedValue');
setInputValue('cityField', 'changedValue');
setInputValue('deeplyNestedField', 'changedValue');

await act(async () => {
formData = formHook!.getFormData();
});
expect(formData).toEqual({
username: 'changedValue',
city: 'changedValue',
deeply: { nested: { value: 'changedValue' } },
});

await act(async () => {
formHook!.reset();
});

await act(async () => {
formData = formHook!.getFormData();
});
expect(formData).toEqual({
username: 'defaultValue',
city: 'inlineDefaultValue', // Inline default value is correctly kept after resetting
deeply: { nested: { value: 'defaultValue' } },
});
});

test('should allow to pass a new "defaultValue" object for the fields', async () => {
const {
form: { setInputValue },
} = setup() as TestBed;

if (!formHook) {
throw new Error(
`formHook is not defined. Use the onForm() prop to update the reference to the form hook.`
);
}

setInputValue('userNameField', 'changedValue');
setInputValue('cityField', 'changedValue');
setInputValue('deeplyNestedField', 'changedValue');

let formData: Partial<RestFormTest> = {};

await act(async () => {
formHook!.reset({
defaultValue: {
city: () => 'newDefaultValue', // A function can also be passed
deeply: { nested: { value: 'newDefaultValue' } },
},
});
});
await act(async () => {
formData = formHook!.getFormData();
});
expect(formData).toEqual({
username: 'configDefaultValue', // Back to the config defaultValue as no value was provided when resetting
city: 'newDefaultValue',
deeply: { nested: { value: 'newDefaultValue' } },
});

// Make sure all field are back to the config defautlValue, even when we have a UseField with inline prop "defaultValue"
await act(async () => {
formHook!.reset({
defaultValue: {},
});
});
await act(async () => {
formData = formHook!.getFormData();
});
expect(formData).toEqual({
username: 'configDefaultValue',
city: 'configDefaultValue', // Inline default value **is not** kept after resetting with undefined "city" value
deeply: { nested: { value: '' } }, // Fallback to empty string as no config was provided
});
});
});
});
Loading

0 comments on commit 9f5ccca

Please sign in to comment.