Skip to content

Commit 3f3b509

Browse files
committed
feat(core): debounced form listeners
1 parent 9f68452 commit 3f3b509

13 files changed

+1296
-14
lines changed

packages/form-core/src/FieldApi.d.ts

+377
Large diffs are not rendered by default.

packages/form-core/src/FieldApi.ts

+43-14
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,7 @@ export class FieldApi<
978978
timeoutIds: {
979979
validations: Record<ValidationCause, ReturnType<typeof setTimeout> | null>
980980
listeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
981+
formListeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
981982
}
982983

983984
/**
@@ -1011,6 +1012,7 @@ export class FieldApi<
10111012
this.timeoutIds = {
10121013
validations: {} as Record<ValidationCause, never>,
10131014
listeners: {} as Record<ListenerCause, never>,
1015+
formListeners: {} as Record<ListenerCause, never>,
10141016
}
10151017

10161018
this.store = new Derived({
@@ -1703,13 +1705,27 @@ export class FieldApi<
17031705
}
17041706

17051707
private triggerOnBlurListener() {
1706-
const debounceMs = this.options.listeners?.onBlurDebounceMs
1707-
this.form.options.listeners?.onBlur?.({
1708-
formApi: this.form,
1709-
fieldApi: this,
1710-
})
1708+
const formDebounceMs = this.form.options.listeners?.onBlurDebounceMs
1709+
if (formDebounceMs && formDebounceMs > 0) {
1710+
if (this.timeoutIds.formListeners.blur) {
1711+
clearTimeout(this.timeoutIds.formListeners.blur)
1712+
}
1713+
1714+
this.timeoutIds.formListeners.blur = setTimeout(() => {
1715+
this.form.options.listeners?.onBlur?.({
1716+
formApi: this.form,
1717+
fieldApi: this,
1718+
})
1719+
}, formDebounceMs)
1720+
} else {
1721+
this.form.options.listeners?.onBlur?.({
1722+
formApi: this.form,
1723+
fieldApi: this,
1724+
})
1725+
}
17111726

1712-
if (debounceMs && debounceMs > 0) {
1727+
const fieldDebounceMs = this.options.listeners?.onBlurDebounceMs
1728+
if (fieldDebounceMs && fieldDebounceMs > 0) {
17131729
if (this.timeoutIds.listeners.blur) {
17141730
clearTimeout(this.timeoutIds.listeners.blur)
17151731
}
@@ -1719,7 +1735,7 @@ export class FieldApi<
17191735
value: this.state.value,
17201736
fieldApi: this,
17211737
})
1722-
}, debounceMs)
1738+
}, fieldDebounceMs)
17231739
} else {
17241740
this.options.listeners?.onBlur?.({
17251741
value: this.state.value,
@@ -1729,14 +1745,27 @@ export class FieldApi<
17291745
}
17301746

17311747
private triggerOnChangeListener() {
1732-
const debounceMs = this.options.listeners?.onChangeDebounceMs
1748+
const formDebounceMs = this.form.options.listeners?.onChangeDebounceMs
1749+
if (formDebounceMs && formDebounceMs > 0) {
1750+
if (this.timeoutIds.formListeners.blur) {
1751+
clearTimeout(this.timeoutIds.formListeners.blur)
1752+
}
17331753

1734-
this.form.options.listeners?.onChange?.({
1735-
formApi: this.form,
1736-
fieldApi: this,
1737-
})
1754+
this.timeoutIds.formListeners.blur = setTimeout(() => {
1755+
this.form.options.listeners?.onChange?.({
1756+
formApi: this.form,
1757+
fieldApi: this,
1758+
})
1759+
}, formDebounceMs)
1760+
} else {
1761+
this.form.options.listeners?.onChange?.({
1762+
formApi: this.form,
1763+
fieldApi: this,
1764+
})
1765+
}
17381766

1739-
if (debounceMs && debounceMs > 0) {
1767+
const fieldDebounceMs = this.options.listeners?.onChangeDebounceMs
1768+
if (fieldDebounceMs && fieldDebounceMs > 0) {
17401769
if (this.timeoutIds.listeners.change) {
17411770
clearTimeout(this.timeoutIds.listeners.change)
17421771
}
@@ -1746,7 +1775,7 @@ export class FieldApi<
17461775
value: this.state.value,
17471776
fieldApi: this,
17481777
})
1749-
}, debounceMs)
1778+
}, fieldDebounceMs)
17501779
} else {
17511780
this.options.listeners?.onChange?.({
17521781
value: this.state.value,

packages/form-core/src/FormApi.d.ts

+462
Large diffs are not rendered by default.

packages/form-core/src/FormApi.ts

+2
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export interface FormListeners<
265265
>
266266
fieldApi: AnyFieldApi
267267
}) => void
268+
onChangeDebounceMs?: number
268269

269270
onBlur?: (props: {
270271
formApi: FormApi<
@@ -281,6 +282,7 @@ export interface FormListeners<
281282
>
282283
fieldApi: AnyFieldApi
283284
}) => void
285+
onBlurDebounceMs?: number
284286

285287
onMount?: (props: {
286288
formApi: FormApi<
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import type { FormOptions } from './FormApi';
2+
export declare function formOptions<T extends Partial<FormOptions<any, any, any, any, any, any, any, any, any, any>>>(defaultOpts: T): T;

packages/form-core/src/index.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export * from './FormApi';
2+
export * from './FieldApi';
3+
export * from './utils';
4+
export * from './util-types';
5+
export * from './types';
6+
export * from './mergeForm';
7+
export * from './formOptions';
8+
export * from './standardSchemaValidator';

packages/form-core/src/mergeForm.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { FormApi } from './FormApi';
2+
/**
3+
* @private
4+
*/
5+
export declare function mutateMergeDeep(target: object | null | undefined, source: object | null | undefined): object;
6+
export declare function mergeForm<TFormData>(baseForm: FormApi<NoInfer<TFormData>, any, any, any, any, any, any, any, any, any>, state: Partial<FormApi<TFormData, any, any, any, any, any, any, any, any, any>['state']>): FormApi<NoInfer<TFormData>, any, any, any, any, any, any, any, any, any>;
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { FormApi, FormAsyncValidateOrFn, FormValidateOrFn } from './FormApi';
2+
import type { AnyFieldMeta } from './FieldApi';
3+
import type { DeepKeys } from './util-types';
4+
type ArrayFieldMode = 'insert' | 'remove' | 'swap' | 'move';
5+
export declare const defaultFieldMeta: AnyFieldMeta;
6+
export declare function metaHelper<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(formApi: FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnServer, TSubmitMeta>): {
7+
handleArrayFieldMetaShift: (field: DeepKeys<TFormData>, index: number, mode: ArrayFieldMode, secondIndex?: number) => void;
8+
};
9+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { ValidationSource } from './types';
2+
export type TStandardSchemaValidatorValue<TData, TSource extends ValidationSource = ValidationSource> = {
3+
value: TData;
4+
validationSource: TSource;
5+
};
6+
export type TStandardSchemaValidatorIssue<TSource extends ValidationSource = ValidationSource> = TSource extends 'form' ? {
7+
form: Record<string, StandardSchemaV1Issue[]>;
8+
fields: Record<string, StandardSchemaV1Issue[]>;
9+
} : TSource extends 'field' ? StandardSchemaV1Issue[] : never;
10+
export declare const standardSchemaValidators: {
11+
validate<TSource extends ValidationSource = ValidationSource>({ value, validationSource, }: TStandardSchemaValidatorValue<unknown, TSource>, schema: StandardSchemaV1): TStandardSchemaValidatorIssue<TSource> | undefined;
12+
validateAsync<TSource extends ValidationSource>({ value, validationSource, }: TStandardSchemaValidatorValue<unknown, TSource>, schema: StandardSchemaV1): Promise<TStandardSchemaValidatorIssue<TSource> | undefined>;
13+
};
14+
export declare const isStandardSchemaValidator: (validator: unknown) => validator is StandardSchemaV1;
15+
/**
16+
* The Standard Schema interface.
17+
*/
18+
export type StandardSchemaV1<Input = unknown, Output = Input> = {
19+
/**
20+
* The Standard Schema properties.
21+
*/
22+
readonly '~standard': StandardSchemaV1Props<Input, Output>;
23+
};
24+
/**
25+
* The Standard Schema properties interface.
26+
*/
27+
interface StandardSchemaV1Props<Input = unknown, Output = Input> {
28+
/**
29+
* The version number of the standard.
30+
*/
31+
readonly version: 1;
32+
/**
33+
* The vendor name of the schema library.
34+
*/
35+
readonly vendor: string;
36+
/**
37+
* Validates unknown input values.
38+
*/
39+
readonly validate: (value: unknown) => StandardSchemaV1Result<Output> | Promise<StandardSchemaV1Result<Output>>;
40+
/**
41+
* Inferred types associated with the schema.
42+
*/
43+
readonly types?: StandardSchemaV1Types<Input, Output> | undefined;
44+
}
45+
/**
46+
* The result interface of the validate function.
47+
*/
48+
type StandardSchemaV1Result<Output> = StandardSchemaV1SuccessResult<Output> | StandardSchemaV1FailureResult;
49+
/**
50+
* The result interface if validation succeeds.
51+
*/
52+
interface StandardSchemaV1SuccessResult<Output> {
53+
/**
54+
* The typed output value.
55+
*/
56+
readonly value: Output;
57+
/**
58+
* The non-existent issues.
59+
*/
60+
readonly issues?: undefined;
61+
}
62+
/**
63+
* The result interface if validation fails.
64+
*/
65+
interface StandardSchemaV1FailureResult {
66+
/**
67+
* The issues of failed validation.
68+
*/
69+
readonly issues: ReadonlyArray<StandardSchemaV1Issue>;
70+
}
71+
/**
72+
* The issue interface of the failure output.
73+
*/
74+
export interface StandardSchemaV1Issue {
75+
/**
76+
* The error message of the issue.
77+
*/
78+
readonly message: string;
79+
/**
80+
* The path of the issue, if any.
81+
*/
82+
readonly path?: ReadonlyArray<PropertyKey | StandardSchemaV1PathSegment> | undefined;
83+
}
84+
/**
85+
* The path segment interface of the issue.
86+
*/
87+
interface StandardSchemaV1PathSegment {
88+
/**
89+
* The key representing a path segment.
90+
*/
91+
readonly key: PropertyKey;
92+
}
93+
/**
94+
* The Standard Schema types interface.
95+
*/
96+
interface StandardSchemaV1Types<Input = unknown, Output = Input> {
97+
/**
98+
* The input type of the schema.
99+
*/
100+
readonly input: Input;
101+
/**
102+
* The output type of the schema.
103+
*/
104+
readonly output: Output;
105+
}
106+
export {};

