mobx-formly 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-form.d.ts +9 -3
- package/dist/create-form.js +5 -7
- package/dist/form.d.ts +239 -41
- package/dist/form.js +249 -318
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/path-store.d.ts +38 -0
- package/dist/path-store.js +132 -0
- package/dist/types.d.ts +88 -9
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +156 -9
- package/dist/validation.d.ts +52 -0
- package/dist/validation.js +232 -0
- package/package.json +10 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { BaseForm } from './form.js';
|
|
2
|
+
export type { Form } from './form.js';
|
|
2
3
|
export { createForm } from './create-form.js';
|
|
3
4
|
export type { InferredFormValues } from './create-form.js';
|
|
4
|
-
export type { FieldError, FieldErrors, FieldPath, FieldPathValue, FieldState, FieldStateTree, FieldValues, FormOptions, FormSchema, SafeParseFormSchema, SchemaIssue, SchemaOutput, SchemaResult, ValibotFormSchema, ValibotRunResult, RegisterOptions, RegisterReturn, ResetOptions, SetValueConfig, SubmitHandlers, } from './types.js';
|
|
5
|
+
export type { ErrorNamespacePath, FieldError, FieldErrors, FieldPath, FieldPathValue, FieldState, FieldStateTree, FieldValues, FieldValidate, FormOptions, FormSchema, GlobalErrors, ResetFieldOptions, SafeParseFormSchema, SchemaIssue, SchemaOutput, SchemaResult, StandardFormSchema, StandardSchemaIssue, StandardSchemaResult, ValibotFormSchema, ValibotRunResult, ValidateResult, Resolver, ResolverOptions, ResolverResult, RegisterOptions, RegisterReturn, ResetOptions, SetErrorConfig, SetValueConfig, SubmitHandlers, TriggerConfig, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { BaseForm } from './form.js';
|
|
2
2
|
export { createForm } from './create-form.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-indexed observable store with lazy nested proxies.
|
|
3
|
+
*
|
|
4
|
+
* Values are stored flat by dot-separated path while reads go through
|
|
5
|
+
* stable proxies that expose the same data as a nested tree. A reverse
|
|
6
|
+
* index of path counts and children sets keeps parent branches alive
|
|
7
|
+
* while any child path exists.
|
|
8
|
+
*/
|
|
9
|
+
export declare class PathStore<V extends object> {
|
|
10
|
+
private readonly store;
|
|
11
|
+
private readonly counts;
|
|
12
|
+
private readonly children;
|
|
13
|
+
private readonly proxies;
|
|
14
|
+
/** Whether a value is stored at the exact path. */
|
|
15
|
+
has(path: string): boolean;
|
|
16
|
+
/** Number of stored values. */
|
|
17
|
+
get size(): number;
|
|
18
|
+
/** Returns the stored value at the exact path. */
|
|
19
|
+
get(path: string): V | undefined;
|
|
20
|
+
/** All stored path and value pairs. */
|
|
21
|
+
entries(): Array<[string, V]>;
|
|
22
|
+
/** All stored paths. */
|
|
23
|
+
paths(): string[];
|
|
24
|
+
/** Stores a value and registers its path in the reverse index. */
|
|
25
|
+
set(path: string, value: V): void;
|
|
26
|
+
/** Removes a value and prunes empty parent branches of the index. */
|
|
27
|
+
delete(path: string): void;
|
|
28
|
+
/** Removes all values and index data. */
|
|
29
|
+
clear(): void;
|
|
30
|
+
/** Returns the stored value or creates, stores, and returns it. */
|
|
31
|
+
ensure(path: string, create: () => V): V;
|
|
32
|
+
/** Returns the stable read-only nested view of the stored values. */
|
|
33
|
+
proxy(): object;
|
|
34
|
+
private createProxy;
|
|
35
|
+
/** Drops cached proxies for the removed path and its children so churn does not accumulate. */
|
|
36
|
+
private evictProxies;
|
|
37
|
+
private addPathToIndex;
|
|
38
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { observable } from 'mobx';
|
|
2
|
+
/**
|
|
3
|
+
* Path-indexed observable store with lazy nested proxies.
|
|
4
|
+
*
|
|
5
|
+
* Values are stored flat by dot-separated path while reads go through
|
|
6
|
+
* stable proxies that expose the same data as a nested tree. A reverse
|
|
7
|
+
* index of path counts and children sets keeps parent branches alive
|
|
8
|
+
* while any child path exists.
|
|
9
|
+
*/
|
|
10
|
+
export class PathStore {
|
|
11
|
+
store = observable.map();
|
|
12
|
+
counts = new Map();
|
|
13
|
+
children = new Map();
|
|
14
|
+
proxies = new Map();
|
|
15
|
+
/** Whether a value is stored at the exact path. */
|
|
16
|
+
has(path) { return this.store.has(path); }
|
|
17
|
+
/** Number of stored values. */
|
|
18
|
+
get size() { return this.store.size; }
|
|
19
|
+
/** Returns the stored value at the exact path. */
|
|
20
|
+
get(path) { return this.store.get(path); }
|
|
21
|
+
/** All stored path and value pairs. */
|
|
22
|
+
entries() { return [...this.store.entries()]; }
|
|
23
|
+
/** All stored paths. */
|
|
24
|
+
paths() { return [...this.store.keys()]; }
|
|
25
|
+
/** Stores a value and registers its path in the reverse index. */
|
|
26
|
+
set(path, value) {
|
|
27
|
+
if (!this.store.has(path))
|
|
28
|
+
this.addPathToIndex(path);
|
|
29
|
+
this.store.set(path, value);
|
|
30
|
+
}
|
|
31
|
+
/** Removes a value and prunes empty parent branches of the index. */
|
|
32
|
+
delete(path) {
|
|
33
|
+
if (!this.store.delete(path))
|
|
34
|
+
return;
|
|
35
|
+
this.evictProxies(path);
|
|
36
|
+
const parts = path.split('.');
|
|
37
|
+
for (let index = parts.length; index > 0; index -= 1) {
|
|
38
|
+
const current = parts.slice(0, index).join('.');
|
|
39
|
+
const count = (this.counts.get(current) ?? 1) - 1;
|
|
40
|
+
if (count > 0) {
|
|
41
|
+
this.counts.set(current, count);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
this.counts.delete(current);
|
|
45
|
+
const parent = parts.slice(0, index - 1).join('.');
|
|
46
|
+
const siblings = this.children.get(parent);
|
|
47
|
+
siblings?.delete(parts[index - 1]);
|
|
48
|
+
if (siblings?.size === 0) {
|
|
49
|
+
this.children.delete(parent);
|
|
50
|
+
if (parent)
|
|
51
|
+
this.proxies.delete(parent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Removes all values and index data. */
|
|
56
|
+
clear() {
|
|
57
|
+
this.store.clear();
|
|
58
|
+
this.counts.clear();
|
|
59
|
+
this.children.clear();
|
|
60
|
+
// The root proxy stays cached so public getters keep a stable identity across clearErrors and reset.
|
|
61
|
+
const root = this.proxies.get('');
|
|
62
|
+
this.proxies.clear();
|
|
63
|
+
if (root)
|
|
64
|
+
this.proxies.set('', root);
|
|
65
|
+
}
|
|
66
|
+
/** Returns the stored value or creates, stores, and returns it. */
|
|
67
|
+
ensure(path, create) {
|
|
68
|
+
const existing = this.store.get(path);
|
|
69
|
+
if (existing)
|
|
70
|
+
return existing;
|
|
71
|
+
this.set(path, create());
|
|
72
|
+
return this.store.get(path);
|
|
73
|
+
}
|
|
74
|
+
/** Returns the stable read-only nested view of the stored values. */
|
|
75
|
+
proxy() {
|
|
76
|
+
return this.createProxy('');
|
|
77
|
+
}
|
|
78
|
+
createProxy(path) {
|
|
79
|
+
const cached = this.proxies.get(path);
|
|
80
|
+
if (cached)
|
|
81
|
+
return cached;
|
|
82
|
+
const proxy = new Proxy({}, {
|
|
83
|
+
get: (_, property) => {
|
|
84
|
+
if (typeof property !== 'string')
|
|
85
|
+
return undefined;
|
|
86
|
+
const current = this.store.get(path);
|
|
87
|
+
if (current && property in current)
|
|
88
|
+
return current[property];
|
|
89
|
+
const childPath = path ? `${path}.${property}` : property;
|
|
90
|
+
const value = this.store.get(childPath);
|
|
91
|
+
if (value && !this.children.has(childPath))
|
|
92
|
+
return value;
|
|
93
|
+
return this.counts.has(childPath) ? this.createProxy(childPath) : undefined;
|
|
94
|
+
},
|
|
95
|
+
ownKeys: () => [...Object.keys(this.store.get(path) ?? {}), ...(this.children.get(path) ?? [])],
|
|
96
|
+
getOwnPropertyDescriptor: (_, property) => {
|
|
97
|
+
if (typeof property !== 'string')
|
|
98
|
+
return undefined;
|
|
99
|
+
const value = this.store.get(path);
|
|
100
|
+
if (value && property in value)
|
|
101
|
+
return { configurable: true, enumerable: true, value: value[property] };
|
|
102
|
+
return this.children.get(path)?.has(property) ? { configurable: true, enumerable: true } : undefined;
|
|
103
|
+
},
|
|
104
|
+
set: () => false,
|
|
105
|
+
deleteProperty: () => false,
|
|
106
|
+
});
|
|
107
|
+
this.proxies.set(path, proxy);
|
|
108
|
+
return proxy;
|
|
109
|
+
}
|
|
110
|
+
/** Drops cached proxies for the removed path and its children so churn does not accumulate. */
|
|
111
|
+
evictProxies(path) {
|
|
112
|
+
if (path)
|
|
113
|
+
this.proxies.delete(path);
|
|
114
|
+
const prefix = `${path}.`;
|
|
115
|
+
for (const key of this.proxies.keys()) {
|
|
116
|
+
if (key.startsWith(prefix))
|
|
117
|
+
this.proxies.delete(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
addPathToIndex(path) {
|
|
121
|
+
const parts = path.split('.');
|
|
122
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
123
|
+
const current = parts.slice(0, index).join('.');
|
|
124
|
+
this.counts.set(current, (this.counts.get(current) ?? 0) + 1);
|
|
125
|
+
const parent = parts.slice(0, index - 1).join('.');
|
|
126
|
+
let siblings = this.children.get(parent);
|
|
127
|
+
if (!siblings)
|
|
128
|
+
this.children.set(parent, siblings = new Set());
|
|
129
|
+
siblings.add(parts[index - 1]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,28 @@ export interface SchemaIssue {
|
|
|
8
8
|
}>;
|
|
9
9
|
message: string;
|
|
10
10
|
}
|
|
11
|
+
export interface StandardSchemaIssue {
|
|
12
|
+
message: string;
|
|
13
|
+
path?: readonly (PropertyKey | {
|
|
14
|
+
readonly key: PropertyKey;
|
|
15
|
+
} | {
|
|
16
|
+
readonly toString: () => string;
|
|
17
|
+
})[];
|
|
18
|
+
}
|
|
19
|
+
export type StandardSchemaResult<T> = {
|
|
20
|
+
value: T;
|
|
21
|
+
issues?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
value?: never;
|
|
24
|
+
issues: readonly StandardSchemaIssue[];
|
|
25
|
+
};
|
|
26
|
+
export interface StandardFormSchema<T> {
|
|
27
|
+
readonly '~standard': {
|
|
28
|
+
readonly version: 1;
|
|
29
|
+
readonly vendor: string;
|
|
30
|
+
readonly validate: (value: unknown) => StandardSchemaResult<T> | Promise<StandardSchemaResult<T>>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
11
33
|
export type SchemaResult<T> = {
|
|
12
34
|
success: true;
|
|
13
35
|
data: T;
|
|
@@ -34,25 +56,36 @@ export interface ValibotRunResult<T> {
|
|
|
34
56
|
issues?: SchemaIssue[];
|
|
35
57
|
}
|
|
36
58
|
/** Schema contract accepted by Form. Both Zod and Valibot schemas fit this interface. */
|
|
37
|
-
export type FormSchema<T> = SafeParseFormSchema<T> | ValibotFormSchema<T>;
|
|
38
|
-
export type SchemaOutput<S> = S extends SafeParseFormSchema<infer T> ? T : S extends {
|
|
59
|
+
export type FormSchema<T> = SafeParseFormSchema<T> | ValibotFormSchema<T> | StandardFormSchema<T>;
|
|
60
|
+
export type SchemaOutput<S> = S extends SafeParseFormSchema<infer T> ? T : S extends StandardFormSchema<infer T> ? T : S extends {
|
|
39
61
|
readonly '~types'?: {
|
|
40
62
|
readonly output: infer T;
|
|
41
63
|
};
|
|
42
64
|
} ? T : never;
|
|
43
65
|
/** Dot-separated path into the form value tree, for example `user.email`. */
|
|
44
66
|
export type FieldPath<T = FieldValues> = 'root' | {
|
|
45
|
-
[K in Extract<keyof T, string>]:
|
|
67
|
+
[K in Extract<keyof T, string>]: FieldPathForValue<K, T[K]>;
|
|
46
68
|
}[Extract<keyof T, string>];
|
|
47
|
-
|
|
69
|
+
type FieldPathForValue<K extends string, V> = V extends readonly (infer I)[] ? I extends object ? K | `${K}.${number}` | `${K}.${number}.${FieldPath<I>}` : K | `${K}.${number}` : V extends ReadonlyMap<infer MapKey, infer MapValue> ? MapKey extends string ? MapValue extends object ? K | `${K}.${MapKey}` | `${K}.${MapKey}.${FieldPath<MapValue>}` : K | `${K}.${MapKey}` : K : V extends Date | RegExp | ReadonlySet<unknown> ? K : V extends object ? K | `${K}.${FieldPath<V>}` : K;
|
|
70
|
+
export type FieldPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? FieldPathValue<T[K], Rest> : T extends readonly (infer I)[] ? FieldPathValue<I, Rest> : T extends ReadonlyMap<infer MapKey, infer MapValue> ? K extends MapKey ? FieldPathValue<MapValue, Rest> : never : never : P extends keyof T ? T[P] : T extends readonly (infer I)[] ? P extends `${number}` ? I : never : T extends ReadonlyMap<infer MapKey, infer MapValue> ? P extends MapKey ? MapValue : never : never;
|
|
48
71
|
export interface FieldError {
|
|
49
72
|
type: string;
|
|
50
73
|
message?: string;
|
|
74
|
+
types?: Record<string, string | true | string[]>;
|
|
51
75
|
}
|
|
76
|
+
/** Result of a single field validator: pass, one message, or several messages. */
|
|
77
|
+
export type ValidateResult = boolean | string | string[] | undefined;
|
|
78
|
+
/** Checks a field value against the rest of the form values. */
|
|
79
|
+
export type FieldValidate<TValue = unknown, TValues extends FieldValues = FieldValues> = (value: TValue, values: TValues) => ValidateResult | Promise<ValidateResult>;
|
|
80
|
+
export type ErrorNamespacePath = 'root' | `root.${string}`;
|
|
81
|
+
export type GlobalErrors = FieldError & Record<string, FieldError | undefined>;
|
|
82
|
+
type FieldErrorTree<V> = V extends readonly (infer I)[] ? FieldError & Array<I extends object ? FieldErrors<I> : FieldError | undefined> : V extends ReadonlyMap<infer MapKey, infer MapValue> ? FieldError & (MapKey extends string ? {
|
|
83
|
+
[K in MapKey]?: FieldErrorTree<MapValue>;
|
|
84
|
+
} : {}) : V extends Date | RegExp | ReadonlySet<unknown> ? FieldError : V extends object ? FieldError & FieldErrors<V> : FieldError;
|
|
52
85
|
export type FieldErrors<T extends object = FieldValues> = {
|
|
53
|
-
[K in keyof T]?:
|
|
86
|
+
[K in keyof T]?: FieldErrorTree<T[K]>;
|
|
54
87
|
} & {
|
|
55
|
-
root?:
|
|
88
|
+
root?: GlobalErrors;
|
|
56
89
|
};
|
|
57
90
|
export interface FieldState {
|
|
58
91
|
invalid: boolean;
|
|
@@ -61,8 +94,11 @@ export interface FieldState {
|
|
|
61
94
|
isValidating: boolean;
|
|
62
95
|
error?: FieldError;
|
|
63
96
|
}
|
|
97
|
+
type FieldStateValueTree<V> = V extends readonly (infer I)[] ? FieldState & Array<I extends object ? FieldStateTree<I> : FieldState | undefined> : V extends ReadonlyMap<infer MapKey, infer MapValue> ? FieldState & (MapKey extends string ? {
|
|
98
|
+
[K in MapKey]?: FieldStateValueTree<MapValue>;
|
|
99
|
+
} : {}) : V extends Date | RegExp | ReadonlySet<unknown> ? FieldState : V extends object ? FieldState & FieldStateTree<V> : FieldState;
|
|
64
100
|
export type FieldStateTree<T extends object = FieldValues> = {
|
|
65
|
-
[K in keyof T]?:
|
|
101
|
+
[K in keyof T]?: FieldStateValueTree<T[K]>;
|
|
66
102
|
} & {
|
|
67
103
|
root?: FieldState;
|
|
68
104
|
};
|
|
@@ -72,6 +108,7 @@ export interface FormState<T extends FieldValues = FieldValues> {
|
|
|
72
108
|
touchedFields: Partial<Record<FieldPath<T>, true>>;
|
|
73
109
|
validatingFields: Partial<Record<FieldPath<T>, true>>;
|
|
74
110
|
isDirty: boolean;
|
|
111
|
+
isTouched: boolean;
|
|
75
112
|
isValid: boolean;
|
|
76
113
|
isValidating: boolean;
|
|
77
114
|
isSubmitting: boolean;
|
|
@@ -102,7 +139,9 @@ export interface RegisterOptions<T extends FieldValues = FieldValues> {
|
|
|
102
139
|
value: RegExp;
|
|
103
140
|
message?: string;
|
|
104
141
|
};
|
|
105
|
-
validate?:
|
|
142
|
+
validate?: FieldValidate<unknown, T> | Record<string, FieldValidate<unknown, T>>;
|
|
143
|
+
/** Field paths re-validated whenever this field changes. */
|
|
144
|
+
deps?: FieldPath<T> | FieldPath<T>[];
|
|
106
145
|
valueAsNumber?: boolean;
|
|
107
146
|
valueAsDate?: boolean;
|
|
108
147
|
setValueAs?: (value: unknown) => unknown;
|
|
@@ -118,23 +157,63 @@ export interface SetValueConfig {
|
|
|
118
157
|
shouldDirty?: boolean;
|
|
119
158
|
shouldTouch?: boolean;
|
|
120
159
|
}
|
|
160
|
+
export interface SetErrorConfig {
|
|
161
|
+
shouldFocus?: boolean;
|
|
162
|
+
}
|
|
163
|
+
export interface TriggerConfig {
|
|
164
|
+
shouldFocus?: boolean;
|
|
165
|
+
shouldTouch?: boolean;
|
|
166
|
+
}
|
|
167
|
+
export interface ResetFieldOptions<T extends FieldValues, P extends FieldPath<T>> {
|
|
168
|
+
keepDirty?: boolean;
|
|
169
|
+
keepTouched?: boolean;
|
|
170
|
+
keepError?: boolean;
|
|
171
|
+
defaultValue?: FieldPathValue<T, P>;
|
|
172
|
+
}
|
|
121
173
|
export interface ResetOptions {
|
|
122
174
|
keepDefaultValues?: boolean;
|
|
175
|
+
keepValues?: boolean;
|
|
123
176
|
keepDirty?: boolean;
|
|
177
|
+
keepDirtyValues?: boolean;
|
|
124
178
|
keepTouched?: boolean;
|
|
125
179
|
keepErrors?: boolean;
|
|
180
|
+
keepIsValid?: boolean;
|
|
181
|
+
keepIsValidating?: boolean;
|
|
126
182
|
keepIsSubmitted?: boolean;
|
|
183
|
+
keepIsSubmitSuccessful?: boolean;
|
|
127
184
|
keepSubmitCount?: boolean;
|
|
128
185
|
}
|
|
129
186
|
export interface FormOptions<T extends FieldValues> {
|
|
130
187
|
defaultValues?: Partial<T>;
|
|
131
188
|
values?: Partial<T>;
|
|
132
189
|
schema?: FormSchema<T>;
|
|
133
|
-
|
|
190
|
+
/** A resolver may submit transformed values, so its output type is not tied to the field values type. */
|
|
191
|
+
resolver?: Resolver<T, any>;
|
|
192
|
+
context?: unknown;
|
|
193
|
+
mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'onTouched' | 'all';
|
|
134
194
|
reValidateMode?: 'onChange' | 'onBlur';
|
|
195
|
+
criteriaMode?: 'firstError' | 'all';
|
|
196
|
+
delayError?: number;
|
|
197
|
+
shouldUseNativeValidation?: boolean;
|
|
198
|
+
shouldFocusError?: boolean;
|
|
135
199
|
disabled?: boolean;
|
|
136
200
|
}
|
|
201
|
+
export interface ResolverOptions<T extends FieldValues> {
|
|
202
|
+
criteriaMode?: 'firstError' | 'all';
|
|
203
|
+
fields: Record<string, RegisterOptions<T>>;
|
|
204
|
+
names?: FieldPath<T>[];
|
|
205
|
+
shouldUseNativeValidation?: boolean;
|
|
206
|
+
}
|
|
207
|
+
export type ResolverResult<T extends FieldValues, TTransformedValues = T> = {
|
|
208
|
+
values: TTransformedValues;
|
|
209
|
+
errors: Record<string, never>;
|
|
210
|
+
} | {
|
|
211
|
+
values: Record<string, never>;
|
|
212
|
+
errors: FieldErrors<T>;
|
|
213
|
+
};
|
|
214
|
+
export type Resolver<T extends FieldValues = FieldValues, TTransformedValues = T> = (values: T, context: unknown, options: ResolverOptions<T>) => ResolverResult<T, TTransformedValues> | Promise<ResolverResult<T, TTransformedValues>>;
|
|
137
215
|
export interface SubmitHandlers<T extends FieldValues> {
|
|
138
216
|
onValid: (values: T, form: unknown) => void | Promise<void>;
|
|
139
217
|
onInvalid?: (errors: FieldErrors<T>, form: unknown) => void | Promise<void>;
|
|
140
218
|
}
|
|
219
|
+
export {};
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import type { FieldError, FieldErrors } from './types.js';
|
|
1
2
|
export declare const clone: <T>(value: T) => T;
|
|
2
3
|
export declare const getAtPath: (source: unknown, path: string) => unknown;
|
|
3
4
|
export declare const setAtPath: (target: Record<string, unknown>, path: string, value: unknown) => void;
|
|
4
5
|
export declare const deleteAtPath: (target: Record<string, unknown>, path: string) => void;
|
|
6
|
+
/** Compares form values while preserving Maps, Sets, Dates, and RegExps. */
|
|
5
7
|
export declare const isEqual: (a: unknown, b: unknown) => boolean;
|
|
8
|
+
/** Returns leaf paths whose current values differ from their defaults. */
|
|
9
|
+
export declare const collectDirtyPaths: (values: unknown, defaultValues: unknown) => string[];
|
|
10
|
+
/** Returns leaf paths changed between two value snapshots. */
|
|
11
|
+
export declare const collectChangedPaths: (before: unknown, after: unknown) => string[];
|
|
12
|
+
/** Returns the field error stored at the path in a nested errors object. */
|
|
13
|
+
export declare const findErrorAtPath: (errors: FieldErrors, path: string) => FieldError | undefined;
|
|
14
|
+
/** Collects the flat paths of all field errors in a nested errors object. */
|
|
15
|
+
export declare const collectErrorPaths: (errors: FieldErrors, base?: string) => string[];
|
|
6
16
|
export declare const extractValue: (eventOrValue: unknown) => unknown;
|
package/dist/utils.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { isObservableMap, toJS } from 'mobx';
|
|
1
2
|
import { isUnsafeProperty } from 'yummies/data';
|
|
2
3
|
export const clone = (value) => {
|
|
3
4
|
if (typeof structuredClone === 'function') {
|
|
4
5
|
try {
|
|
5
|
-
|
|
6
|
+
// toJS strips MobX proxies so structuredClone can run; it keeps
|
|
7
|
+
// Dates, Maps, and Sets intact instead of stringifying them.
|
|
8
|
+
return structuredClone(toJS(value));
|
|
6
9
|
}
|
|
7
10
|
catch {
|
|
8
|
-
//
|
|
11
|
+
// Values may still contain non-cloneable data such as functions or DOM nodes.
|
|
9
12
|
}
|
|
10
13
|
}
|
|
11
14
|
return JSON.parse(JSON.stringify(value));
|
|
@@ -13,6 +16,8 @@ export const clone = (value) => {
|
|
|
13
16
|
export const getAtPath = (source, path) => path.split('.').reduce((value, key) => {
|
|
14
17
|
if (isUnsafeProperty(key) || value === null || value === undefined)
|
|
15
18
|
return undefined;
|
|
19
|
+
if (value instanceof Map || isObservableMap(value))
|
|
20
|
+
return value.get(key);
|
|
16
21
|
return value[key];
|
|
17
22
|
}, source);
|
|
18
23
|
export const setAtPath = (target, path, value) => {
|
|
@@ -21,14 +26,28 @@ export const setAtPath = (target, path, value) => {
|
|
|
21
26
|
for (const [index, key] of keys.slice(0, -1).entries()) {
|
|
22
27
|
if (isUnsafeProperty(key))
|
|
23
28
|
return;
|
|
24
|
-
const next = current[key];
|
|
25
29
|
const nextKey = keys[index + 1];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const create = () => /^\d+$/.test(nextKey) ? [] : {};
|
|
31
|
+
if (current instanceof Map || isObservableMap(current)) {
|
|
32
|
+
let next = current.get(key);
|
|
33
|
+
if (!next || typeof next !== 'object')
|
|
34
|
+
current.set(key, next = create());
|
|
35
|
+
current = next;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const record = current;
|
|
39
|
+
let next = record[key];
|
|
40
|
+
if (!next || typeof next !== 'object')
|
|
41
|
+
record[key] = next = create();
|
|
42
|
+
current = next;
|
|
43
|
+
}
|
|
29
44
|
}
|
|
30
45
|
const lastKey = keys.at(-1);
|
|
31
|
-
if (lastKey
|
|
46
|
+
if (!lastKey || isUnsafeProperty(lastKey))
|
|
47
|
+
return;
|
|
48
|
+
if (current instanceof Map || isObservableMap(current))
|
|
49
|
+
current.set(lastKey, value);
|
|
50
|
+
else
|
|
32
51
|
current[lastKey] = value;
|
|
33
52
|
};
|
|
34
53
|
export const deleteAtPath = (target, path) => {
|
|
@@ -39,13 +58,141 @@ export const deleteAtPath = (target, path) => {
|
|
|
39
58
|
const parent = keys.reduce((value, key) => {
|
|
40
59
|
if (isUnsafeProperty(key) || !value || typeof value !== 'object')
|
|
41
60
|
return undefined;
|
|
61
|
+
if (value instanceof Map || isObservableMap(value))
|
|
62
|
+
return value.get(key);
|
|
42
63
|
return value[key];
|
|
43
64
|
}, target);
|
|
44
65
|
if (!parent || typeof parent !== 'object')
|
|
45
66
|
return;
|
|
46
|
-
|
|
67
|
+
if (parent instanceof Map || isObservableMap(parent))
|
|
68
|
+
parent.delete(lastKey);
|
|
69
|
+
else
|
|
70
|
+
delete parent[lastKey];
|
|
71
|
+
};
|
|
72
|
+
/** Compares form values while preserving Maps, Sets, Dates, and RegExps. */
|
|
73
|
+
export const isEqual = (a, b) => deepEqual(toJS(a), toJS(b), new WeakMap());
|
|
74
|
+
/** Returns leaf paths whose current values differ from their defaults. */
|
|
75
|
+
export const collectDirtyPaths = (values, defaultValues) => {
|
|
76
|
+
return collectDifferencePaths(values, defaultValues);
|
|
77
|
+
};
|
|
78
|
+
/** Returns leaf paths changed between two value snapshots. */
|
|
79
|
+
export const collectChangedPaths = (before, after) => {
|
|
80
|
+
return collectDifferencePaths(before, after);
|
|
81
|
+
};
|
|
82
|
+
const collectDifferencePaths = (values, defaultValues) => {
|
|
83
|
+
const paths = [];
|
|
84
|
+
const visit = (value, defaultValue, path) => {
|
|
85
|
+
if (isEqual(value, defaultValue))
|
|
86
|
+
return;
|
|
87
|
+
if (value instanceof Map || defaultValue instanceof Map) {
|
|
88
|
+
if (!(value instanceof Map) || !(defaultValue instanceof Map) || !hasStringKeys(value) || !hasStringKeys(defaultValue)) {
|
|
89
|
+
if (path)
|
|
90
|
+
paths.push(path);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const keys = new Set([...value.keys(), ...defaultValue.keys()]);
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
if (!isUnsafeProperty(key))
|
|
96
|
+
visit(value.get(key), defaultValue.get(key), path ? `${path}.${key}` : key);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value) || Array.isArray(defaultValue)) {
|
|
101
|
+
const current = Array.isArray(value) ? value : [];
|
|
102
|
+
const defaults = Array.isArray(defaultValue) ? defaultValue : [];
|
|
103
|
+
const length = Math.max(current.length, defaults.length);
|
|
104
|
+
for (let index = 0; index < length; index += 1)
|
|
105
|
+
visit(current[index], defaults[index], path ? `${path}.${index}` : String(index));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isRecord(value) || isRecord(defaultValue)) {
|
|
109
|
+
const current = isRecord(value) ? value : {};
|
|
110
|
+
const defaults = isRecord(defaultValue) ? defaultValue : {};
|
|
111
|
+
const keys = new Set([...Object.keys(current), ...Object.keys(defaults)]);
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
if (!isUnsafeProperty(key))
|
|
114
|
+
visit(current[key], defaults[key], path ? `${path}.${key}` : key);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (isAtomicValue(value) || isAtomicValue(defaultValue)) {
|
|
119
|
+
if (path)
|
|
120
|
+
paths.push(path);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (path)
|
|
124
|
+
paths.push(path);
|
|
125
|
+
};
|
|
126
|
+
visit(toJS(values), toJS(defaultValues), '');
|
|
127
|
+
return paths;
|
|
128
|
+
};
|
|
129
|
+
const isAtomicValue = (value) => value === null || value === undefined || typeof value !== 'object' || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set;
|
|
130
|
+
const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value) && !isAtomicValue(value);
|
|
131
|
+
const hasStringKeys = (value) => [...value.keys()].every((key) => typeof key === 'string');
|
|
132
|
+
const deepEqual = (a, b, seen) => {
|
|
133
|
+
if (Object.is(a, b))
|
|
134
|
+
return true;
|
|
135
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
|
|
136
|
+
return false;
|
|
137
|
+
const left = a;
|
|
138
|
+
const right = b;
|
|
139
|
+
if (seen.get(left) === right)
|
|
140
|
+
return true;
|
|
141
|
+
seen.set(left, right);
|
|
142
|
+
if (a instanceof Date || b instanceof Date)
|
|
143
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
144
|
+
if (a instanceof RegExp || b instanceof RegExp)
|
|
145
|
+
return a instanceof RegExp && b instanceof RegExp && a.source === b.source && a.flags === b.flags;
|
|
146
|
+
if (a instanceof Map || b instanceof Map) {
|
|
147
|
+
if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size)
|
|
148
|
+
return false;
|
|
149
|
+
const unmatched = [...b.entries()];
|
|
150
|
+
return [...a.entries()].every(([key, value]) => {
|
|
151
|
+
const index = unmatched.findIndex(([otherKey, otherValue]) => deepEqual(key, otherKey, seen) && deepEqual(value, otherValue, seen));
|
|
152
|
+
if (index < 0)
|
|
153
|
+
return false;
|
|
154
|
+
unmatched.splice(index, 1);
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (a instanceof Set || b instanceof Set) {
|
|
159
|
+
if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size)
|
|
160
|
+
return false;
|
|
161
|
+
const unmatched = [...b];
|
|
162
|
+
return [...a].every((value) => {
|
|
163
|
+
const index = unmatched.findIndex((other) => deepEqual(value, other, seen));
|
|
164
|
+
if (index < 0)
|
|
165
|
+
return false;
|
|
166
|
+
unmatched.splice(index, 1);
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
171
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => deepEqual(value, b[index], seen));
|
|
172
|
+
}
|
|
173
|
+
const leftKeys = Reflect.ownKeys(a);
|
|
174
|
+
const rightKeys = Reflect.ownKeys(b);
|
|
175
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => (rightKeys.includes(key) && deepEqual(a[key], b[key], seen)));
|
|
176
|
+
};
|
|
177
|
+
/** Returns the field error stored at the path in a nested errors object. */
|
|
178
|
+
export const findErrorAtPath = (errors, path) => {
|
|
179
|
+
const value = getAtPath(errors, path);
|
|
180
|
+
return value && typeof value === 'object' && 'type' in value ? value : undefined;
|
|
181
|
+
};
|
|
182
|
+
/** Collects the flat paths of all field errors in a nested errors object. */
|
|
183
|
+
export const collectErrorPaths = (errors, base = '') => {
|
|
184
|
+
const paths = [];
|
|
185
|
+
for (const [key, value] of Object.entries(errors)) {
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
const path = base ? `${base}.${key}` : key;
|
|
189
|
+
if (value && typeof value === 'object' && 'type' in value)
|
|
190
|
+
paths.push(path);
|
|
191
|
+
if (value && typeof value === 'object')
|
|
192
|
+
paths.push(...collectErrorPaths(value, path));
|
|
193
|
+
}
|
|
194
|
+
return paths;
|
|
47
195
|
};
|
|
48
|
-
export const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
49
196
|
export const extractValue = (eventOrValue) => {
|
|
50
197
|
if (!eventOrValue || typeof eventOrValue !== 'object' || !('target' in eventOrValue))
|
|
51
198
|
return eventOrValue;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Ref } from 'yummies/mobx';
|
|
2
|
+
import type { FieldError, FieldErrors, FieldValues, FormOptions, RegisterOptions } from './types.js';
|
|
3
|
+
/** Form-side collaborators the validator needs, kept as callbacks so Form internals stay private. */
|
|
4
|
+
export interface ValidatorHost<T extends FieldValues> {
|
|
5
|
+
/** Current form options: schema, resolver, criteriaMode, delayError, and native validation settings. */
|
|
6
|
+
readonly options: FormOptions<T>;
|
|
7
|
+
/** Registration options per field path. */
|
|
8
|
+
readonly fieldOptions: Map<string, RegisterOptions<T>>;
|
|
9
|
+
/** Registered field refs used for native validity reporting. */
|
|
10
|
+
readonly refs: Map<string, Ref<HTMLElement | null>>;
|
|
11
|
+
/** Current value stored at the field path. */
|
|
12
|
+
valueAt(path: string): unknown;
|
|
13
|
+
/** Plain copy of the current values for schema and resolver runs. */
|
|
14
|
+
snapshot(): T;
|
|
15
|
+
/** Applies a validated error to the form state. */
|
|
16
|
+
applyError(path: string, error: FieldError | undefined): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Schema, resolver, and rule validation with error delivery for Form.
|
|
20
|
+
*
|
|
21
|
+
* Runs Zod-like, Valibot, and Standard Schema contracts or a custom resolver,
|
|
22
|
+
* checks registration rules with firstError or all criteria modes, relays
|
|
23
|
+
* messages to native DOM validity, and can delay error display.
|
|
24
|
+
*/
|
|
25
|
+
export declare class FormValidator<T extends FieldValues> {
|
|
26
|
+
private readonly host;
|
|
27
|
+
private readonly delayedErrorTimers;
|
|
28
|
+
private readonly delayedErrors;
|
|
29
|
+
constructor(host: ValidatorHost<T>);
|
|
30
|
+
/** Validates values with the resolver or schema and returns nested errors and resolver output. */
|
|
31
|
+
resolveSchemaErrors(paths?: string[]): Promise<{
|
|
32
|
+
errors: FieldErrors<T>;
|
|
33
|
+
values?: T;
|
|
34
|
+
}>;
|
|
35
|
+
/** Checks registration rules for one field and returns the failing error if any. */
|
|
36
|
+
validateRules(path: string): Promise<FieldError | undefined>;
|
|
37
|
+
/** Groups rule failures into the types map; repeated messages of one rule become arrays. */
|
|
38
|
+
private collectRuleTypes;
|
|
39
|
+
/** Combines schema and rule errors for a field according to the criteria mode. */
|
|
40
|
+
mergeErrors(schemaError: FieldError | undefined, ruleError: FieldError | undefined): FieldError | undefined;
|
|
41
|
+
/** Delivers a validated error, respecting the delay option and native validity reporting. */
|
|
42
|
+
applyValidationError(path: string, error: FieldError | undefined): void;
|
|
43
|
+
/** Applies a pending delayed error immediately, for example on blur. */
|
|
44
|
+
flushDelayed(path: string): void;
|
|
45
|
+
/** Drops a pending delayed error without applying it. */
|
|
46
|
+
cancelDelayed(path: string): void;
|
|
47
|
+
/** Drops all pending delayed errors without applying them. */
|
|
48
|
+
cancelAllDelayed(): void;
|
|
49
|
+
private resolverResultValues;
|
|
50
|
+
private normalizeSchemaErrors;
|
|
51
|
+
private applyNativeValidity;
|
|
52
|
+
}
|