Skip to content

Commit

Permalink
[Form lib] Allow new "defaultValue" to be provided when resetting the…
Browse files Browse the repository at this point in the history
… form
  • Loading branch information
sebelga committed Aug 18, 2020
1 parent 8d85198 commit 66a7ce3
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 62 deletions.
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 @@ -44,14 +44,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 = defaultValue) => {
if (typeof updatedDefaultValue === 'function') {
return deserializer ? deserializer(updatedDefaultValue()) : updatedDefaultValue();
}
return deserializer ? deserializer(updatedDefaultValue) : updatedDefaultValue;
},
[defaultValue, 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 +437,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 +446,12 @@ export const useField = <T>(
setErrors([]);

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

// -- 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,120 @@ 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,
});

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

return (
<Form form={form}>
<UseField path="username" data-test-subj="userNameField" />
<UseField path="city" defaultValue="Paris" 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: 'Paris',
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: 'Paris',
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: 'defaultValue', // Back to the initial defaultValue as no value was provided when resetting
city: 'newDefaultValue',
deeply: { nested: { value: 'newDefaultValue' } },
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ export function useForm<T extends FormData = FormData>(
return {};
}

return Object.entries(defaultValue as object)
const defaultValueFiltered = Object.entries(defaultValue as object)
.filter(({ 1: value }) => value !== undefined)
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
}, [defaultValue]);

return deserializer ? deserializer(defaultValueFiltered) : defaultValueFiltered;
}, [defaultValue, deserializer]);

const defaultValueDeserialized = useRef(formDefaultValue);

const { errorDisplayDelay, stripEmptyFields: doStripEmptyFields } = options ?? {};
const formOptions = useMemo(
Expand All @@ -58,11 +62,6 @@ export function useForm<T extends FormData = FormData>(
[errorDisplayDelay, doStripEmptyFields]
);

const defaultValueDeserialized = useMemo(
() => (deserializer ? deserializer(formDefaultValue) : formDefaultValue),
[formDefaultValue, deserializer]
);

const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [isValid, setIsValid] = useState<boolean | undefined>(undefined);
Expand Down Expand Up @@ -277,8 +276,8 @@ export function useForm<T extends FormData = FormData>(
const getFields: FormHook<T>['getFields'] = useCallback(() => fieldsRefs.current, []);

const getFieldDefaultValue: FormHook['getFieldDefaultValue'] = useCallback(
(fieldName) => get(defaultValueDeserialized, fieldName),
[defaultValueDeserialized]
(fieldName) => get(defaultValueDeserialized.current, fieldName),
[]
);

const readFieldConfigFromSchema: FormHook<T>['__readFieldConfigFromSchema'] = useCallback(
Expand Down Expand Up @@ -343,15 +342,23 @@ export function useForm<T extends FormData = FormData>(
*/
const reset: FormHook<T>['reset'] = useCallback(
(resetOptions = { resetValues: true }) => {
const { resetValues = true } = resetOptions;
const { resetValues = true, defaultValue: updatedDefaultValue } = resetOptions;
const currentFormData = { ...getFormData$().value } as FormData;

if (updatedDefaultValue) {
defaultValueDeserialized.current = deserializer
? deserializer(updatedDefaultValue)
: updatedDefaultValue;
}

Object.entries(fieldsRefs.current).forEach(([path, field]) => {
// By resetting the form, some field might be unmounted. In order
// to avoid a race condition, we check that the field still exists.
const isFieldMounted = fieldsRefs.current[path] !== undefined;
if (isFieldMounted) {
const fieldValue = field.reset({ resetValue: resetValues }) ?? currentFormData[path];
currentFormData[path] = fieldValue;
const fieldDefaultValue = getFieldDefaultValue(path);
field.reset({ resetValue: resetValues, defaultValue: fieldDefaultValue });
currentFormData[path] = fieldDefaultValue;
}
});
if (resetValues) {
Expand All @@ -362,7 +369,7 @@ export function useForm<T extends FormData = FormData>(
setSubmitting(false);
setIsValid(undefined);
},
[getFormData$]
[getFormData$, deserializer, getFieldDefaultValue]
);

const form = useMemo<FormHook<T>>(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface FormHook<T extends FormData = FormData> {
getFieldDefaultValue: (fieldName: string) => unknown;
/* Returns a list of all errors in the form */
getErrors: () => string[];
reset: (options?: { resetValues?: boolean }) => void;
reset: (options?: { resetValues?: boolean; defaultValue?: FormData }) => void;
readonly __options: Required<FormOptions>;
__getFormData$: () => Subject<T>;
__addField: (field: FieldHook) => void;
Expand Down Expand Up @@ -115,7 +115,7 @@ export interface FieldHook<T = unknown> {
value?: unknown;
validationType?: string;
}) => FieldValidateResponse | Promise<FieldValidateResponse>;
reset: (options?: { resetValue: boolean }) => unknown | undefined;
reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined;
__serializeOutput: (rawValue?: unknown) => unknown;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const formDeserializer = (formData: GenericObject) => {
};

export const ConfigurationForm = React.memo(({ value }: Props) => {
const isMounted = useRef<boolean | undefined>(undefined);
const isMounted = useRef(false);

const { form } = useForm<MappingsConfiguration>({
schema: configurationFormSchema,
Expand Down Expand Up @@ -115,23 +115,16 @@ export const ConfigurationForm = React.memo(({ value }: Props) => {
}, [dispatch, subscribe, submit]);

useEffect(() => {
if (isMounted.current === undefined) {
// On mount: don't reset the form
isMounted.current = true;
return;
} else if (isMounted.current === false) {
// When we save the snapshot on unMount we update the "defaultValue" in our state
// wich updates the "value" prop here on the component.
// To avoid resetting the form at this stage, we exit early.
return;
if (isMounted.current) {
// If the value has changed (it probably means that we have loaded a new JSON)
// we need to reset the form to update the fields values.
reset({ resetValues: true, defaultValue: value });
}

// If the value has changed (it probably means that we have loaded a new JSON)
// we need to reset the form to update the fields values.
reset({ resetValues: true });
}, [value, reset]);

useEffect(() => {
isMounted.current = true;

return () => {
isMounted.current = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const formDeserializer = (formData: { [key: string]: any }) => {
};

export const TemplatesForm = React.memo(({ value }: Props) => {
const isMounted = useRef<boolean | undefined>(undefined);
const isMounted = useRef(false);

const { form } = useForm<any>({
schema: templatesFormSchema,
Expand All @@ -75,23 +75,16 @@ export const TemplatesForm = React.memo(({ value }: Props) => {
}, [subscribe, dispatch, submitForm]);

useEffect(() => {
if (isMounted.current === undefined) {
// On mount: don't reset the form
isMounted.current = true;
return;
} else if (isMounted.current === false) {
// When we save the snapshot on unMount we update the "defaultValue" in our state
// wich updates the "value" prop here on the component.
// To avoid resetting the form at this stage, we exit early.
return;
if (isMounted.current) {
// If the value has changed (it probably means that we have loaded a new JSON)
// we need to reset the form to update the fields values.
reset({ resetValues: true, defaultValue: value });
}

// If the value has changed (it probably means that we have loaded a new JSON)
// we need to reset the form to update the fields values.
reset({ resetValues: true });
}, [value, reset]);

useEffect(() => {
isMounted.current = true;

return () => {
isMounted.current = false;

Expand Down

0 comments on commit 66a7ce3

Please sign in to comment.