diff --git a/src/mantine-form/src/tests/dirty.test.ts b/src/mantine-form/src/tests/dirty.test.ts index 24a172b3ea4..344692e521a 100644 --- a/src/mantine-form/src/tests/dirty.test.ts +++ b/src/mantine-form/src/tests/dirty.test.ts @@ -38,6 +38,26 @@ describe('@mantine/form/dirty', () => { expect(hook.result.current.isDirty()).toBe(true); act(() => hook.result.current.resetDirty()); + expect(hook.result.current.isDirty()).toBe(false); }); + + it('sets list field as dirty is 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); + }); }); diff --git a/src/mantine-form/src/types.ts b/src/mantine-form/src/types.ts index 21b12db1fb0..76885e83978 100644 --- a/src/mantine-form/src/types.ts +++ b/src/mantine-form/src/types.ts @@ -70,6 +70,7 @@ export type SetFieldValue = >( ) => 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; diff --git a/src/mantine-form/src/use-form.ts b/src/mantine-form/src/use-form.ts index 9fdb9ddcf51..2dca0da7fb5 100644 --- a/src/mantine-form/src/use-form.ts +++ b/src/mantine-form/src/use-form.ts @@ -28,6 +28,7 @@ import { ResetDirty, IsValid, _TransformValues, + ClearFieldDirty, } from './types'; export function useForm< @@ -36,8 +37,8 @@ export function useForm< >({ initialValues = {} as Values, initialErrors = {}, - initialDirty = {}, initialTouched = {}, + initialDirty = {}, clearInputErrorOnChange = true, validateInputOnChange = false, validateInputOnBlur = false, @@ -45,7 +46,7 @@ export function useForm< validate: rules, }: UseFormInput = {}): UseFormReturnType { 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(initialValues); @@ -56,7 +57,7 @@ export function useForm< const resetTouched = useCallback(() => setTouched({}), []); const resetDirty: ResetDirty = (_values) => { _setDirtyValues(_values || values); - setDirty({}); + setDirtyManualOverride({}); }; const setErrors: SetErrors = useCallback( @@ -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 = 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) { @@ -123,21 +135,21 @@ export function useForm< clearInputErrorOnChange && clearErrors(); }, []); - const reorderListItem: ReorderListItem = useCallback( - (path, payload) => _setValues((current) => reorderPath(path, payload, current)), - [] - ); + const reorderListItem: ReorderListItem = useCallback((path, payload) => { + clearDirtyOverride(path); + _setValues((current) => reorderPath(path, payload, current)); + }, []); const removeListItem: RemoveListItem = 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 = useCallback( - (path, item, index) => _setValues((current) => insertPath(path, item, index, current)), - [] - ); + const insertListItem: InsertListItem = useCallback((path, item, index) => { + clearDirtyOverride(path); + _setValues((current) => insertPath(path, item, index, current)); + }, []); const validate: Validate = useCallback(() => { const results = validateValues(rules, values); @@ -204,7 +216,22 @@ export function useForm< reset(); }, []); - const isDirty: GetFieldStatus = useCallback((path) => getStatus(dirty, path), [dirty]); + const isDirty: GetFieldStatus = (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 = useCallback( (path) => getStatus(touched, path), [touched] @@ -239,7 +266,7 @@ export function useForm< isDirty, isTouched, setTouched, - setDirty, + setDirty: setDirtyManualOverride, resetTouched, resetDirty, isValid,