@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/dist/index.js ADDED
@@ -0,0 +1,763 @@
1
+ // src/state.ts
2
+ var FormState = class {
3
+ /** Field values */
4
+ values = /* @__PURE__ */ new Map();
5
+ /** Field errors */
6
+ errors = /* @__PURE__ */ new Map();
7
+ /** Touched fields (user has interacted) */
8
+ touched = /* @__PURE__ */ new Set();
9
+ /** Dirty fields (value has changed from default) */
10
+ dirty = /* @__PURE__ */ new Set();
11
+ /** Current step index (for multi-step forms) */
12
+ currentStepIndex = 0;
13
+ /** Initial values (for reset) */
14
+ initialValues = /* @__PURE__ */ new Map();
15
+ constructor(defaultValues) {
16
+ if (defaultValues) {
17
+ this.values = new Map(defaultValues);
18
+ this.initialValues = new Map(defaultValues);
19
+ }
20
+ }
21
+ // ========================================================================
22
+ // VALUES
23
+ // ========================================================================
24
+ getValue(fieldName) {
25
+ return this.values.get(fieldName);
26
+ }
27
+ setValue(fieldName, value) {
28
+ this.values.set(fieldName, value);
29
+ const initialValue = this.initialValues.get(fieldName);
30
+ if (value !== initialValue) {
31
+ this.dirty.add(fieldName);
32
+ } else {
33
+ this.dirty.delete(fieldName);
34
+ }
35
+ }
36
+ getAllValues() {
37
+ return this.values;
38
+ }
39
+ // ========================================================================
40
+ // ERRORS
41
+ // ========================================================================
42
+ getError(fieldName) {
43
+ return this.errors.get(fieldName);
44
+ }
45
+ setError(fieldName, error) {
46
+ if (error) {
47
+ this.errors.set(fieldName, error);
48
+ } else {
49
+ this.errors.delete(fieldName);
50
+ }
51
+ }
52
+ getAllErrors() {
53
+ const errorObj = {};
54
+ this.errors.forEach((error, fieldName) => {
55
+ errorObj[fieldName] = error;
56
+ });
57
+ return errorObj;
58
+ }
59
+ clearError(fieldName) {
60
+ this.errors.delete(fieldName);
61
+ }
62
+ clearAllErrors() {
63
+ this.errors.clear();
64
+ }
65
+ hasErrors() {
66
+ return Array.from(this.errors.values()).some(Boolean);
67
+ }
68
+ // ========================================================================
69
+ // TOUCHED
70
+ // ========================================================================
71
+ markTouched(fieldName) {
72
+ this.touched.add(fieldName);
73
+ }
74
+ isTouched(fieldName) {
75
+ return this.touched.has(fieldName);
76
+ }
77
+ // ========================================================================
78
+ // DIRTY
79
+ // ========================================================================
80
+ isDirty(fieldName) {
81
+ return this.dirty.has(fieldName);
82
+ }
83
+ isFormDirty() {
84
+ return this.dirty.size > 0;
85
+ }
86
+ // ========================================================================
87
+ // STEP
88
+ // ========================================================================
89
+ getCurrentStepIndex() {
90
+ return this.currentStepIndex;
91
+ }
92
+ setCurrentStepIndex(index) {
93
+ this.currentStepIndex = index;
94
+ }
95
+ // ========================================================================
96
+ // RESET
97
+ // ========================================================================
98
+ reset() {
99
+ this.values = new Map(this.initialValues);
100
+ this.errors.clear();
101
+ this.touched.clear();
102
+ this.dirty.clear();
103
+ this.currentStepIndex = 0;
104
+ }
105
+ };
106
+
107
+ // src/steps.ts
108
+ var StepController = class {
109
+ schema;
110
+ state;
111
+ constructor(schema, state) {
112
+ this.schema = schema;
113
+ this.state = state;
114
+ }
115
+ // ========================================================================
116
+ // STEP QUERIES
117
+ // ========================================================================
118
+ /**
119
+ * Check if this is a multi-step form
120
+ */
121
+ isMultiStep() {
122
+ return this.schema?.isMultiStep ?? false;
123
+ }
124
+ /**
125
+ * Get total number of steps
126
+ */
127
+ getTotalSteps() {
128
+ return this.schema?.steps?.length ?? 1;
129
+ }
130
+ /**
131
+ * Get current step index (0-based)
132
+ */
133
+ getCurrentStepIndex() {
134
+ if (!this.isMultiStep()) {
135
+ return 0;
136
+ }
137
+ return this.state.getCurrentStepIndex();
138
+ }
139
+ /**
140
+ * Get current step ID
141
+ */
142
+ getCurrentStepId() {
143
+ if (!this.isMultiStep()) {
144
+ return void 0;
145
+ }
146
+ const currentIndex = this.getCurrentStepIndex();
147
+ const step = this.schema?.steps?.[currentIndex];
148
+ return step?.id;
149
+ }
150
+ /**
151
+ * Get current step schema
152
+ */
153
+ getCurrentStep() {
154
+ if (!this.isMultiStep()) {
155
+ return void 0;
156
+ }
157
+ const currentIndex = this.getCurrentStepIndex();
158
+ return this.schema?.steps?.[currentIndex];
159
+ }
160
+ // ========================================================================
161
+ // NAVIGATION
162
+ // ========================================================================
163
+ /**
164
+ * Check if can navigate to next step
165
+ */
166
+ canGoNext() {
167
+ if (!this.isMultiStep()) {
168
+ return false;
169
+ }
170
+ const currentIndex = this.getCurrentStepIndex();
171
+ const totalSteps = this.getTotalSteps();
172
+ return currentIndex < totalSteps - 1;
173
+ }
174
+ /**
175
+ * Check if can navigate to previous step
176
+ */
177
+ canGoPrev() {
178
+ if (!this.isMultiStep()) {
179
+ return false;
180
+ }
181
+ const currentIndex = this.getCurrentStepIndex();
182
+ const currentStep = this.getCurrentStep();
183
+ if (currentStep?.allowBack === false) {
184
+ return false;
185
+ }
186
+ return currentIndex > 0;
187
+ }
188
+ /**
189
+ * Navigate to next step
190
+ * Returns true if navigation succeeded
191
+ */
192
+ nextStep() {
193
+ if (!this.canGoNext()) {
194
+ return false;
195
+ }
196
+ const currentIndex = this.getCurrentStepIndex();
197
+ this.state.setCurrentStepIndex(currentIndex + 1);
198
+ return true;
199
+ }
200
+ /**
201
+ * Navigate to previous step
202
+ * Returns true if navigation succeeded
203
+ */
204
+ prevStep() {
205
+ if (!this.canGoPrev()) {
206
+ return false;
207
+ }
208
+ const currentIndex = this.getCurrentStepIndex();
209
+ this.state.setCurrentStepIndex(currentIndex - 1);
210
+ return true;
211
+ }
212
+ /**
213
+ * Jump to specific step index
214
+ */
215
+ goToStep(index) {
216
+ if (!this.isMultiStep()) {
217
+ return false;
218
+ }
219
+ const totalSteps = this.getTotalSteps();
220
+ if (index < 0 || index >= totalSteps) {
221
+ return false;
222
+ }
223
+ this.state.setCurrentStepIndex(index);
224
+ return true;
225
+ }
226
+ // ========================================================================
227
+ // VALIDATION
228
+ // ========================================================================
229
+ /**
230
+ * Check if current step allows proceeding without validation
231
+ */
232
+ allowNextWithoutValidation() {
233
+ const currentStep = this.getCurrentStep();
234
+ return currentStep?.allowNextWithoutValidation ?? false;
235
+ }
236
+ /**
237
+ * Check if current step is required
238
+ */
239
+ isCurrentStepRequired() {
240
+ const currentStep = this.getCurrentStep();
241
+ return currentStep?.required ?? true;
242
+ }
243
+ /**
244
+ * Get fields for current step
245
+ */
246
+ getCurrentStepFields() {
247
+ if (!this.isMultiStep()) {
248
+ return this.schema?.fieldNames ?? [];
249
+ }
250
+ const currentStep = this.getCurrentStep();
251
+ return currentStep?.fields ?? [];
252
+ }
253
+ };
254
+
255
+ // src/engine/init.ts
256
+ function initializeFormState(schema) {
257
+ const defaultValues = /* @__PURE__ */ new Map();
258
+ schema?.fields?.forEach((field) => {
259
+ if (field?.defaultValue !== void 0) {
260
+ defaultValues.set(field?.name, field.defaultValue);
261
+ }
262
+ });
263
+ return new FormState(defaultValues);
264
+ }
265
+ function initializeStepController(schema, state) {
266
+ if (!schema?.steps || schema?.steps?.length === 0) {
267
+ return void 0;
268
+ }
269
+ return new StepController(schema, state);
270
+ }
271
+ function isDirty(state) {
272
+ const values = state.getAllValues();
273
+ for (const fieldName of values.keys()) {
274
+ if (state.isDirty(fieldName)) {
275
+ return true;
276
+ }
277
+ }
278
+ return false;
279
+ }
280
+
281
+ // src/validation.ts
282
+ import { shouldValidateOnTrigger } from "@formos/schema";
283
+ async function validateField(field, value, trigger, validators, context) {
284
+ const validations = field?.validations ?? [];
285
+ const relevantValidations = validations.filter(
286
+ (validation) => {
287
+ if (trigger === "manual") {
288
+ return true;
289
+ }
290
+ return shouldValidateOnTrigger(validation, trigger);
291
+ }
292
+ );
293
+ for (const validation of relevantValidations) {
294
+ const error = await executeValidation(
295
+ validation,
296
+ value,
297
+ validators,
298
+ context
299
+ );
300
+ if (error) {
301
+ return {
302
+ fieldName: field?.name,
303
+ isValid: false,
304
+ error
305
+ };
306
+ }
307
+ }
308
+ return {
309
+ fieldName: field?.name,
310
+ isValid: true
311
+ };
312
+ }
313
+ async function executeValidation(validation, value, validators, context) {
314
+ const executor = validators?.[validation?.validator];
315
+ if (!executor) {
316
+ console.warn(
317
+ `Validator "${validation?.validator}" not found. Skipping validation for field "${context?.fieldName}".`
318
+ );
319
+ return null;
320
+ }
321
+ try {
322
+ const result = await executor(value, validation?.rule, context);
323
+ if (result && validation?.message) {
324
+ return validation.message;
325
+ }
326
+ return result;
327
+ } catch (error) {
328
+ console.error(`Validation error for field "${context?.fieldName}":`, error);
329
+ return "Validation failed";
330
+ }
331
+ }
332
+ async function validateFields(fields, values, trigger, validators, getValue2) {
333
+ const errors = /* @__PURE__ */ new Map();
334
+ const validationPromises = fields.map((field) => {
335
+ const value = values.get(field?.name);
336
+ const context = {
337
+ fieldName: field?.name,
338
+ values,
339
+ getValue: getValue2
340
+ };
341
+ return validateField(field, value, trigger, validators, context);
342
+ });
343
+ const results = await Promise.all(validationPromises);
344
+ results.forEach((result) => {
345
+ if (!result?.isValid && result?.error && result?.fieldName) {
346
+ errors.set(result.fieldName, result.error);
347
+ }
348
+ });
349
+ return errors;
350
+ }
351
+
352
+ // src/effects.ts
353
+ async function executeEffects(sourceField, sourceValue, effects, effectExecutors, values, getValue2, setValue2) {
354
+ if (!effects || effects?.length === 0) {
355
+ return;
356
+ }
357
+ const relevantEffects = effects.filter(
358
+ (effect) => effect?.sourceField === sourceField
359
+ );
360
+ for (const effect of relevantEffects) {
361
+ await executeEffect(
362
+ effect,
363
+ sourceField,
364
+ sourceValue,
365
+ effectExecutors,
366
+ values,
367
+ getValue2,
368
+ setValue2
369
+ );
370
+ }
371
+ }
372
+ async function executeEffect(effect, sourceField, sourceValue, effectExecutors, values, getValue2, setValue2) {
373
+ const { type, config, targetField } = effect;
374
+ if (type === "sync") {
375
+ await executeSyncEffect(
376
+ sourceValue,
377
+ targetField,
378
+ config,
379
+ effectExecutors,
380
+ values,
381
+ getValue2,
382
+ setValue2
383
+ );
384
+ return;
385
+ }
386
+ const executor = effectExecutors?.[type];
387
+ if (!executor) {
388
+ console.warn(`Effect executor "${type}" not found. Skipping effect.`);
389
+ return;
390
+ }
391
+ const context = {
392
+ sourceField,
393
+ sourceValue,
394
+ targetField,
395
+ values,
396
+ getValue: getValue2,
397
+ setValue: setValue2
398
+ };
399
+ try {
400
+ await executor(config, context);
401
+ } catch (error) {
402
+ console.error(`Effect execution error for field "${sourceField}":`, error);
403
+ }
404
+ }
405
+ async function executeSyncEffect(sourceValue, targetField, config, effectExecutors, values, getValue2, setValue2) {
406
+ if (!targetField) {
407
+ console.warn("Sync effect requires targetField");
408
+ return;
409
+ }
410
+ const syncExecutor = effectExecutors?.["sync"];
411
+ if (syncExecutor) {
412
+ const context = {
413
+ sourceField: "",
414
+ sourceValue,
415
+ targetField,
416
+ values,
417
+ getValue: getValue2,
418
+ setValue: setValue2
419
+ };
420
+ const transformedValue = await syncExecutor(config, context);
421
+ setValue2(targetField, transformedValue);
422
+ } else {
423
+ setValue2(targetField, sourceValue);
424
+ }
425
+ }
426
+
427
+ // src/conditionals.ts
428
+ function evaluateConditional(conditional, getValue2) {
429
+ if ("logic" in conditional) {
430
+ return evaluateConditionalGroup(conditional, getValue2);
431
+ }
432
+ return evaluateConditionalRule(conditional, getValue2);
433
+ }
434
+ function evaluateConditionalGroup(group, getValue2) {
435
+ const { logic, rules } = group;
436
+ if (!rules || rules?.length === 0) {
437
+ return true;
438
+ }
439
+ if (logic === "and") {
440
+ return rules.every(
441
+ (rule) => evaluateConditional(rule, getValue2)
442
+ );
443
+ } else {
444
+ return rules.some(
445
+ (rule) => evaluateConditional(rule, getValue2)
446
+ );
447
+ }
448
+ }
449
+ function evaluateConditionalRule(rule, getValue2) {
450
+ const { field, operator, value } = rule;
451
+ const fieldValue = getValue2(field);
452
+ switch (operator) {
453
+ case "equals":
454
+ return fieldValue === value;
455
+ case "notEquals":
456
+ return fieldValue !== value;
457
+ case "exists":
458
+ return fieldValue !== void 0 && fieldValue !== null && fieldValue !== "";
459
+ case "notExists":
460
+ return fieldValue === void 0 || fieldValue === null || fieldValue === "";
461
+ case "includes":
462
+ if (Array.isArray(fieldValue)) {
463
+ return fieldValue.includes(value);
464
+ }
465
+ if (typeof fieldValue === "string" && typeof value === "string") {
466
+ return fieldValue.includes(value);
467
+ }
468
+ return false;
469
+ case "notIncludes":
470
+ if (Array.isArray(fieldValue)) {
471
+ return !fieldValue.includes(value);
472
+ }
473
+ if (typeof fieldValue === "string" && typeof value === "string") {
474
+ return !fieldValue.includes(value);
475
+ }
476
+ return true;
477
+ case "greaterThan":
478
+ if (typeof fieldValue === "number" && typeof value === "number") {
479
+ return fieldValue > value;
480
+ }
481
+ return false;
482
+ case "lessThan":
483
+ if (typeof fieldValue === "number" && typeof value === "number") {
484
+ return fieldValue < value;
485
+ }
486
+ return false;
487
+ default:
488
+ return false;
489
+ }
490
+ }
491
+ function isFieldVisible(field, getValue2) {
492
+ if (!field?.visibleWhen) {
493
+ return true;
494
+ }
495
+ return evaluateConditional(field.visibleWhen, getValue2);
496
+ }
497
+ function isFieldRequired(field, getValue2) {
498
+ if (field?.required === void 0) {
499
+ return false;
500
+ }
501
+ if (typeof field.required === "boolean") {
502
+ return field.required;
503
+ }
504
+ return evaluateConditional(field.required, getValue2);
505
+ }
506
+
507
+ // src/engine/helpers.ts
508
+ function getVisibleFields(schema, getValue2) {
509
+ return schema?.fields?.filter((field) => {
510
+ return isFieldVisible(field, getValue2);
511
+ }) ?? [];
512
+ }
513
+ function getFieldSchema(schema, fieldName) {
514
+ return schema?.fieldMap?.get(fieldName);
515
+ }
516
+
517
+ // src/engine/values.ts
518
+ function getValue(state, fieldName) {
519
+ return state.getValue(fieldName);
520
+ }
521
+ async function setValue(state, schema, fieldName, value, options, validators, effectExecutors, getValueHelper) {
522
+ const { trigger = "onChange", silent = false } = options;
523
+ state.setValue(fieldName, value);
524
+ if (silent) {
525
+ return;
526
+ }
527
+ state.clearError(fieldName);
528
+ if (trigger !== "manual") {
529
+ const field2 = getFieldSchema(schema, fieldName);
530
+ if (field2) {
531
+ const context = {
532
+ fieldName,
533
+ values: state.getAllValues(),
534
+ getValue: getValueHelper
535
+ };
536
+ const result = await validateField(
537
+ field2,
538
+ value,
539
+ trigger,
540
+ validators,
541
+ context
542
+ );
543
+ if (!result?.isValid && result?.error) {
544
+ state.setError(fieldName, result?.error);
545
+ }
546
+ }
547
+ }
548
+ const field = getFieldSchema(schema, fieldName);
549
+ const fieldEffects = field?.effects ?? [];
550
+ const globalEffects = schema?.globalEffects ?? [];
551
+ const allEffects = [...fieldEffects, ...globalEffects];
552
+ await executeEffects(
553
+ fieldName,
554
+ value,
555
+ allEffects,
556
+ effectExecutors,
557
+ state.getAllValues(),
558
+ getValueHelper,
559
+ (field2, val) => state.setValue(field2, val)
560
+ );
561
+ }
562
+
563
+ // src/engine/validation-ops.ts
564
+ function isValid(state) {
565
+ return !state.hasErrors();
566
+ }
567
+ function getError(state, fieldName) {
568
+ return state.getError(fieldName);
569
+ }
570
+ function getErrors(state) {
571
+ return state.getAllErrors();
572
+ }
573
+ async function validate(state, schema, validators, getValue2, fieldName, trigger = "manual") {
574
+ if (fieldName) {
575
+ const field = getFieldSchema(schema, fieldName);
576
+ if (!field) {
577
+ return true;
578
+ }
579
+ const value = state.getValue(fieldName);
580
+ const context = {
581
+ fieldName,
582
+ values: state.getAllValues(),
583
+ getValue: getValue2
584
+ };
585
+ const result = await validateField(
586
+ field,
587
+ value,
588
+ trigger,
589
+ validators,
590
+ context
591
+ );
592
+ if (result?.isValid) {
593
+ state.clearError(fieldName);
594
+ return true;
595
+ } else {
596
+ state.setError(fieldName, result?.error);
597
+ return false;
598
+ }
599
+ } else {
600
+ const visibleFields = getVisibleFields(schema, getValue2);
601
+ state.clearAllErrors();
602
+ const errors = await validateFields(
603
+ visibleFields,
604
+ state.getAllValues(),
605
+ trigger,
606
+ validators,
607
+ getValue2
608
+ );
609
+ errors.forEach((error, field) => {
610
+ if (error) {
611
+ state.setError(field, error);
612
+ }
613
+ });
614
+ return !Array.from(errors.values()).some(Boolean);
615
+ }
616
+ }
617
+
618
+ // src/engine/lifecycle.ts
619
+ async function submit(state, schema, validators, getValue2, onSubmit) {
620
+ const visibleFields = getVisibleFields(schema, getValue2);
621
+ state.clearAllErrors();
622
+ const errors = await validateFields(
623
+ visibleFields,
624
+ state.getAllValues(),
625
+ "onSubmit",
626
+ validators,
627
+ getValue2
628
+ );
629
+ errors.forEach((error, field) => {
630
+ if (error) {
631
+ state.setError(field, error);
632
+ }
633
+ });
634
+ const hasErrors = Array.from(errors.values()).some(Boolean);
635
+ if (!hasErrors && onSubmit) {
636
+ const values = {};
637
+ state.getAllValues().forEach((value, key) => {
638
+ values[key] = value;
639
+ });
640
+ await onSubmit(values);
641
+ return true;
642
+ }
643
+ return !hasErrors;
644
+ }
645
+ function reset(state, stepController, schema) {
646
+ state.reset();
647
+ schema?.fields?.forEach((field) => {
648
+ if (field?.defaultValue !== void 0) {
649
+ state.setValue(field?.name, field.defaultValue);
650
+ }
651
+ });
652
+ if (stepController) {
653
+ stepController.goToStep(0);
654
+ }
655
+ }
656
+
657
+ // src/engine/factory.ts
658
+ function createFormEngine(schema, options = {}) {
659
+ const { validators = {}, effectExecutors = {}, onSubmit } = options;
660
+ const state = initializeFormState(schema);
661
+ const stepController = initializeStepController(
662
+ schema,
663
+ state
664
+ );
665
+ const getValueHelper = (fieldName) => getValue(state, fieldName);
666
+ const engine = {
667
+ // State accessors
668
+ getValue: (fieldName) => getValue(state, fieldName),
669
+ getError: (fieldName) => getError(state, fieldName),
670
+ getErrors: () => getErrors(state),
671
+ isValid: () => isValid(state),
672
+ isDirty: (fieldName) => fieldName ? state.isDirty(fieldName) : isDirty(state),
673
+ isTouched: (fieldName) => state.isTouched(fieldName),
674
+ // State mutators
675
+ setValue: async (fieldName, value, opts) => {
676
+ await setValue(
677
+ state,
678
+ schema,
679
+ fieldName,
680
+ value,
681
+ opts ?? {},
682
+ validators,
683
+ effectExecutors,
684
+ getValueHelper
685
+ );
686
+ },
687
+ setError: (fieldName, error) => {
688
+ state.setError(fieldName, error);
689
+ },
690
+ clearError: (fieldName) => {
691
+ state.clearError(fieldName);
692
+ },
693
+ markTouched: (fieldName) => {
694
+ state.markTouched(fieldName);
695
+ },
696
+ // Validation
697
+ validate: async (fieldName, trigger = "manual") => {
698
+ return validate(
699
+ state,
700
+ schema,
701
+ validators,
702
+ getValueHelper,
703
+ fieldName,
704
+ trigger
705
+ );
706
+ },
707
+ // Lifecycle
708
+ submit: async () => {
709
+ return submit(state, schema, validators, getValueHelper, onSubmit);
710
+ },
711
+ reset: () => {
712
+ reset(state, stepController, schema);
713
+ }
714
+ };
715
+ if (stepController) {
716
+ engine.getCurrentStep = () => stepController.getCurrentStepIndex();
717
+ engine.getTotalSteps = () => stepController.getTotalSteps();
718
+ engine.nextStep = async () => {
719
+ if (!stepController.canGoNext()) {
720
+ return false;
721
+ }
722
+ const isCurrentStepValid = await validate(
723
+ state,
724
+ schema,
725
+ validators,
726
+ getValueHelper
727
+ );
728
+ if (!isCurrentStepValid) {
729
+ return false;
730
+ }
731
+ return stepController.nextStep();
732
+ };
733
+ engine.prevStep = () => {
734
+ stepController.prevStep();
735
+ };
736
+ engine.goToStep = async (stepIndex) => {
737
+ if (stepIndex === stepController.getCurrentStepIndex()) {
738
+ return true;
739
+ }
740
+ const isCurrentStepValid = await validate(
741
+ state,
742
+ schema,
743
+ validators,
744
+ getValueHelper
745
+ );
746
+ if (!isCurrentStepValid) {
747
+ return false;
748
+ }
749
+ return stepController.goToStep(stepIndex);
750
+ };
751
+ engine.canGoToStep = (stepIndex) => {
752
+ return stepIndex >= 0 && stepIndex < stepController.getTotalSteps();
753
+ };
754
+ }
755
+ return engine;
756
+ }
757
+ export {
758
+ createFormEngine,
759
+ evaluateConditional,
760
+ isFieldRequired,
761
+ isFieldVisible
762
+ };
763
+ //# sourceMappingURL=index.js.map