Skip to content

Commit

Permalink
feat(useformstate): replace setInitialValue/reset with prefill/revert…
Browse files Browse the repository at this point in the history
… and reinitialize/clear (#65)

* feat(useformstate): replace setInitialValue/reset with prefill/revert and reinitialize/clear

To allow for isDirty to be based on prefilled values but also allow clearing the form to a real
empty/default value

BREAKING CHANGE: `setInitialValue` and `reset` have been replaced with `reinitialize` and `clear`. For better `isDirty` behavior you may want to use `prefill` instead of `reinitialize`.

* Simplify sentence about revert/clear
  • Loading branch information
mturley authored Jun 24, 2021
1 parent 56cc00d commit 5dd826e
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 38 deletions.
34 changes: 22 additions & 12 deletions src/hooks/useFormState/useFormState.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,16 @@ function useFormState<TFieldValues>(
interface IFormField<T> {
value: T;
setValue: React.Dispatch<React.SetStateAction<T>>;
isDirty: boolean; // true if the value is different from initialValue by deep comparison
defaultValue: T;
cleanValue: T;
reinitialize: (value: T) => void; // Sets defaultValue, cleanValue and value
prefill: (value: T) => void; // Sets cleanValue and value
clear: () => void; // Sets value to defaultValue
revert: () => void; // Sets value to cleanValue
isDirty: boolean; // True if value is different from cleanValue
isTouched: boolean;
setIsTouched: (isTouched: boolean) => void;
reset: () => void;
schema: [T] extends [Array<infer E>] ? yup.ArraySchema<E> : yup.Schema<T>;
schema: yup.AnySchema<T>;
}

interface IValidatedFormField<T> extends IFormField<T> {
Expand All @@ -63,14 +68,13 @@ interface IValidatedFormField<T> extends IFormField<T> {
}

interface IFormState<TFieldValues> {
fields: {
// The same fields object passed into useFormState, but with error and isValid properties added
[key in keyof TFieldValues]: IValidatedFormField<TFieldValues[key]>;
};
fields: ValidatedFormFields<TFieldValues>;
values: TFieldValues; // For convenience in submitting forms (values are also included in fields property)
isDirty: boolean; // true if any field has isDirty
isValid: boolean; // true if every field has isValid
reset: () => void;
isDirty: boolean;
isValid: boolean;
isTouched: boolean;
clear: () => void;
revert: () => void;
}
```

Expand Down Expand Up @@ -138,10 +142,16 @@ If you're having trouble with yup schema types resolving to `T | undefined` inst
If the form's initial values are known when it is first rendered, they can simply be passed as the `initialValue` arguments of each `useFormField` call.
However, let's say you're using a form to edit some object you're loading from the network and you need to wait for that request to resolve before you pre-fill the form.

For this, you can use the `setInitialValue` function on each field object. This changes the current field value, but it also changes the value used to determine if the form `isDirty` and used when `reset`ing the form.
For this, you can use the `prefill` function on each field object. This changes the current field value, but it also changes the `cleanValue` (used to determine if the form `isDirty` and used when `revert`ing the form).

Here, the submit button is only enabled if the form is dirty, so you can clearly see whether changes have been made to the object from the network that we're editing.
The reset button also changes the values back to the values we got from the network instead of clearing the form.
The revert button also changes the values back to the values we got from the network instead of clearing the form.

Note that we have separate buttons here for "Revert" and "Clear". The `revert` function sets each field to its `cleanValue` (which can be changed by `prefill`), and the `clear` function sets each
field to its `defaultValue` (which can be changed by `reinitialize`). Both default to the `initialValue` originally passed into `useFormField`.

This distinction between clean/prefilled and default/initialized values is useful in cases like this where we only want to enable the submit button if the values have been changed compared to
our real server-side data, but we also want to provide a way to clear the form back to a truly empty state.

<Canvas>
<Story story={AsyncPrefilling} />
Expand Down
27 changes: 15 additions & 12 deletions src/hooks/useFormState/useFormState.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export const BasicTextFields: React.FunctionComponent = () => {
>
Submit
</button>
<button disabled={!form.isDirty} onClick={form.reset} style={{ marginLeft: 5 }}>
Reset
<button disabled={!form.isDirty} onClick={form.clear} style={{ marginLeft: 5 }}>
Clear
</button>
</>
);
Expand Down Expand Up @@ -105,8 +105,8 @@ export const PatternFlyTextFields: React.FunctionComponent = () => {
>
Submit
</Button>
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.reset}>
Reset
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.clear}>
Clear
</Button>
</Flex>
</Form>
Expand Down Expand Up @@ -144,8 +144,8 @@ export const PatternFlyTextFieldsWithHelpers: React.FunctionComponent = () => {
>
Submit
</Button>
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.reset}>
Reset
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.clear}>
Clear
</Button>
</Flex>
</Form>
Expand All @@ -164,8 +164,8 @@ export const AsyncPrefilling: React.FunctionComponent = () => {
if (isLoading) {
setTimeout(() => {
const objectFromServer = { name: 'Existing name', description: 'Existing description' };
form.fields.name.setInitialValue(objectFromServer.name);
form.fields.description.setInitialValue(objectFromServer.description);
form.fields.name.prefill(objectFromServer.name);
form.fields.description.prefill(objectFromServer.description);
setIsLoading(false);
}, 1000);
}
Expand Down Expand Up @@ -198,8 +198,11 @@ export const AsyncPrefilling: React.FunctionComponent = () => {
>
Submit
</Button>
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.reset}>
Reset
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.revert}>
Revert
</Button>
<Button variant="secondary" isDisabled={!form.isDirty} onClick={form.clear}>
Clear
</Button>
</Flex>
</Form>
Expand Down Expand Up @@ -293,8 +296,8 @@ export const ComplexFieldTypes: React.FunctionComponent = () => {
>
Submit
</button>
<button disabled={!form.isDirty} onClick={form.reset} style={{ marginLeft: 5 }}>
Reset
<button disabled={!form.isDirty} onClick={form.clear} style={{ marginLeft: 5 }}>
Clear
</button>
</>
);
Expand Down
46 changes: 32 additions & 14 deletions src/hooks/useFormState/useFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import { ValidateOptions } from 'yup/lib/types';
export interface IFormField<T> {
value: T;
setValue: React.Dispatch<React.SetStateAction<T>>;
setInitialValue: (value: T) => void;
isDirty: boolean;
defaultValue: T;
cleanValue: T;
reinitialize: (value: T) => void; // Sets defaultValue, cleanValue and value
prefill: (value: T) => void; // Sets cleanValue and value
clear: () => void; // Sets value to defaultValue
revert: () => void; // Sets value to cleanValue
isDirty: boolean; // True if value is different from cleanValue
isTouched: boolean;
setIsTouched: (isTouched: boolean) => void;
reset: () => void;
schema: yup.AnySchema<T>;
}

Expand All @@ -37,7 +41,8 @@ export interface IFormState<TFieldValues> {
isDirty: boolean;
isValid: boolean;
isTouched: boolean;
reset: () => void;
clear: () => void;
revert: () => void;
}

// The generic T type variable is the type of the field's value (the T in IFormField<T>).
Expand All @@ -55,23 +60,35 @@ export const useFormField = <T>(
schema: yup.AnySchema<T | undefined>,
options: { initialTouched?: boolean } = {}
): IFormField<T> => {
const [initializedValue, setInitializedValue] = React.useState<T>(initialValue);
const [value, setValue] = React.useState<T>(initialValue);
const [defaultValue, setDefaultValue] = React.useState<T>(initialValue); // The value used on clear()
const [cleanValue, setCleanValue] = React.useState<T>(initialValue); // The value considered "unchanged", determines isDirty and used on revert()
const [value, setValue] = React.useState<T>(initialValue); // The actual value in the field
const [isTouched, setIsTouched] = React.useState(options.initialTouched || false);
return {
value,
setValue,
setInitialValue: (value: T) => {
setInitializedValue(value);
defaultValue,
cleanValue,
reinitialize: (value: T) => {
setDefaultValue(value);
setCleanValue(value);
setValue(value);
},
isDirty: !equal(value, initializedValue),
isTouched,
setIsTouched,
reset: () => {
setValue(initializedValue);
prefill: (value: T) => {
setCleanValue(value);
setValue(value);
},
clear: () => {
setValue(defaultValue);
setIsTouched(options.initialTouched || false);
},
revert: () => {
setValue(cleanValue);
setIsTouched(options.initialTouched || false);
},
isDirty: !equal(value, cleanValue),
isTouched,
setIsTouched,
schema: schema.defined(),
};
};
Expand Down Expand Up @@ -152,7 +169,8 @@ export const useFormState = <TFieldValues>(
isDirty,
isTouched,
isValid: hasRunInitialValidation && !validationError,
reset: () => fieldKeys.forEach((key) => fields[key].reset()),
clear: () => fieldKeys.forEach((key) => fields[key].clear()),
revert: () => fieldKeys.forEach((key) => fields[key].revert()),
};
};

Expand Down

0 comments on commit 5dd826e

Please sign in to comment.