Skip to content

Commit

Permalink
[@mantine/form] Fix incorrect dirty state of the fields calculation a…
Browse files Browse the repository at this point in the history
…fter one of list actions was called (#3025)

* [@mantine/form] useForm: remove dirty state

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.

* [@mantine/form] useForm: revert renaming of dirty state
  • Loading branch information
gdostie committed Dec 4, 2022
1 parent f729437 commit fd56c7d
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 15 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
57 changes: 42 additions & 15 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 Down Expand Up @@ -92,14 +93,25 @@ export function useForm<
[]
);

const clearFieldDirty: ClearFieldDirty = useCallback(
(path) =>
setDirty((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);
clearFieldDirty(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) => {
clearFieldDirty(path);
_setValues((current) => reorderPath(path, payload, current));
}, []);

const removeListItem: RemoveListItem<Values> = useCallback((path, index) => {
clearFieldDirty(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) => {
clearFieldDirty(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(dirty).length > 0;

if (isOverridden) {
return getStatus(dirty, 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

0 comments on commit fd56c7d

Please sign in to comment.