@formos/kernel 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/README.md +433 -0
- package/dist/index.d.ts +372 -0
- package/dist/index.js +763 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { ValidationTrigger, NormalizedSchemaV1, Conditional } from '@formos/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definitions for kernel adapters and execution context
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validator executor function signature.
|
|
8
|
+
*
|
|
9
|
+
* Adapters implement this interface to execute actual validation logic.
|
|
10
|
+
* The kernel calls validators with field values and expects either:
|
|
11
|
+
* - `null` if the value is valid
|
|
12
|
+
* - Error message string if invalid
|
|
13
|
+
*
|
|
14
|
+
* **IMPORTANT**: Return `null` for valid values, NOT `undefined`.
|
|
15
|
+
*
|
|
16
|
+
* Validators can be synchronous or async (returning a Promise).
|
|
17
|
+
*
|
|
18
|
+
* @param value - The field value to validate
|
|
19
|
+
* @param params - Validation parameters from the schema's `rule` property
|
|
20
|
+
* @param context - Execution context with access to other field values
|
|
21
|
+
* @returns Error message if invalid, `null` if valid
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
* @stable
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const required: ValidatorExecutor = (value, params, context) => {
|
|
29
|
+
* return value ? null : 'This field is required';
|
|
30
|
+
* };
|
|
31
|
+
*
|
|
32
|
+
* const matchesField: ValidatorExecutor = (value, params, context) => {
|
|
33
|
+
* const { targetField } = params as { targetField: string };
|
|
34
|
+
* const targetValue = context.getValue(targetField);
|
|
35
|
+
* return value === targetValue ? null : 'Fields must match';
|
|
36
|
+
* };
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
type ValidatorExecutor = (value: unknown, params: unknown, context: ValidationContext) => Promise<string | null> | string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Validation execution context.
|
|
42
|
+
*
|
|
43
|
+
* Provides validators with read-only access to form state.
|
|
44
|
+
* Validators can check other field values for cross-field validation.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
* @stable
|
|
48
|
+
*/
|
|
49
|
+
interface ValidationContext {
|
|
50
|
+
/** Current field name being validated */
|
|
51
|
+
fieldName: string;
|
|
52
|
+
/** All form values */
|
|
53
|
+
values: ReadonlyMap<string, unknown>;
|
|
54
|
+
/** Get value of another field */
|
|
55
|
+
getValue: (fieldName: string) => unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Effect executor function signature.
|
|
59
|
+
*
|
|
60
|
+
* Adapters implement this interface to execute side effects triggered
|
|
61
|
+
* by field changes. Common use cases:
|
|
62
|
+
* - Sync fields (e.g., calculate fullName from firstName + lastName)
|
|
63
|
+
* - Trigger API calls (e.g., fetch cities when country changes)
|
|
64
|
+
* - Custom business logic
|
|
65
|
+
*
|
|
66
|
+
* Effects have write access via `context.setValue()`.
|
|
67
|
+
*
|
|
68
|
+
* @param params - Effect configuration from the schema's `config` property
|
|
69
|
+
* @param context - Execution context with form values and setValue
|
|
70
|
+
* @returns Optional result value (typically unused)
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
* @stable
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const syncFullName: EffectExecutor = (params, context) => {
|
|
78
|
+
* const firstName = context.getValue('firstName') as string;
|
|
79
|
+
* const lastName = context.getValue('lastName') as string;
|
|
80
|
+
* context.setValue('fullName', `${firstName} ${lastName}`);\n * };
|
|
81
|
+
*
|
|
82
|
+
* const fetchCities: EffectExecutor = async (params, context) => {
|
|
83
|
+
* const country = context.sourceValue as string;
|
|
84
|
+
* const response = await fetch(`/api/cities?country=${country}`);
|
|
85
|
+
* const cities = await response.json();
|
|
86
|
+
* context.setValue('cities', cities);\n * };
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
type EffectExecutor = (params: unknown, context: EffectContext) => Promise<unknown> | unknown;
|
|
90
|
+
/**
|
|
91
|
+
* Effect execution context.
|
|
92
|
+
*
|
|
93
|
+
* Provides effects with read-write access to form state.
|
|
94
|
+
* Effects can read any field value and update the target field.
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
* @stable
|
|
98
|
+
*/
|
|
99
|
+
interface EffectContext {
|
|
100
|
+
/** Source field that triggered the effect */
|
|
101
|
+
sourceField: string;
|
|
102
|
+
/** Source field's new value */
|
|
103
|
+
sourceValue: unknown;
|
|
104
|
+
/** Target field (if applicable) */
|
|
105
|
+
targetField?: string;
|
|
106
|
+
/** All form values */
|
|
107
|
+
values: ReadonlyMap<string, unknown>;
|
|
108
|
+
/** Get value of a field */
|
|
109
|
+
getValue: (fieldName: string) => unknown;
|
|
110
|
+
/** Set value of a field (for sync effects) */
|
|
111
|
+
setValue: (fieldName: string, value: unknown) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Value management operations
|
|
116
|
+
* Handles getting and setting field values with validation and effects
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Options for setValue operation
|
|
121
|
+
*/
|
|
122
|
+
/**
|
|
123
|
+
* Options for setValue operations.
|
|
124
|
+
*
|
|
125
|
+
* @public
|
|
126
|
+
* @stable
|
|
127
|
+
*/
|
|
128
|
+
interface SetValueOptions {
|
|
129
|
+
/** Validation trigger (default: "onChange") */
|
|
130
|
+
trigger?: ValidationTrigger | "manual";
|
|
131
|
+
/** If true, skip validation and effects */
|
|
132
|
+
silent?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Form lifecycle operations
|
|
137
|
+
* Handles submit and reset operations
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Submit handler callback
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* Callback invoked when form is successfully submitted.
|
|
145
|
+
*
|
|
146
|
+
* Only called if all validations pass. Receives all form values.
|
|
147
|
+
*
|
|
148
|
+
* @public
|
|
149
|
+
* @stable
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const onSubmit: SubmitHandler = async (values) => {
|
|
154
|
+
* await fetch('/api/submit', {
|
|
155
|
+
* method: 'POST',
|
|
156
|
+
* body: JSON.stringify(values)
|
|
157
|
+
* });
|
|
158
|
+
* };
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
type SubmitHandler = (values: Record<string, unknown>) => void | Promise<void>;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Form engine factory
|
|
165
|
+
* Creates FormEngine instances with configuration
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Configuration options for creating a form engine.
|
|
170
|
+
*
|
|
171
|
+
* All options use the **adapter pattern** to remain framework-agnostic.
|
|
172
|
+
* The kernel provides orchestration; adapters provide implementation.
|
|
173
|
+
*
|
|
174
|
+
* @public
|
|
175
|
+
* @stable
|
|
176
|
+
*/
|
|
177
|
+
interface FormEngineOptions {
|
|
178
|
+
/** Custom validator executors */
|
|
179
|
+
validators?: Record<string, ValidatorExecutor>;
|
|
180
|
+
/** Custom effect executors */
|
|
181
|
+
effectExecutors?: Record<string, EffectExecutor>;
|
|
182
|
+
/** Submit handler */
|
|
183
|
+
onSubmit?: SubmitHandler;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Form engine interface - the primary API for interacting with forms.
|
|
187
|
+
*
|
|
188
|
+
* This headless engine manages all form logic:
|
|
189
|
+
* - State management (values, errors, touched, dirty)
|
|
190
|
+
* - Validation orchestration with pluggable validators
|
|
191
|
+
* - Effect execution for field interactions
|
|
192
|
+
* - Multi-step navigation with validation guards
|
|
193
|
+
*
|
|
194
|
+
* The engine is **completely UI-agnostic** - no React, no DOM.
|
|
195
|
+
* UI packages consume this API to build interactive forms.
|
|
196
|
+
*
|
|
197
|
+
* @public
|
|
198
|
+
* @stable
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* import { createFormEngine } from '@formos/kernel';
|
|
203
|
+
* import { normalizeSchema } from '@formos/schema';
|
|
204
|
+
*
|
|
205
|
+
* const normalized = normalizeSchema(mySchema);
|
|
206
|
+
* const engine = createFormEngine(normalized, {
|
|
207
|
+
* validators: { required: (value) => value ? null : 'Required' }
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* // Get/set values
|
|
211
|
+
* await engine.setValue('email', 'user@example.com');
|
|
212
|
+
* const email = engine.getValue('email');
|
|
213
|
+
*
|
|
214
|
+
* // Validate
|
|
215
|
+
* const isValid = await engine.validate();
|
|
216
|
+
*
|
|
217
|
+
* // Submit
|
|
218
|
+
* const success = await engine.submit();
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
interface FormEngine {
|
|
222
|
+
getValue(fieldName: string): unknown;
|
|
223
|
+
getError(fieldName: string): string | undefined;
|
|
224
|
+
getErrors(): Record<string, string | undefined>;
|
|
225
|
+
isValid(): boolean;
|
|
226
|
+
isDirty(fieldName?: string): boolean;
|
|
227
|
+
isTouched(fieldName: string): boolean;
|
|
228
|
+
setValue(fieldName: string, value: unknown, options?: SetValueOptions): Promise<void>;
|
|
229
|
+
setError(fieldName: string, error: string): void;
|
|
230
|
+
clearError(fieldName: string): void;
|
|
231
|
+
markTouched(fieldName: string): void;
|
|
232
|
+
validate(fieldName?: string, trigger?: "onChange" | "onBlur" | "onSubmit" | "manual"): Promise<boolean>;
|
|
233
|
+
submit(): Promise<boolean>;
|
|
234
|
+
reset(): void;
|
|
235
|
+
getCurrentStep?(): number;
|
|
236
|
+
getTotalSteps?(): number;
|
|
237
|
+
nextStep?(): Promise<boolean>;
|
|
238
|
+
prevStep?(): void;
|
|
239
|
+
goToStep?(stepIndex: number): Promise<boolean>;
|
|
240
|
+
canGoToStep?(stepIndex: number): boolean;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Create a form engine instance from a normalized schema.
|
|
244
|
+
*
|
|
245
|
+
* This is the primary entry point for the kernel. It instantiates
|
|
246
|
+
* a {@link FormEngine} that manages all form logic.
|
|
247
|
+
*
|
|
248
|
+
* The engine is stateful and mutable. Each call creates a new
|
|
249
|
+
* independent form instance with its own state.
|
|
250
|
+
*
|
|
251
|
+
* @param schema - Normalized schema from {@link normalizeSchema}
|
|
252
|
+
* @param options - Configuration with validators, effects, and submit handler
|
|
253
|
+
* @returns Fully configured form engine instance
|
|
254
|
+
*
|
|
255
|
+
* @public
|
|
256
|
+
* @stable
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* import { createFormEngine } from '@formos/kernel';
|
|
261
|
+
* import { normalizeSchema } from '@formos/schema';
|
|
262
|
+
*
|
|
263
|
+
* const schema = { version: 'v1', fields: [...] };
|
|
264
|
+
* const normalized = normalizeSchema(schema);
|
|
265
|
+
*
|
|
266
|
+
* const engine = createFormEngine(normalized, {
|
|
267
|
+
* validators: {
|
|
268
|
+
* required: (value) => value ? null : 'Required',
|
|
269
|
+
* email: (value) => /\\S+@\\S+/.test(value) ? null : 'Invalid'
|
|
270
|
+
* },
|
|
271
|
+
* onSubmit: async (values) => {
|
|
272
|
+
* await api.post('/submit', values);
|
|
273
|
+
* }
|
|
274
|
+
* });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
declare function createFormEngine(schema: NormalizedSchemaV1, options?: FormEngineOptions): FormEngine;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Conditional evaluation engine
|
|
281
|
+
* Evaluates ConditionalRule and ConditionalGroup from schema
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Evaluate a conditional expression against current form values.
|
|
286
|
+
*
|
|
287
|
+
* Handles both single {@link ConditionalRule} and complex {@link ConditionalGroup}
|
|
288
|
+
* with AND/OR logic. This is the core function for dynamic field behavior.
|
|
289
|
+
*
|
|
290
|
+
* @param conditional - Rule or group to evaluate
|
|
291
|
+
* @param getValue - Function to retrieve field values by name
|
|
292
|
+
* @returns True if the condition is satisfied
|
|
293
|
+
*
|
|
294
|
+
* @public
|
|
295
|
+
* @stable
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const condition: ConditionalRule = {
|
|
300
|
+
* field: 'age',
|
|
301
|
+
* operator: 'greaterThan',
|
|
302
|
+
* value: 18
|
|
303
|
+
* };
|
|
304
|
+
*
|
|
305
|
+
* const getValue = (name: string) => formValues[name];
|
|
306
|
+
* const result = evaluateConditional(condition, getValue);
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
declare function evaluateConditional(conditional: Conditional, getValue: (fieldName: string) => unknown): boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Check if a field should be visible based on its visibleWhen condition.
|
|
312
|
+
*
|
|
313
|
+
* Returns `true` if:
|
|
314
|
+
* - Field has no `visibleWhen` property (always visible)
|
|
315
|
+
* - The `visibleWhen` condition evaluates to `true`
|
|
316
|
+
*
|
|
317
|
+
* @param field - Field schema with optional visibleWhen property
|
|
318
|
+
* @param getValue - Function to retrieve field values
|
|
319
|
+
* @returns True if field should be visible
|
|
320
|
+
*
|
|
321
|
+
* @public
|
|
322
|
+
* @stable
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* const field = {
|
|
327
|
+
* name: 'ssn',
|
|
328
|
+
* type: 'text',
|
|
329
|
+
* visibleWhen: { field: 'country', operator: 'equals', value: 'US' }
|
|
330
|
+
* };
|
|
331
|
+
*
|
|
332
|
+
* if (isFieldVisible(field, getValue)) {
|
|
333
|
+
* // Render the field
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
declare function isFieldVisible(field: {
|
|
338
|
+
visibleWhen?: Conditional;
|
|
339
|
+
}, getValue: (fieldName: string) => unknown): boolean;
|
|
340
|
+
/**
|
|
341
|
+
* Check if a field is required based on its required configuration.
|
|
342
|
+
*
|
|
343
|
+
* Handles three cases:
|
|
344
|
+
* - `undefined`: Field is optional (returns `false`)
|
|
345
|
+
* - `boolean`: Returns the boolean value directly
|
|
346
|
+
* - `Conditional`: Evaluates the condition dynamically
|
|
347
|
+
*
|
|
348
|
+
* @param field - Field schema with optional required property
|
|
349
|
+
* @param getValue - Function to retrieve field values
|
|
350
|
+
* @returns True if field is required
|
|
351
|
+
*
|
|
352
|
+
* @public
|
|
353
|
+
* @stable
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```typescript
|
|
357
|
+
* const field = {
|
|
358
|
+
* name: 'phone',
|
|
359
|
+
* type: 'tel',
|
|
360
|
+
* required: { field: 'contactMethod', operator: 'equals', value: 'phone' }
|
|
361
|
+
* };
|
|
362
|
+
*
|
|
363
|
+
* if (isFieldRequired(field, getValue)) {
|
|
364
|
+
* // Show required indicator
|
|
365
|
+
* }
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
declare function isFieldRequired(field: {
|
|
369
|
+
required?: boolean | Conditional;
|
|
370
|
+
}, getValue: (fieldName: string) => unknown): boolean;
|
|
371
|
+
|
|
372
|
+
export { type EffectContext, type EffectExecutor, type FormEngine, type FormEngineOptions, type SetValueOptions, type SubmitHandler, type ValidationContext, type ValidatorExecutor, createFormEngine, evaluateConditional, isFieldRequired, isFieldVisible };
|