mobx-formly 0.0.3 → 0.1.1

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/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { Form } from './form.js';
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 { Form } from './form.js';
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,134 @@
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 = observable.map();
13
+ children = observable.map({}, { deep: false });
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
+ siblings = observable.set();
129
+ this.children.set(parent, siblings);
130
+ }
131
+ siblings.add(parts[index - 1]);
132
+ }
133
+ }
134
+ }
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>]: T[K] extends readonly unknown[] ? T[K] extends readonly (infer I)[] ? I extends object ? K | `${K}.${number}` | `${K}.${number}.${FieldPath<I>}` : K | `${K}.${number}` : K : T[K] extends object ? K | `${K}.${FieldPath<T[K]>}` : K;
67
+ [K in Extract<keyof T, string>]: FieldPathForValue<K, T[K]>;
46
68
  }[Extract<keyof T, string>];
47
- 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> : never : P extends keyof T ? T[P] : T extends readonly (infer I)[] ? P extends `${number}` ? I : never : never;
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]?: T[K] extends readonly (infer I)[] ? FieldError & Array<I extends object ? FieldErrors<I> : FieldError | undefined> : T[K] extends object ? FieldError & FieldErrors<T[K]> : FieldError;
86
+ [K in keyof T]?: FieldErrorTree<T[K]>;
54
87
  } & {
55
- root?: FieldError;
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]?: T[K] extends readonly (infer I)[] ? FieldState & Array<I extends object ? FieldStateTree<I> : FieldState | undefined> : T[K] extends object ? FieldState & FieldStateTree<T[K]> : FieldState;
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?: (value: unknown, values: T) => boolean | string | Promise<boolean | string>;
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
- mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'all';
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
- return structuredClone(value);
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
- // MobX observable proxies cannot be structured-cloned directly.
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
- if (!next || typeof next !== 'object')
27
- current[key] = /^\d+$/.test(nextKey) ? [] : {};
28
- current = current[key];
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 && !isUnsafeProperty(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
- delete parent[lastKey];
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
+ }