packages/form-core/src/types.d.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { DeepKeys } from './util-types';
2+
export type ValidationError = unknown;
3+
export type ValidationSource = 'form' | 'field';
4+
/**
5+
* "server" is only intended for SSR/SSG validation and should not execute anything
6+
* @private
7+
*/
8+
export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | 'server';
9+
/**
10+
* @private
11+
*/
12+
export type ListenerCause = 'change' | 'blur' | 'submit' | 'mount';
13+
/**
14+
* @private
15+
*/
16+
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`;
17+
/**
18+
* @private
19+
*/
20+
export type ValidationErrorMap<TOnMountReturn = unknown, TOnChangeReturn = unknown, TOnChangeAsyncReturn = unknown, TOnBlurReturn = unknown, TOnBlurAsyncReturn = unknown, TOnSubmitReturn = unknown, TOnSubmitAsyncReturn = unknown, TOnServerReturn = unknown> = {
21+
onMount?: TOnMountReturn;
22+
onChange?: TOnChangeReturn | TOnChangeAsyncReturn;
23+
onBlur?: TOnBlurReturn | TOnBlurAsyncReturn;
24+
onSubmit?: TOnSubmitReturn | TOnSubmitAsyncReturn;
25+
onServer?: TOnServerReturn;
26+
};
27+
/**
28+
* @private allows tracking the source of the errors in the error map
29+
*/
30+
export type ValidationErrorMapSource = {
31+
onMount?: ValidationSource;
32+
onChange?: ValidationSource;
33+
onBlur?: ValidationSource;
34+
onSubmit?: ValidationSource;
35+
onServer?: ValidationSource;
36+
};
37+
/**
38+
* @private
39+
*/
40+
export type FormValidationErrorMap<TOnMountReturn = unknown, TOnChangeReturn = unknown, TOnChangeAsyncReturn = unknown, TOnBlurReturn = unknown, TOnBlurAsyncReturn = unknown, TOnSubmitReturn = unknown, TOnSubmitAsyncReturn = unknown, TOnServerReturn = unknown> = {
41+
onMount?: TOnMountReturn;
42+
onChange?: TOnChangeReturn | TOnChangeAsyncReturn;
43+
onBlur?: TOnBlurReturn | TOnBlurAsyncReturn;
44+
onSubmit?: TOnSubmitReturn | TOnSubmitAsyncReturn;
45+
onServer?: TOnServerReturn;
46+
};
47+
export type FormValidationError<TFormData> = ValidationError | GlobalFormValidationError<TFormData>;
48+
/**
49+
* @private
50+
*
51+
* @example
52+
* ```tsx
53+
* {
54+
* form: 'This form contains an error',
55+
* fields: {
56+
* age: "Must be 13 or older to register"
57+
* }
58+
* }
59+
* ````
60+
*/
61+
export type GlobalFormValidationError<TFormData> = {
62+
form?: ValidationError;
63+
fields: Partial<Record<DeepKeys<TFormData>, ValidationError>>;
64+
};
65+
/**
66+
* @private
67+
*/
68+
export interface UpdateMetaOptions {
69+
/**
70+
* @default false
71+
*/
72+
dontUpdateMeta?: boolean;
73+
}
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @private
3+
*/
4+
export type UnwrapOneLevelOfArray<T> = T extends (infer U)[] ? U : T;
5+
type Narrowable = string | number | bigint | boolean;
6+
type NarrowRaw<A> = (A extends [] ? [] : never) | (A extends Narrowable ? A : never) | {
7+
[K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>;
8+
};
9+
type Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch;
10+
/**
11+
* @private
12+
*/
13+
export type Narrow<A> = Try<A, [], NarrowRaw<A>>;
14+
export interface AnyDeepKeyAndValue {
15+
key: string;
16+
value: any;
17+
}
18+
export type ArrayAccessor<TParent extends AnyDeepKeyAndValue> = `${TParent['key'] extends never ? '' : TParent['key']}[${number}]`;
19+
export interface ArrayDeepKeyAndValue<in out TParent extends AnyDeepKeyAndValue, in out T extends ReadonlyArray<any>> extends AnyDeepKeyAndValue {
20+
key: ArrayAccessor<TParent>;
21+
value: T[number] | Nullable<TParent['value']>;
22+
}
23+
export type DeepKeyAndValueArray<TParent extends AnyDeepKeyAndValue, T extends ReadonlyArray<any>, TAcc> = DeepKeysAndValuesImpl<NonNullable<T[number]>, ArrayDeepKeyAndValue<TParent, T>, TAcc | ArrayDeepKeyAndValue<TParent, T>>;
24+
export type TupleAccessor<TParent extends AnyDeepKeyAndValue, TKey extends string> = `${TParent['key'] extends never ? '' : TParent['key']}[${TKey}]`;
25+
export interface TupleDeepKeyAndValue<in out TParent extends AnyDeepKeyAndValue, in out T, in out TKey extends AllTupleKeys<T>> extends AnyDeepKeyAndValue {
26+
key: TupleAccessor<TParent, TKey>;
27+
value: T[TKey] | Nullable<TParent['value']>;
28+
}
29+
export type AllTupleKeys<T> = T extends any ? keyof T & `${number}` : never;
30+
export type DeepKeyAndValueTuple<TParent extends AnyDeepKeyAndValue, T extends ReadonlyArray<any>, TAcc, TAllKeys extends AllTupleKeys<T> = AllTupleKeys<T>> = TAllKeys extends any ? DeepKeysAndValuesImpl<NonNullable<T[TAllKeys]>, TupleDeepKeyAndValue<TParent, T, TAllKeys>, TAcc | TupleDeepKeyAndValue<TParent, T, TAllKeys>> : never;
31+
export type AllObjectKeys<T> = T extends any ? keyof T & (string | number) : never;
32+
export type ObjectAccessor<TParent extends AnyDeepKeyAndValue, TKey extends string | number> = TParent['key'] extends never ? `${TKey}` : `${TParent['key']}.${TKey}`;
33+
export type Nullable<T> = T & (undefined | null);
34+
export type ObjectValue<TParent extends AnyDeepKeyAndValue, T, TKey extends AllObjectKeys<T>> = T[TKey] | Nullable<TParent['value']>;
35+
export interface ObjectDeepKeyAndValue<in out TParent extends AnyDeepKeyAndValue, in out T, in out TKey extends AllObjectKeys<T>> extends AnyDeepKeyAndValue {
36+
key: ObjectAccessor<TParent, TKey>;
37+
value: ObjectValue<TParent, T, TKey>;
38+
}
39+
export type DeepKeyAndValueObject<TParent extends AnyDeepKeyAndValue, T, TAcc, TAllKeys extends AllObjectKeys<T> = AllObjectKeys<T>> = TAllKeys extends any ? DeepKeysAndValuesImpl<NonNullable<T[TAllKeys]>, ObjectDeepKeyAndValue<TParent, T, TAllKeys>, TAcc | ObjectDeepKeyAndValue<TParent, T, TAllKeys>> : never;
40+
export type UnknownAccessor<TParent extends AnyDeepKeyAndValue> = TParent['key'] extends never ? string : `${TParent['key']}.${string}`;
41+
export interface UnknownDeepKeyAndValue<TParent extends AnyDeepKeyAndValue> extends AnyDeepKeyAndValue {
42+
key: UnknownAccessor<TParent>;
43+
value: unknown;
44+
}
45+
export type DeepKeysAndValues<T> = DeepKeysAndValuesImpl<T> extends AnyDeepKeyAndValue ? DeepKeysAndValuesImpl<T> : never;
46+
export type DeepKeysAndValuesImpl<T, TParent extends AnyDeepKeyAndValue = never, TAcc = never> = unknown extends T ? TAcc | UnknownDeepKeyAndValue<TParent> : unknown extends T ? T : T extends string | number | boolean | bigint | Date ? TAcc : T extends ReadonlyArray<any> ? number extends T['length'] ? DeepKeyAndValueArray<TParent, T, TAcc> : DeepKeyAndValueTuple<TParent, T, TAcc> : keyof T extends never ? TAcc | UnknownDeepKeyAndValue<TParent> : T extends object ? DeepKeyAndValueObject<TParent, T, TAcc> : TAcc;
47+
export type DeepRecord<T> = {
48+
[TRecord in DeepKeysAndValues<T> as TRecord['key']]: TRecord['value'];
49+
};
50+
/**
51+
* The keys of an object or array, deeply nested.
52+
*/
53+
export type DeepKeys<T> = unknown extends T ? string : DeepKeysAndValues<T>['key'];
54+
/**
55+
* Infer the type of a deeply nested property within an object or an array.
56+
*/
57+
export type DeepValue<TValue, TAccessor> = unknown extends TValue ? TValue : TAccessor extends DeepKeys<TValue> ? DeepRecord<TValue>[TAccessor] : never;
58+
export {};

0 commit comments

Comments
 (0)