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

fix: manually track the dirty state #128

Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/giant-cooks-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@formwerk/core': patch
---

fix: track dirty state manually
2 changes: 2 additions & 0 deletions packages/core/src/types/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type FormSchema<TInput extends FormObject = FormObject, TOutput = TInput>

export type TouchedSchema<TForm extends FormObject> = Simplify<Schema<TForm, boolean>>;

export type DirtySchema<TForm extends FormObject> = Simplify<Schema<TForm, boolean>>;

export type DisabledSchema<TForm extends FormObject> = Partial<Record<Path<TForm>, boolean>>;

export type ErrorsSchema<TForm extends FormObject> = Partial<Record<Path<TForm>, string[]>>;
Expand Down
53 changes: 51 additions & 2 deletions packages/core/src/useForm/formContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StandardSchema,
ErrorsSchema,
IssueCollection,
DirtySchema,
} from '../types';
import { cloneDeep, isEqual, normalizeArrayable } from '../utils/common';
import {
Expand All @@ -35,6 +36,8 @@ export interface BaseFormContext<TForm extends FormObject = FormObject> {
setTouched<TPath extends Path<TForm>>(path: TPath, value: boolean): void;
isTouched<TPath extends Path<TForm>>(path?: TPath): boolean;
isDirty<TPath extends Path<TForm>>(path?: TPath): boolean;
setDirty(value: boolean): void;
setDirty<TPath extends Path<TForm>>(path: TPath, value: boolean): void;
isFieldSet<TPath extends Path<TForm>>(path: TPath): boolean;
getFieldInitialValue<TPath extends Path<TForm>>(path: TPath): PathValue<TForm, TPath>;
getFieldOriginalValue<TPath extends Path<TForm>>(path: TPath): PathValue<TForm, TPath>;
Expand All @@ -54,6 +57,7 @@ export interface BaseFormContext<TForm extends FormObject = FormObject> {
setValues: (newValues: Partial<TForm>, opts?: SetValueOptions) => void;
revertValues: () => void;
revertTouched: () => void;
revertDirty: () => void;
isPathDisabled: (path: Path<TForm>) => boolean;
}

Expand All @@ -65,13 +69,15 @@ export interface FormContextCreateOptions<TForm extends FormObject = FormObject,
id: string;
values: TForm;
touched: TouchedSchema<TForm>;
dirty: DirtySchema<TForm>;
disabled: DisabledSchema<TForm>;
errors: Ref<ErrorsSchema<TForm>>;
submitErrors: Ref<ErrorsSchema<TForm>>;
schema: StandardSchema<TForm, TOutput> | undefined;
snapshots: {
values: FormSnapshot<TForm>;
touched: FormSnapshot<TouchedSchema<TForm>>;
dirty: FormSnapshot<DirtySchema<TForm>>;
};
}

Expand All @@ -80,13 +86,16 @@ export function createFormContext<TForm extends FormObject = FormObject, TOutput
values,
disabled,
errors,
dirty,
submitErrors,
schema,
touched,
snapshots,
}: FormContextCreateOptions<TForm, TOutput>): BaseFormContext<TForm> {
function setValue<TPath extends Path<TForm>>(path: TPath, value: PathValue<TForm, TPath> | undefined) {
setInPath(values, path, cloneDeep(value));
const oldValue = getFieldOriginalValue(path);
setDirty(path, !isEqual(oldValue, value));
}

function setTouched(value: boolean): void;
Expand Down Expand Up @@ -123,10 +132,29 @@ export function createFormContext<TForm extends FormObject = FormObject, TOutput

function isDirty<TPath extends Path<TForm>>(path?: TPath) {
if (!path) {
return !isEqual(values, snapshots.values.originals.value);
return !!findLeaf(dirty, l => l === true);
}

return !isEqual(getValue(path), getFieldOriginalValue(path));
const value = getFromPath(dirty, path);
if (isObject(value)) {
return !!findLeaf(value, v => !!v);
}

return !!value;
}

function setDirty(value: boolean): void;
function setDirty<TPath extends Path<TForm>>(path: TPath, value: boolean): void;
function setDirty<TPath extends Path<TForm>>(pathOrValue: TPath | boolean, valueOrUndefined?: boolean) {
if (typeof pathOrValue === 'boolean') {
for (const key in dirty) {
setInPath(dirty, key, pathOrValue, true);
}

return;
}

setInPath(dirty, pathOrValue, valueOrUndefined, true);
}

function isFieldSet<TPath extends Path<TForm>>(path: TPath) {
Expand Down Expand Up @@ -267,6 +295,21 @@ export function createFormContext<TForm extends FormObject = FormObject, TOutput
merge(touched, newTouched);
}

function updateDirty(newDirty: Partial<DirtySchema<TForm>>, opts?: SetValueOptions) {
if (opts?.behavior === 'merge') {
merge(dirty, newDirty);

return;
}

// Delete all keys, then set new values
Object.keys(dirty).forEach(key => {
delete dirty[key as keyof typeof dirty];
});

merge(dirty, newDirty);
}

function clearErrors(path?: string) {
if (!path) {
errors.value = {} as ErrorsSchema<TForm>;
Expand Down Expand Up @@ -301,6 +344,10 @@ export function createFormContext<TForm extends FormObject = FormObject, TOutput
updateTouched(cloneDeep(snapshots.touched.originals.value), { behavior: 'replace' });
}

function revertDirty() {
updateDirty(cloneDeep(snapshots.dirty.originals.value), { behavior: 'replace' });
}

function getValidationMode(): FormValidationMode {
return schema ? 'schema' : 'aggregate';
}
Expand All @@ -314,6 +361,7 @@ export function createFormContext<TForm extends FormObject = FormObject, TOutput
getValue,
isTouched,
isDirty,
setDirty,
isFieldSet,
destroyPath,
unsetPath,
Expand All @@ -322,6 +370,7 @@ export function createFormContext<TForm extends FormObject = FormObject, TOutput
revertValues,
revertTouched,
setInitialValues,
revertDirty,
setInitialTouched,
getFieldOriginalValue,
setFieldDisabled,
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/useForm/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
GroupValidationResult,
GenericFormSchema,
StandardSchema,
DirtySchema,
} from '../types';
import { createFormContext, BaseFormContext } from './formContext';
import { FormTransactionManager, useFormTransactions } from './useFormTransactions';
Expand Down Expand Up @@ -44,6 +45,11 @@ export interface FormProps<
*/
initialTouched?: TouchedSchema<TInput>;

/**
* The initial dirty state for form fields.
*/
initialDirty?: DirtySchema<TInput>;

/**
* The validation schema for the form.
*/
Expand Down Expand Up @@ -90,6 +96,7 @@ export function useForm<
TOutput extends FormObject = StandardSchemaV1.InferOutput<TSchema>,
>(props?: Partial<FormProps<TSchema, TInput>>) {
const touchedSnapshot = useFormSnapshots(props?.initialTouched);
const dirtySnapshot = useFormSnapshots(props?.initialDirty);
const valuesSnapshot = useFormSnapshots<TInput, TOutput>(props?.initialValues as TInput, {
onAsyncInit,
schema: props?.schema as StandardSchema<TInput, TOutput>,
Expand All @@ -100,6 +107,7 @@ export function useForm<
const isHtmlValidationDisabled = () => props?.disableHtmlValidation ?? getConfig().disableHtmlValidation;
const values = reactive(cloneDeep(valuesSnapshot.originals.value)) as PartialDeep<TInput>;
const touched = reactive(cloneDeep(touchedSnapshot.originals.value)) as TouchedSchema<TInput>;
const dirty = reactive(cloneDeep(dirtySnapshot.originals.value)) as DirtySchema<TInput>;
const disabled = reactive({}) as DisabledSchema<TInput>;
const errors = ref({}) as Ref<ErrorsSchema<TInput>>;
const submitErrors = ref({}) as Ref<ErrorsSchema<TInput>>;
Expand All @@ -109,12 +117,14 @@ export function useForm<
values: values as TInput,
touched,
disabled,
dirty,
schema: props?.schema as StandardSchema<TInput, TOutput>,
errors,
submitErrors,
snapshots: {
values: valuesSnapshot,
touched: touchedSnapshot,
dirty: dirtySnapshot,
},
});

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/useForm/useFormActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export function useFormActions<TForm extends FormObject = FormObject, TOutput ex

form.revertValues();
form.revertTouched();
form.revertDirty();
submitAttemptsCount.value = 0;
isSubmitAttempted.value = false;

Expand Down
22 changes: 13 additions & 9 deletions packages/core/src/useForm/useFormTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { nextTick } from 'vue';
import { FormObject, Path, PathValue } from '../types';
import { BaseFormContext } from './formContext';

interface SetPathStateTransaction<TForm extends FormObject> {
kind: 2;
interface BaseStateTransaction<TForm extends FormObject> {
path: Path<TForm>;
value: PathValue<TForm, Path<TForm>>;
touched: boolean;
dirty: boolean;
disabled: boolean;
errors: string[];
}

interface SetPathStateTransaction<TForm extends FormObject> extends BaseStateTransaction<TForm> {
kind: 2;
}

interface UnsetPathStateTransaction<TForm extends FormObject> {
kind: 1;
path: Path<TForm>;
Expand All @@ -21,13 +25,8 @@ interface DestroyPathStateTransaction<TForm extends FormObject> {
path: Path<TForm>;
}

interface InitializeFieldTransaction<TForm extends FormObject> {
interface InitializeFieldTransaction<TForm extends FormObject> extends BaseStateTransaction<TForm> {
kind: 3;
path: Path<TForm>;
value: PathValue<TForm, Path<TForm>>;
touched: boolean;
disabled: boolean;
errors: string[];
}

export type FormTransaction<TForm extends FormObject> =
Expand All @@ -49,7 +48,10 @@ const TransactionKind = {
export interface FormTransactionManager<TForm extends FormObject> {
transaction(
tr: (
formCtx: Pick<BaseFormContext<TForm>, 'getValues' | 'getValue' | 'isFieldSet' | 'isTouched' | 'getErrors'>,
formCtx: Pick<
BaseFormContext<TForm>,
'getValues' | 'getValue' | 'isFieldSet' | 'isTouched' | 'getErrors' | 'isDirty'
>,
codes: typeof TransactionKind,
) => FormTransaction<TForm> | null,
): void;
Expand Down Expand Up @@ -89,6 +91,7 @@ export function useFormTransactions<TForm extends FormObject>(form: BaseFormCont
if (tr.kind === TransactionKind.SET_PATH) {
form.setValue(tr.path, tr.value);
form.setTouched(tr.path, tr.touched);
form.setDirty(tr.path, tr.dirty);
form.setFieldDisabled(tr.path, tr.disabled);
form.setErrors(tr.path, tr.errors);
continue;
Expand All @@ -109,6 +112,7 @@ export function useFormTransactions<TForm extends FormObject>(form: BaseFormCont
form.setValue(tr.path, tr.value ?? formInit);
form.setFieldDisabled(tr.path, tr.disabled);
form.setTouched(tr.path, tr.touched);
form.setDirty(tr.path, tr.dirty);
form.unsetInitialValue(tr.path);
form.setErrors(tr.path, tr.errors);
continue;
Expand Down
36 changes: 28 additions & 8 deletions packages/core/src/useFormField/useFormField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface FormFieldOptions<TValue = unknown> {
path: MaybeRefOrGetter<string | undefined> | undefined;
initialValue: TValue;
initialTouched: boolean;
initialDirty: boolean;
syncModel: boolean;
modelName: string;
disabled: MaybeRefOrGetter<boolean | undefined>;
Expand Down Expand Up @@ -138,7 +139,14 @@ export function useFormField<TValue = unknown>(opts?: Partial<FormFieldOptions<T
return field;
}

initFormPathIfNecessary(form, getPath, initialValue, opts?.initialTouched ?? false, isDisabled);
initFormPathIfNecessary({
form,
getPath,
initialValue,
initialTouched: opts?.initialTouched ?? false,
initialDirty: opts?.initialDirty ?? false,
isDisabled,
});

form.onSubmitAttempt(() => {
setTouched(true);
Expand Down Expand Up @@ -175,6 +183,7 @@ export function useFormField<TValue = unknown>(opts?: Partial<FormFieldOptions<T
path: newPath,
value: cloneDeep(oldPath ? tf.getValue(oldPath) : pathlessValue.value),
touched: oldPath ? tf.isTouched(oldPath) : pathlessTouched.value,
dirty: oldPath ? tf.isDirty(oldPath) : isDirty.value,
disabled: isDisabled.value,
errors: [...(oldPath ? tf.getErrors(oldPath) : pathlessValidity.errors.value)],
};
Expand Down Expand Up @@ -298,16 +307,26 @@ function createLocalValueRef<TValue = unknown>(initialValue?: TValue) {
};
}

interface FormPathInitOptions {
form: FormContext;
getPath: Getter<string | undefined>;
initialValue: unknown;
initialTouched: boolean;
initialDirty: boolean;
isDisabled: MaybeRefOrGetter<boolean>;
}

/**
* Sets the initial value of the form if not already set and if an initial value is provided.
*/
function initFormPathIfNecessary(
form: FormContext,
getPath: Getter<string | undefined>,
initialValue: unknown,
initialTouched: boolean,
isDisabled: MaybeRefOrGetter<boolean>,
) {
function initFormPathIfNecessary({
form,
getPath,
initialValue,
initialTouched,
initialDirty,
isDisabled,
}: FormPathInitOptions) {
const path = getPath();
if (!path) {
return;
Expand All @@ -320,6 +339,7 @@ function initFormPathIfNecessary(
path,
value: initialValue ?? form.getFieldInitialValue(path),
touched: initialTouched,
dirty: initialDirty,
disabled: toValue(isDisabled),
errors: [...tf.getErrors(path)],
}));
Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/useFormGroup/useFormGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
StandardSchema,
ValidationResult,
} from '../types';
import { isEqual, normalizeProps, useUniqId, warn, withRefCapture } from '../utils/common';
import { normalizeProps, useUniqId, warn, withRefCapture } from '../utils/common';
import { FormKey } from '../useForm';
import { useValidationProvider } from '../validation/useValidationProvider';
import { FormValidationMode } from '../useForm/formContext';
Expand Down Expand Up @@ -137,16 +137,16 @@ export function useFormGroup<TInput extends FormObject = FormObject, TOutput ext
}

function getValue(path?: string) {
return form?.getValue(prefixPath(path) ?? '');
if (!path) {
return form?.getValue(getPath()) ?? {};
}

return form?.getValue(prefixPath(path) || '');
}

const isValid = computed(() => getErrors().length === 0);
const isTouched = computed(() => form?.isTouched(getPath()) ?? false);
const isDirty = computed(() => {
const path = getPath();

return !isEqual(getValue(), form?.getFieldOriginalValue(path) ?? {});
});
const isDirty = computed(() => form?.isDirty(getPath()) ?? false);

function getError(path: string) {
return form?.getErrors(prefixPath(path) ?? '')?.[0];
Expand Down Expand Up @@ -236,6 +236,11 @@ export function useFormGroup<TInput extends FormObject = FormObject, TOutput ext
* Gets the group's value, passing in a path will return the value of that field.
*/
getValue,
/**
* Gets the values for the form group.
* @deprecated Use `getValue` without arguments instead.
*/
getValues: () => getValue(),
/**
* Gets the error for a given field.
*/
Expand Down
Loading
Loading