Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Form lib] Allow new "defaultValue" to be provided when resetting the… #75302

Merged
merged 5 commits into from
Aug 19, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
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