@neutro/form 0.0.4 → 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/adapters/angular.cjs +24 -6
- package/dist/adapters/angular.d.cts +171 -6
- package/dist/adapters/angular.d.ts +171 -6
- package/dist/adapters/angular.js +25 -7
- package/dist/adapters/react.cjs +5 -1
- package/dist/adapters/react.d.cts +139 -1
- package/dist/adapters/react.d.ts +139 -1
- package/dist/adapters/react.js +6 -2
- package/dist/adapters/solid.cjs +12 -3
- package/dist/adapters/solid.d.cts +171 -1
- package/dist/adapters/solid.d.ts +171 -1
- package/dist/adapters/solid.js +12 -3
- package/dist/adapters/svelte.cjs +18 -9
- package/dist/adapters/svelte.d.cts +171 -1
- package/dist/adapters/svelte.d.ts +171 -1
- package/dist/adapters/svelte.js +18 -9
- package/dist/adapters/vue.cjs +15 -4
- package/dist/adapters/vue.d.cts +176 -1
- package/dist/adapters/vue.d.ts +176 -1
- package/dist/adapters/vue.js +23 -5
- package/dist/chunk-FDAQJJJ7.js +657 -0
- package/dist/core.cjs +510 -425
- package/dist/core.d.cts +248 -1
- package/dist/core.d.ts +248 -1
- package/dist/core.js +24 -569
- package/dist/devtools.cjs +244 -0
- package/dist/devtools.d.cts +141 -0
- package/dist/devtools.d.ts +141 -0
- package/dist/devtools.js +217 -0
- package/dist/testing.cjs +667 -0
- package/dist/testing.d.cts +254 -0
- package/dist/testing.d.ts +254 -0
- package/dist/testing.js +39 -0
- package/package.json +25 -7
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @neutro/form-core
|
|
3
|
+
* High-Performance, Zero-Dependency, Framework-Agnostic Reactive Form Engine.
|
|
4
|
+
*/
|
|
5
|
+
type Primitive = string | number | boolean | null | undefined | Date | File;
|
|
6
|
+
type Prev = [never, 0, 1, 2, 3, 4, 5, ...any[]];
|
|
7
|
+
type PathImpl<T, K extends keyof T, Depth extends number = 5> = [Depth] extends [never] ? never : K extends string ? T[K] extends Primitive ? K : T[K] extends Array<infer U> ? K | `${K}.${number}` | (U extends object ? `${K}.${number}.${PathImpl<U, keyof U, Prev[Depth]>}` : never) : NonNullable<T[K]> extends object ? K | `${K}.${PathImpl<NonNullable<T[K]>, keyof NonNullable<T[K]>, Prev[Depth]>}` : K : never;
|
|
8
|
+
type Path<T> = PathImpl<T, keyof T> & string;
|
|
9
|
+
type _GetPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? _GetPathValue<NonNullable<T[K]>, Rest> : T extends readonly any[] ? _GetPathValue<NonNullable<T[number]>, Rest> : unknown : P extends keyof T ? T[P] : T extends readonly any[] ? T[number] : unknown;
|
|
10
|
+
type GetPathValue<T, P extends string> = _GetPathValue<T, P>;
|
|
11
|
+
interface FormState<T> {
|
|
12
|
+
values: T;
|
|
13
|
+
errors: Record<string, string>;
|
|
14
|
+
touched: Record<string, boolean>;
|
|
15
|
+
dirty: Record<string, boolean>;
|
|
16
|
+
isSubmitting: boolean;
|
|
17
|
+
isValidating: boolean;
|
|
18
|
+
}
|
|
19
|
+
type FormSubscriber<T> = (state: FormState<T>) => void;
|
|
20
|
+
type PathSubscriber<V = any> = (value: V, fieldState: {
|
|
21
|
+
error?: string;
|
|
22
|
+
touched?: boolean;
|
|
23
|
+
dirty?: boolean;
|
|
24
|
+
}) => void;
|
|
25
|
+
type BuiltInRule = 'required' | 'accepted' | 'email' | 'url' | 'numeric' | 'integer' | 'positive' | 'nonNegative' | 'alpha' | 'alphanumeric' | 'date' | {
|
|
26
|
+
minLength: number;
|
|
27
|
+
message?: string;
|
|
28
|
+
} | {
|
|
29
|
+
maxLength: number;
|
|
30
|
+
message?: string;
|
|
31
|
+
} | {
|
|
32
|
+
min: number;
|
|
33
|
+
message?: string;
|
|
34
|
+
} | {
|
|
35
|
+
max: number;
|
|
36
|
+
message?: string;
|
|
37
|
+
} | {
|
|
38
|
+
startsWith: string;
|
|
39
|
+
message?: string;
|
|
40
|
+
} | {
|
|
41
|
+
endsWith: string;
|
|
42
|
+
message?: string;
|
|
43
|
+
} | {
|
|
44
|
+
includes: string;
|
|
45
|
+
message?: string;
|
|
46
|
+
} | {
|
|
47
|
+
pattern: string | RegExp;
|
|
48
|
+
message?: string;
|
|
49
|
+
} | {
|
|
50
|
+
minItems: number;
|
|
51
|
+
message?: string;
|
|
52
|
+
} | {
|
|
53
|
+
maxItems: number;
|
|
54
|
+
message?: string;
|
|
55
|
+
} | 'unique' | {
|
|
56
|
+
contains: unknown;
|
|
57
|
+
message?: string;
|
|
58
|
+
} | {
|
|
59
|
+
oneOf: unknown[];
|
|
60
|
+
message?: string;
|
|
61
|
+
} | {
|
|
62
|
+
notOneOf: unknown[];
|
|
63
|
+
message?: string;
|
|
64
|
+
} | {
|
|
65
|
+
matches: string;
|
|
66
|
+
message?: string;
|
|
67
|
+
} | {
|
|
68
|
+
doesNotMatch: string;
|
|
69
|
+
message?: string;
|
|
70
|
+
} | {
|
|
71
|
+
greaterThan: string;
|
|
72
|
+
message?: string;
|
|
73
|
+
} | {
|
|
74
|
+
lessThan: string;
|
|
75
|
+
message?: string;
|
|
76
|
+
} | {
|
|
77
|
+
after: string;
|
|
78
|
+
message?: string;
|
|
79
|
+
} | {
|
|
80
|
+
before: string;
|
|
81
|
+
message?: string;
|
|
82
|
+
} | {
|
|
83
|
+
requiredIf: string;
|
|
84
|
+
message?: string;
|
|
85
|
+
} | {
|
|
86
|
+
requiredUnless: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
};
|
|
89
|
+
type ValidationMode = 'onChange' | 'onBlur' | 'onTouched' | 'onSubmitOnly';
|
|
90
|
+
interface ValidationModeConfig<T extends object> {
|
|
91
|
+
default?: ValidationMode;
|
|
92
|
+
fields?: Partial<Record<Path<T> | (string & {}), ValidationMode>>;
|
|
93
|
+
}
|
|
94
|
+
type FormAction = {
|
|
95
|
+
type: 'SET';
|
|
96
|
+
path: string;
|
|
97
|
+
value: unknown;
|
|
98
|
+
options?: {
|
|
99
|
+
touch?: boolean;
|
|
100
|
+
validate?: boolean;
|
|
101
|
+
};
|
|
102
|
+
} | {
|
|
103
|
+
type: 'VALIDATE';
|
|
104
|
+
paths?: string[];
|
|
105
|
+
} | {
|
|
106
|
+
type: 'SUBMIT';
|
|
107
|
+
} | {
|
|
108
|
+
type: 'RESET';
|
|
109
|
+
newValues?: unknown;
|
|
110
|
+
} | {
|
|
111
|
+
type: 'SET_ERRORS';
|
|
112
|
+
errors: Record<string, string>;
|
|
113
|
+
} | {
|
|
114
|
+
type: 'CONNECT';
|
|
115
|
+
path: string;
|
|
116
|
+
} | {
|
|
117
|
+
type: 'DISCONNECT';
|
|
118
|
+
path: string;
|
|
119
|
+
} | {
|
|
120
|
+
type: 'BLUR';
|
|
121
|
+
path: string;
|
|
122
|
+
} | {
|
|
123
|
+
type: 'BATCH_START';
|
|
124
|
+
} | {
|
|
125
|
+
type: 'BATCH_END';
|
|
126
|
+
} | {
|
|
127
|
+
type: 'ARRAY_APPEND';
|
|
128
|
+
path: string;
|
|
129
|
+
item: unknown;
|
|
130
|
+
} | {
|
|
131
|
+
type: 'ARRAY_INSERT';
|
|
132
|
+
path: string;
|
|
133
|
+
index: number;
|
|
134
|
+
item: unknown;
|
|
135
|
+
} | {
|
|
136
|
+
type: 'ARRAY_REMOVE';
|
|
137
|
+
path: string;
|
|
138
|
+
index: number;
|
|
139
|
+
} | {
|
|
140
|
+
type: 'ARRAY_MOVE';
|
|
141
|
+
path: string;
|
|
142
|
+
from: number;
|
|
143
|
+
to: number;
|
|
144
|
+
} | {
|
|
145
|
+
type: 'ARRAY_SWAP';
|
|
146
|
+
path: string;
|
|
147
|
+
i: number;
|
|
148
|
+
j: number;
|
|
149
|
+
} | {
|
|
150
|
+
type: 'CLEAR_ERRORS';
|
|
151
|
+
};
|
|
152
|
+
interface AriaPropsOptions {
|
|
153
|
+
required?: boolean;
|
|
154
|
+
errorId?: string;
|
|
155
|
+
}
|
|
156
|
+
interface AriaProps {
|
|
157
|
+
'aria-invalid': 'true' | 'false';
|
|
158
|
+
'aria-describedby': string | undefined;
|
|
159
|
+
'aria-required': true | undefined;
|
|
160
|
+
}
|
|
161
|
+
interface FormConfig<T extends object> {
|
|
162
|
+
initialValues: T;
|
|
163
|
+
rules?: Partial<Record<Path<T> | (string & {}), BuiltInRule | BuiltInRule[]>>;
|
|
164
|
+
validator?: (values: T, scopePaths?: string[], signal?: AbortSignal) => Record<string, string> | Promise<Record<string, string>>;
|
|
165
|
+
dependencies?: Record<string, string[]>;
|
|
166
|
+
asyncDebounceMs?: number;
|
|
167
|
+
/** Per-field validation trigger mode. Defaults to 'onTouched'. */
|
|
168
|
+
validationMode?: ValidationMode | ValidationModeConfig<T>;
|
|
169
|
+
}
|
|
170
|
+
interface ConnectOptions {
|
|
171
|
+
persist?: boolean;
|
|
172
|
+
format?: (val: string) => string;
|
|
173
|
+
validateOn?: ValidationMode;
|
|
174
|
+
}
|
|
175
|
+
interface FormInstance<T extends object> {
|
|
176
|
+
subscribe: (fn: FormSubscriber<T>) => () => void;
|
|
177
|
+
subscribeToPath<P extends Path<T>>(path: P, fn: PathSubscriber<GetPathValue<T, P>>): () => void;
|
|
178
|
+
subscribeToPath(path: string, fn: PathSubscriber): () => void;
|
|
179
|
+
get<P extends Path<T>>(path: P): GetPathValue<T, P>;
|
|
180
|
+
get(path: string | string[]): any;
|
|
181
|
+
set: (path: Path<T> | string | string[], val: any, options?: {
|
|
182
|
+
touch?: boolean;
|
|
183
|
+
validate?: boolean;
|
|
184
|
+
}) => void;
|
|
185
|
+
validate: (scopePaths?: Path<T>[] | string[] | string[][]) => Promise<boolean>;
|
|
186
|
+
connect: (path: Path<T> | string, el: HTMLElement, options?: ConnectOptions) => () => void;
|
|
187
|
+
submit: (onValid: (payload: Partial<T>) => void | Promise<void>) => Promise<boolean>;
|
|
188
|
+
handleSubmit: (onValid: (payload: Partial<T>) => void | Promise<void>, onInvalid?: (errors: Record<string, string>) => void) => (e?: Event) => void;
|
|
189
|
+
getState: () => FormState<T>;
|
|
190
|
+
getPayload: () => Partial<T>;
|
|
191
|
+
getAriaProps: (path: Path<T> | string, options?: AriaPropsOptions) => AriaProps;
|
|
192
|
+
batch: (fn: () => void) => void;
|
|
193
|
+
arrayAppend: (path: Path<T> | string | string[], item: any) => void;
|
|
194
|
+
arrayInsert: (path: Path<T> | string | string[], index: number, item: any) => void;
|
|
195
|
+
arrayRemove: (path: Path<T> | string | string[], index: number) => void;
|
|
196
|
+
arrayMove: (path: Path<T> | string | string[], fromIndex: number, toIndex: number) => void;
|
|
197
|
+
arraySwap: (path: Path<T> | string | string[], indexA: number, indexB: number) => void;
|
|
198
|
+
reset: (newValues?: T) => void;
|
|
199
|
+
getConnectedCount: () => number;
|
|
200
|
+
destroy: () => void;
|
|
201
|
+
setErrors: (errors: Record<Path<T> | (string & {}), string>) => void;
|
|
202
|
+
clearErrors: () => void;
|
|
203
|
+
/**
|
|
204
|
+
* Returns the effective ValidationMode for a field. Useful for debugging
|
|
205
|
+
* validation timing; framework adapters should rely on this only in custom
|
|
206
|
+
* event handlers, not in render logic.
|
|
207
|
+
*/
|
|
208
|
+
getFieldMode: (path: string) => ValidationMode;
|
|
209
|
+
_subscribeToActions: (fn: (action: FormAction, state: FormState<T>) => void) => () => void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Batch-sets multiple field values in one notification flush. */
|
|
213
|
+
declare function fillForm<T extends object>(form: FormInstance<T>, values: Partial<Record<string, unknown>>): void;
|
|
214
|
+
/**
|
|
215
|
+
* Marks a field as touched without changing its value.
|
|
216
|
+
*
|
|
217
|
+
* This simulates a user visiting and leaving a field. It does NOT trigger
|
|
218
|
+
* validation automatically — call triggerValidation() (or fixture.validate())
|
|
219
|
+
* afterwards to assert the resulting errors.
|
|
220
|
+
*
|
|
221
|
+
* Implementation note: form.set() is a no-op when the value is unchanged, so a
|
|
222
|
+
* unique sentinel object is used to force the touched flag, then the original
|
|
223
|
+
* value is restored. Both calls are wrapped in batch() so subscribers see only
|
|
224
|
+
* the final state.
|
|
225
|
+
*/
|
|
226
|
+
declare function blurField<T extends object>(form: FormInstance<T>, path: string): void;
|
|
227
|
+
/**
|
|
228
|
+
* Runs form validation and returns whether the form is valid.
|
|
229
|
+
*
|
|
230
|
+
* Thin wrapper around form.validate() for discoverability. When using the
|
|
231
|
+
* standalone function (not createFormFixture), set asyncDebounceMs: 0 on the
|
|
232
|
+
* form to prevent tests from timing out on async validators.
|
|
233
|
+
*/
|
|
234
|
+
declare function triggerValidation<T extends object>(form: FormInstance<T>, paths?: string[]): Promise<boolean>;
|
|
235
|
+
interface FormFixture<T extends object> {
|
|
236
|
+
form: FormInstance<T>;
|
|
237
|
+
fill(values: Partial<Record<string, unknown>>): void;
|
|
238
|
+
blur(path: string): void;
|
|
239
|
+
validate(paths?: string[]): Promise<boolean>;
|
|
240
|
+
cleanup(): void;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Creates a test fixture wrapping a form instance.
|
|
244
|
+
*
|
|
245
|
+
* Defaults asyncDebounceMs to 0 so async validators resolve in tests without
|
|
246
|
+
* fake timers. Pass asyncDebounceMs explicitly to override.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const fixture = createFormFixture({ initialValues: { email: '' }, rules: { email: ['required'] } });
|
|
250
|
+
* afterEach(() => fixture.cleanup());
|
|
251
|
+
*/
|
|
252
|
+
declare function createFormFixture<T extends object>(config: FormConfig<T>): FormFixture<T>;
|
|
253
|
+
|
|
254
|
+
export { type FormFixture, blurField, createFormFixture, fillForm, triggerValidation };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @neutro/form-core
|
|
3
|
+
* High-Performance, Zero-Dependency, Framework-Agnostic Reactive Form Engine.
|
|
4
|
+
*/
|
|
5
|
+
type Primitive = string | number | boolean | null | undefined | Date | File;
|
|
6
|
+
type Prev = [never, 0, 1, 2, 3, 4, 5, ...any[]];
|
|
7
|
+
type PathImpl<T, K extends keyof T, Depth extends number = 5> = [Depth] extends [never] ? never : K extends string ? T[K] extends Primitive ? K : T[K] extends Array<infer U> ? K | `${K}.${number}` | (U extends object ? `${K}.${number}.${PathImpl<U, keyof U, Prev[Depth]>}` : never) : NonNullable<T[K]> extends object ? K | `${K}.${PathImpl<NonNullable<T[K]>, keyof NonNullable<T[K]>, Prev[Depth]>}` : K : never;
|
|
8
|
+
type Path<T> = PathImpl<T, keyof T> & string;
|
|
9
|
+
type _GetPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? _GetPathValue<NonNullable<T[K]>, Rest> : T extends readonly any[] ? _GetPathValue<NonNullable<T[number]>, Rest> : unknown : P extends keyof T ? T[P] : T extends readonly any[] ? T[number] : unknown;
|
|
10
|
+
type GetPathValue<T, P extends string> = _GetPathValue<T, P>;
|
|
11
|
+
interface FormState<T> {
|
|
12
|
+
values: T;
|
|
13
|
+
errors: Record<string, string>;
|
|
14
|
+
touched: Record<string, boolean>;
|
|
15
|
+
dirty: Record<string, boolean>;
|
|
16
|
+
isSubmitting: boolean;
|
|
17
|
+
isValidating: boolean;
|
|
18
|
+
}
|
|
19
|
+
type FormSubscriber<T> = (state: FormState<T>) => void;
|
|
20
|
+
type PathSubscriber<V = any> = (value: V, fieldState: {
|
|
21
|
+
error?: string;
|
|
22
|
+
touched?: boolean;
|
|
23
|
+
dirty?: boolean;
|
|
24
|
+
}) => void;
|
|
25
|
+
type BuiltInRule = 'required' | 'accepted' | 'email' | 'url' | 'numeric' | 'integer' | 'positive' | 'nonNegative' | 'alpha' | 'alphanumeric' | 'date' | {
|
|
26
|
+
minLength: number;
|
|
27
|
+
message?: string;
|
|
28
|
+
} | {
|
|
29
|
+
maxLength: number;
|
|
30
|
+
message?: string;
|
|
31
|
+
} | {
|
|
32
|
+
min: number;
|
|
33
|
+
message?: string;
|
|
34
|
+
} | {
|
|
35
|
+
max: number;
|
|
36
|
+
message?: string;
|
|
37
|
+
} | {
|
|
38
|
+
startsWith: string;
|
|
39
|
+
message?: string;
|
|
40
|
+
} | {
|
|
41
|
+
endsWith: string;
|
|
42
|
+
message?: string;
|
|
43
|
+
} | {
|
|
44
|
+
includes: string;
|
|
45
|
+
message?: string;
|
|
46
|
+
} | {
|
|
47
|
+
pattern: string | RegExp;
|
|
48
|
+
message?: string;
|
|
49
|
+
} | {
|
|
50
|
+
minItems: number;
|
|
51
|
+
message?: string;
|
|
52
|
+
} | {
|
|
53
|
+
maxItems: number;
|
|
54
|
+
message?: string;
|
|
55
|
+
} | 'unique' | {
|
|
56
|
+
contains: unknown;
|
|
57
|
+
message?: string;
|
|
58
|
+
} | {
|
|
59
|
+
oneOf: unknown[];
|
|
60
|
+
message?: string;
|
|
61
|
+
} | {
|
|
62
|
+
notOneOf: unknown[];
|
|
63
|
+
message?: string;
|
|
64
|
+
} | {
|
|
65
|
+
matches: string;
|
|
66
|
+
message?: string;
|
|
67
|
+
} | {
|
|
68
|
+
doesNotMatch: string;
|
|
69
|
+
message?: string;
|
|
70
|
+
} | {
|
|
71
|
+
greaterThan: string;
|
|
72
|
+
message?: string;
|
|
73
|
+
} | {
|
|
74
|
+
lessThan: string;
|
|
75
|
+
message?: string;
|
|
76
|
+
} | {
|
|
77
|
+
after: string;
|
|
78
|
+
message?: string;
|
|
79
|
+
} | {
|
|
80
|
+
before: string;
|
|
81
|
+
message?: string;
|
|
82
|
+
} | {
|
|
83
|
+
requiredIf: string;
|
|
84
|
+
message?: string;
|
|
85
|
+
} | {
|
|
86
|
+
requiredUnless: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
};
|
|
89
|
+
type ValidationMode = 'onChange' | 'onBlur' | 'onTouched' | 'onSubmitOnly';
|
|
90
|
+
interface ValidationModeConfig<T extends object> {
|
|
91
|
+
default?: ValidationMode;
|
|
92
|
+
fields?: Partial<Record<Path<T> | (string & {}), ValidationMode>>;
|
|
93
|
+
}
|
|
94
|
+
type FormAction = {
|
|
95
|
+
type: 'SET';
|
|
96
|
+
path: string;
|
|
97
|
+
value: unknown;
|
|
98
|
+
options?: {
|
|
99
|
+
touch?: boolean;
|
|
100
|
+
validate?: boolean;
|
|
101
|
+
};
|
|
102
|
+
} | {
|
|
103
|
+
type: 'VALIDATE';
|
|
104
|
+
paths?: string[];
|
|
105
|
+
} | {
|
|
106
|
+
type: 'SUBMIT';
|
|
107
|
+
} | {
|
|
108
|
+
type: 'RESET';
|
|
109
|
+
newValues?: unknown;
|
|
110
|
+
} | {
|
|
111
|
+
type: 'SET_ERRORS';
|
|
112
|
+
errors: Record<string, string>;
|
|
113
|
+
} | {
|
|
114
|
+
type: 'CONNECT';
|
|
115
|
+
path: string;
|
|
116
|
+
} | {
|
|
117
|
+
type: 'DISCONNECT';
|
|
118
|
+
path: string;
|
|
119
|
+
} | {
|
|
120
|
+
type: 'BLUR';
|
|
121
|
+
path: string;
|
|
122
|
+
} | {
|
|
123
|
+
type: 'BATCH_START';
|
|
124
|
+
} | {
|
|
125
|
+
type: 'BATCH_END';
|
|
126
|
+
} | {
|
|
127
|
+
type: 'ARRAY_APPEND';
|
|
128
|
+
path: string;
|
|
129
|
+
item: unknown;
|
|
130
|
+
} | {
|
|
131
|
+
type: 'ARRAY_INSERT';
|
|
132
|
+
path: string;
|
|
133
|
+
index: number;
|
|
134
|
+
item: unknown;
|
|
135
|
+
} | {
|
|
136
|
+
type: 'ARRAY_REMOVE';
|
|
137
|
+
path: string;
|
|
138
|
+
index: number;
|
|
139
|
+
} | {
|
|
140
|
+
type: 'ARRAY_MOVE';
|
|
141
|
+
path: string;
|
|
142
|
+
from: number;
|
|
143
|
+
to: number;
|
|
144
|
+
} | {
|
|
145
|
+
type: 'ARRAY_SWAP';
|
|
146
|
+
path: string;
|
|
147
|
+
i: number;
|
|
148
|
+
j: number;
|
|
149
|
+
} | {
|
|
150
|
+
type: 'CLEAR_ERRORS';
|
|
151
|
+
};
|
|
152
|
+
interface AriaPropsOptions {
|
|
153
|
+
required?: boolean;
|
|
154
|
+
errorId?: string;
|
|
155
|
+
}
|
|
156
|
+
interface AriaProps {
|
|
157
|
+
'aria-invalid': 'true' | 'false';
|
|
158
|
+
'aria-describedby': string | undefined;
|
|
159
|
+
'aria-required': true | undefined;
|
|
160
|
+
}
|
|
161
|
+
interface FormConfig<T extends object> {
|
|
162
|
+
initialValues: T;
|
|
163
|
+
rules?: Partial<Record<Path<T> | (string & {}), BuiltInRule | BuiltInRule[]>>;
|
|
164
|
+
validator?: (values: T, scopePaths?: string[], signal?: AbortSignal) => Record<string, string> | Promise<Record<string, string>>;
|
|
165
|
+
dependencies?: Record<string, string[]>;
|
|
166
|
+
asyncDebounceMs?: number;
|
|
167
|
+
/** Per-field validation trigger mode. Defaults to 'onTouched'. */
|
|
168
|
+
validationMode?: ValidationMode | ValidationModeConfig<T>;
|
|
169
|
+
}
|
|
170
|
+
interface ConnectOptions {
|
|
171
|
+
persist?: boolean;
|
|
172
|
+
format?: (val: string) => string;
|
|
173
|
+
validateOn?: ValidationMode;
|
|
174
|
+
}
|
|
175
|
+
interface FormInstance<T extends object> {
|
|
176
|
+
subscribe: (fn: FormSubscriber<T>) => () => void;
|
|
177
|
+
subscribeToPath<P extends Path<T>>(path: P, fn: PathSubscriber<GetPathValue<T, P>>): () => void;
|
|
178
|
+
subscribeToPath(path: string, fn: PathSubscriber): () => void;
|
|
179
|
+
get<P extends Path<T>>(path: P): GetPathValue<T, P>;
|
|
180
|
+
get(path: string | string[]): any;
|
|
181
|
+
set: (path: Path<T> | string | string[], val: any, options?: {
|
|
182
|
+
touch?: boolean;
|
|
183
|
+
validate?: boolean;
|
|
184
|
+
}) => void;
|
|
185
|
+
validate: (scopePaths?: Path<T>[] | string[] | string[][]) => Promise<boolean>;
|
|
186
|
+
connect: (path: Path<T> | string, el: HTMLElement, options?: ConnectOptions) => () => void;
|
|
187
|
+
submit: (onValid: (payload: Partial<T>) => void | Promise<void>) => Promise<boolean>;
|
|
188
|
+
handleSubmit: (onValid: (payload: Partial<T>) => void | Promise<void>, onInvalid?: (errors: Record<string, string>) => void) => (e?: Event) => void;
|
|
189
|
+
getState: () => FormState<T>;
|
|
190
|
+
getPayload: () => Partial<T>;
|
|
191
|
+
getAriaProps: (path: Path<T> | string, options?: AriaPropsOptions) => AriaProps;
|
|
192
|
+
batch: (fn: () => void) => void;
|
|
193
|
+
arrayAppend: (path: Path<T> | string | string[], item: any) => void;
|
|
194
|
+
arrayInsert: (path: Path<T> | string | string[], index: number, item: any) => void;
|
|
195
|
+
arrayRemove: (path: Path<T> | string | string[], index: number) => void;
|
|
196
|
+
arrayMove: (path: Path<T> | string | string[], fromIndex: number, toIndex: number) => void;
|
|
197
|
+
arraySwap: (path: Path<T> | string | string[], indexA: number, indexB: number) => void;
|
|
198
|
+
reset: (newValues?: T) => void;
|
|
199
|
+
getConnectedCount: () => number;
|
|
200
|
+
destroy: () => void;
|
|
201
|
+
setErrors: (errors: Record<Path<T> | (string & {}), string>) => void;
|
|
202
|
+
clearErrors: () => void;
|
|
203
|
+
/**
|
|
204
|
+
* Returns the effective ValidationMode for a field. Useful for debugging
|
|
205
|
+
* validation timing; framework adapters should rely on this only in custom
|
|
206
|
+
* event handlers, not in render logic.
|
|
207
|
+
*/
|
|
208
|
+
getFieldMode: (path: string) => ValidationMode;
|
|
209
|
+
_subscribeToActions: (fn: (action: FormAction, state: FormState<T>) => void) => () => void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Batch-sets multiple field values in one notification flush. */
|
|
213
|
+
declare function fillForm<T extends object>(form: FormInstance<T>, values: Partial<Record<string, unknown>>): void;
|
|
214
|
+
/**
|
|
215
|
+
* Marks a field as touched without changing its value.
|
|
216
|
+
*
|
|
217
|
+
* This simulates a user visiting and leaving a field. It does NOT trigger
|
|
218
|
+
* validation automatically — call triggerValidation() (or fixture.validate())
|
|
219
|
+
* afterwards to assert the resulting errors.
|
|
220
|
+
*
|
|
221
|
+
* Implementation note: form.set() is a no-op when the value is unchanged, so a
|
|
222
|
+
* unique sentinel object is used to force the touched flag, then the original
|
|
223
|
+
* value is restored. Both calls are wrapped in batch() so subscribers see only
|
|
224
|
+
* the final state.
|
|
225
|
+
*/
|
|
226
|
+
declare function blurField<T extends object>(form: FormInstance<T>, path: string): void;
|
|
227
|
+
/**
|
|
228
|
+
* Runs form validation and returns whether the form is valid.
|
|
229
|
+
*
|
|
230
|
+
* Thin wrapper around form.validate() for discoverability. When using the
|
|
231
|
+
* standalone function (not createFormFixture), set asyncDebounceMs: 0 on the
|
|
232
|
+
* form to prevent tests from timing out on async validators.
|
|
233
|
+
*/
|
|
234
|
+
declare function triggerValidation<T extends object>(form: FormInstance<T>, paths?: string[]): Promise<boolean>;
|
|
235
|
+
interface FormFixture<T extends object> {
|
|
236
|
+
form: FormInstance<T>;
|
|
237
|
+
fill(values: Partial<Record<string, unknown>>): void;
|
|
238
|
+
blur(path: string): void;
|
|
239
|
+
validate(paths?: string[]): Promise<boolean>;
|
|
240
|
+
cleanup(): void;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Creates a test fixture wrapping a form instance.
|
|
244
|
+
*
|
|
245
|
+
* Defaults asyncDebounceMs to 0 so async validators resolve in tests without
|
|
246
|
+
* fake timers. Pass asyncDebounceMs explicitly to override.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const fixture = createFormFixture({ initialValues: { email: '' }, rules: { email: ['required'] } });
|
|
250
|
+
* afterEach(() => fixture.cleanup());
|
|
251
|
+
*/
|
|
252
|
+
declare function createFormFixture<T extends object>(config: FormConfig<T>): FormFixture<T>;
|
|
253
|
+
|
|
254
|
+
export { type FormFixture, blurField, createFormFixture, fillForm, triggerValidation };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
be
|
|
3
|
+
} from "./chunk-FDAQJJJ7.js";
|
|
4
|
+
|
|
5
|
+
// ../testing/dist/index.js
|
|
6
|
+
function fillForm(form, values) {
|
|
7
|
+
form.batch(() => {
|
|
8
|
+
for (const [path, value] of Object.entries(values)) {
|
|
9
|
+
form.set(path, value);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function blurField(form, path) {
|
|
14
|
+
const current = form.get(path);
|
|
15
|
+
const sentinel = /* @__PURE__ */ Object.create(null);
|
|
16
|
+
form.batch(() => {
|
|
17
|
+
form.set(path, sentinel, { touch: true });
|
|
18
|
+
form.set(path, current);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function triggerValidation(form, paths) {
|
|
22
|
+
return form.validate(paths);
|
|
23
|
+
}
|
|
24
|
+
function createFormFixture(config) {
|
|
25
|
+
const form = be({ ...config, asyncDebounceMs: config.asyncDebounceMs ?? 0 });
|
|
26
|
+
return {
|
|
27
|
+
form,
|
|
28
|
+
fill: (values) => fillForm(form, values),
|
|
29
|
+
blur: (path) => blurField(form, path),
|
|
30
|
+
validate: (paths) => triggerValidation(form, paths),
|
|
31
|
+
cleanup: () => form.destroy()
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
blurField,
|
|
36
|
+
createFormFixture,
|
|
37
|
+
fillForm,
|
|
38
|
+
triggerValidation
|
|
39
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutro/form",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "High-performance, zero-dependency, framework-agnostic reactive form engine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"form",
|
|
@@ -23,12 +23,18 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
26
27
|
"exports": {
|
|
27
28
|
"./core": {
|
|
28
29
|
"types": "./dist/core.d.ts",
|
|
29
30
|
"import": "./dist/core.js",
|
|
30
31
|
"require": "./dist/core.cjs"
|
|
31
32
|
},
|
|
33
|
+
"./devtools": {
|
|
34
|
+
"types": "./dist/devtools.d.ts",
|
|
35
|
+
"import": "./dist/devtools.js",
|
|
36
|
+
"require": "./dist/devtools.cjs"
|
|
37
|
+
},
|
|
32
38
|
"./adapters/react": {
|
|
33
39
|
"types": "./dist/adapters/react.d.ts",
|
|
34
40
|
"import": "./dist/adapters/react.js",
|
|
@@ -53,6 +59,11 @@
|
|
|
53
59
|
"types": "./dist/adapters/angular.d.ts",
|
|
54
60
|
"import": "./dist/adapters/angular.js",
|
|
55
61
|
"require": "./dist/adapters/angular.cjs"
|
|
62
|
+
},
|
|
63
|
+
"./testing": {
|
|
64
|
+
"types": "./dist/testing.d.ts",
|
|
65
|
+
"import": "./dist/testing.js",
|
|
66
|
+
"require": "./dist/testing.cjs"
|
|
56
67
|
}
|
|
57
68
|
},
|
|
58
69
|
"typesVersions": {
|
|
@@ -60,6 +71,9 @@
|
|
|
60
71
|
"core": [
|
|
61
72
|
"./dist/core.d.ts"
|
|
62
73
|
],
|
|
74
|
+
"devtools": [
|
|
75
|
+
"./dist/devtools.d.ts"
|
|
76
|
+
],
|
|
63
77
|
"adapters/react": [
|
|
64
78
|
"./dist/adapters/react.d.ts"
|
|
65
79
|
],
|
|
@@ -74,6 +88,9 @@
|
|
|
74
88
|
],
|
|
75
89
|
"adapters/angular": [
|
|
76
90
|
"./dist/adapters/angular.d.ts"
|
|
91
|
+
],
|
|
92
|
+
"testing": [
|
|
93
|
+
"./dist/testing.d.ts"
|
|
77
94
|
]
|
|
78
95
|
}
|
|
79
96
|
},
|
|
@@ -107,12 +124,13 @@
|
|
|
107
124
|
},
|
|
108
125
|
"devDependencies": {
|
|
109
126
|
"tsup": "^8.0.0",
|
|
110
|
-
"@neutro/form-core": "0.0
|
|
111
|
-
"@neutro/form-
|
|
112
|
-
"@neutro/form-
|
|
113
|
-
"@neutro/form-
|
|
114
|
-
"@neutro/form-
|
|
115
|
-
"@neutro/form-
|
|
127
|
+
"@neutro/form-core": "0.1.0",
|
|
128
|
+
"@neutro/form-vue": "0.1.0",
|
|
129
|
+
"@neutro/form-solid": "0.1.0",
|
|
130
|
+
"@neutro/form-angular": "0.1.0",
|
|
131
|
+
"@neutro/form-svelte": "0.1.0",
|
|
132
|
+
"@neutro/form-testing": "0.1.0",
|
|
133
|
+
"@neutro/form-react": "0.1.0"
|
|
116
134
|
},
|
|
117
135
|
"scripts": {
|
|
118
136
|
"build": "tsup"
|