Skip to content

Commit

Permalink
[@mantine/form] useForm: remove dirty state
Browse files Browse the repository at this point in the history
Simplify the dirty state functionalities by computing the isDirty
only on demand using deep comparison of values. Manually
managing a dirty state parallel to the values state was very much
bug prone.
  • Loading branch information
gdostie committed Nov 21, 2022
1 parent 58a7c7a commit bf76ebb
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 18 deletions.
19 changes: 19 additions & 0 deletions src/mantine-form/src/tests/dirty.test.ts
Expand Up @@ -40,4 +40,23 @@ describe('@mantine/form/dirty', () => {
act(() => hook.result.current.resetDirty());
expect(hook.result.current.isDirty()).toBe(false);
});

it('sets list field as dirty if list item changes', () => {
const hook = renderHook(() => useForm({ initialValues: { a: [{ b: 1 }, { b: 2 }] } }));
act(() => hook.result.current.setFieldValue('a.0', 3));
expect(hook.result.current.isDirty('a.0')).toBe(true);
expect(hook.result.current.isDirty('a')).toBe(true);

act(() => hook.result.current.setFieldValue('a', [{ b: 1 }, { b: 2 }]));
expect(hook.result.current.isDirty('a.0')).toBe(false);
expect(hook.result.current.isDirty('a')).toBe(false);

act(() => hook.result.current.insertListItem('a', [{ b: 3 }]));
expect(hook.result.current.isDirty('a.2')).toBe(true);
expect(hook.result.current.isDirty('a')).toBe(true);

act(() => hook.result.current.removeListItem('a', 2));
expect(hook.result.current.isDirty('a.2')).toBe(false);
expect(hook.result.current.isDirty('a')).toBe(false);
});
});
1 change: 1 addition & 0 deletions src/mantine-form/src/types.ts
Expand Up @@ -70,6 +70,7 @@ export type SetFieldValue<Values> = <Field extends LooseKeys<Values>>(
) => void;

export type ClearFieldError = (path: unknown) => void;
export type ClearFieldDirty = (path: unknown) => void;
export type ClearErrors = () => void;
export type Reset = () => void;
export type Validate = () => FormValidationResult;
Expand Down
63 changes: 45 additions & 18 deletions src/mantine-form/src/use-form.ts
Expand Up @@ -28,6 +28,7 @@ import {
ResetDirty,
IsValid,
_TransformValues,
ClearFieldDirty,
} from './types';

export function useForm<
Expand All @@ -45,7 +46,7 @@ export function useForm<
validate: rules,
}: UseFormInput<Values, TransformValues> = {}): UseFormReturnType<Values, TransformValues> {
const [touched, setTouched] = useState(initialTouched);
const [dirty, setDirty] = useState(initialDirty);
const [manualDirtyOverride, setDirtyManualOverride] = useState(initialDirty);
const [values, _setValues] = useState(initialValues);
const [errors, _setErrors] = useState(filterErrors(initialErrors));
const _dirtyValues = useRef<Values>(initialValues);
Expand All @@ -56,7 +57,7 @@ export function useForm<
const resetTouched = useCallback(() => setTouched({}), []);
const resetDirty: ResetDirty<Values> = (_values) => {
_setDirtyValues(_values || values);
setDirty({});
setDirtyManualOverride({});
};

const setErrors: SetErrors = useCallback(
Expand Down Expand Up @@ -92,14 +93,25 @@ export function useForm<
[]
);

const clearDirtyOverride: ClearFieldDirty = useCallback(
(path) =>
setDirtyManualOverride((current) => {
if (typeof path !== 'string') {
return current;
}

const result = clearListState(path, current);
delete result[path];
return result;
}),
[]
);

const setFieldValue: SetFieldValue<Values> = useCallback((path, value) => {
const shouldValidate = shouldValidateOnChange(path, validateInputOnChange);
clearDirtyOverride(path);
setTouched((currentTouched) => ({ ...currentTouched, [path]: true }));
_setValues((current) => {
const initialValue = getPath(path, _dirtyValues.current);
const isFieldDirty = !isEqual(initialValue, value);
setDirty((currentDirty) => ({ ...currentDirty, [path]: isFieldDirty }));
setTouched((currentTouched) => ({ ...currentTouched, [path]: true }));

const result = setPath(path, value, current);

if (shouldValidate) {
Expand All @@ -123,21 +135,21 @@ export function useForm<
clearInputErrorOnChange && clearErrors();
}, []);

const reorderListItem: ReorderListItem<Values> = useCallback(
(path, payload) => _setValues((current) => reorderPath(path, payload, current)),
[]
);
const reorderListItem: ReorderListItem<Values> = useCallback((path, payload) => {
clearDirtyOverride(path);
_setValues((current) => reorderPath(path, payload, current));
}, []);

const removeListItem: RemoveListItem<Values> = useCallback((path, index) => {
clearDirtyOverride(path);
_setValues((current) => removePath(path, index, current));
_setErrors((errs) => clearListState(path, errs));
setDirty((current) => clearListState(`${String(path)}.${index}`, current));
}, []);

const insertListItem: InsertListItem<Values> = useCallback(
(path, item, index) => _setValues((current) => insertPath(path, item, index, current)),
[]
);
const insertListItem: InsertListItem<Values> = useCallback((path, item, index) => {
clearDirtyOverride(path);
_setValues((current) => insertPath(path, item, index, current));
}, []);

const validate: Validate = useCallback(() => {
const results = validateValues(rules, values);
Expand Down Expand Up @@ -204,7 +216,22 @@ export function useForm<
reset();
}, []);

const isDirty: GetFieldStatus<Values> = useCallback((path) => getStatus(dirty, path), [dirty]);
const isDirty: GetFieldStatus<Values> = (path) => {
const isOverridden = Object.keys(manualDirtyOverride).length > 0;

if (isOverridden) {
return getStatus(manualDirtyOverride, path);
}

if (path) {
const sliceOfValues = getPath(path, values);
const sliceOfInitialValues = getPath(path, _dirtyValues.current);
return !isEqual(sliceOfValues, sliceOfInitialValues);
}

return !isEqual(values, _dirtyValues.current);
};

const isTouched: GetFieldStatus<Values> = useCallback(
(path) => getStatus(touched, path),
[touched]
Expand Down Expand Up @@ -239,7 +266,7 @@ export function useForm<
isDirty,
isTouched,
setTouched,
setDirty,
setDirty: setDirtyManualOverride,
resetTouched,
resetDirty,
isValid,
Expand Down

0 comments on commit bf76ebb

Please sign in to comment.