@formhaus/core 0.1.0 → 0.3.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.
@@ -14,6 +14,8 @@ export declare class FormEngine {
14
14
  private _listeners;
15
15
  private _validators;
16
16
  private _allFields;
17
+ private _isDirty;
18
+ private _cache;
17
19
  constructor(schema: FormSchema, initialValues?: Record<string, unknown>, options?: FormEngineOptions);
18
20
  subscribe(listener: () => void): () => void;
19
21
  getSnapshot(): number;
@@ -40,6 +42,7 @@ export declare class FormEngine {
40
42
  reset(values?: Record<string, unknown>): void;
41
43
  private _computeAllFields;
42
44
  private _cascadeClearHiddenFields;
45
+ private _recomputeIfDirty;
43
46
  private _notify;
44
47
  }
45
48
  //# sourceMappingURL=form-engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"form-engine.d.ts","sourceRoot":"","sources":["../../src/engine/form-engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAMjD,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC1C;AAED,qBAAa,UAAU;IACrB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAE5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEtC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,UAAU,CAAc;gBAG9B,MAAM,EAAE,UAAU,EAClB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,CAAC,EAAE,iBAAiB;IA0C7B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,WAAW,IAAI,MAAM;IAMrB,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,YAAY,IAAI,QAAQ,EAAE,CAI7B;IAED,IAAI,WAAW,IAAI,QAAQ,GAAG,IAAI,CAKjC;IAED,IAAI,aAAa,IAAI,SAAS,EAAE,CAO/B;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,SAAS,IAAI,OAAO,CAMvB;IAED,IAAI,QAAQ,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAGjD;IAID,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAY3C,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAyB/C,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IASpD,QAAQ,IAAI,OAAO;IAsBnB,QAAQ,IAAI,IAAI;IAQhB,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAkBlC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAezC,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IA6B1C,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAezC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAkB7C,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,yBAAyB;IAgBjC,OAAO,CAAC,OAAO;CAMhB"}
1
+ {"version":3,"file":"form-engine.d.ts","sourceRoot":"","sources":["../../src/engine/form-engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAMjD,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC1C;AAED,qBAAa,UAAU;IACrB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAE5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEtC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,MAAM,CAKZ;gBAGA,MAAM,EAAE,UAAU,EAClB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,CAAC,EAAE,iBAAiB;IA4C7B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,WAAW,IAAI,MAAM;IAMrB,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,YAAY,IAAI,QAAQ,EAAE,CAG7B;IAED,IAAI,WAAW,IAAI,QAAQ,GAAG,IAAI,CAGjC;IAED,IAAI,aAAa,IAAI,SAAS,EAAE,CAG/B;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,SAAS,IAAI,OAAO,CAGvB;IAED,IAAI,QAAQ,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAGjD;IAID,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAY3C,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IA0B/C,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IASpD,QAAQ,IAAI,OAAO;IAsBnB,QAAQ,IAAI,IAAI;IAQhB,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAkBlC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAezC,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IA6B1C,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAezC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAkB7C,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,yBAAyB;IA4BjC,OAAO,CAAC,iBAAiB;IAyBzB,OAAO,CAAC,OAAO;CAOhB"}
@@ -5,8 +5,8 @@ const MAX_CASCADE_ITERATIONS = 50;
5
5
  export class FormEngine {
6
6
  constructor(schema, initialValues, options) {
7
7
  // Validate schema structure
8
- const hasFields = schema.fields && schema.fields.length > 0;
9
- const hasSteps = schema.steps && schema.steps.length > 0;
8
+ const hasFields = (schema.fields?.length ?? 0) > 0;
9
+ const hasSteps = (schema.steps?.length ?? 0) > 0;
10
10
  if (hasFields && hasSteps) {
11
11
  throw new Error('FormSchema cannot have both "fields" and "steps" as non-empty arrays.');
12
12
  }
@@ -26,6 +26,8 @@ export class FormEngine {
26
26
  this.topLevelErrors = [];
27
27
  this.fieldLoading = {};
28
28
  this._allFields = this._computeAllFields();
29
+ this._isDirty = true;
30
+ this._cache = { visibleSteps: [], currentStep: null, visibleFields: [], canGoNext: false };
29
31
  // Initialize values with defaults, then overlay initialValues
30
32
  this.values = {};
31
33
  for (const field of this._allFields) {
@@ -52,26 +54,16 @@ export class FormEngine {
52
54
  return (this.schema.steps ?? []).length > 0;
53
55
  }
54
56
  get visibleSteps() {
55
- if (!this.isMultiStep)
56
- return [];
57
- const steps = this.schema.steps ?? [];
58
- return steps.filter((s) => isStepVisible(s, this.values));
57
+ this._recomputeIfDirty();
58
+ return this._cache.visibleSteps;
59
59
  }
60
60
  get currentStep() {
61
- const steps = this.visibleSteps;
62
- if (steps.length === 0)
63
- return null;
64
- // Find the step at currentStepIndex within visible steps
65
- return steps[this.currentStepIndex] ?? steps[steps.length - 1] ?? null;
61
+ this._recomputeIfDirty();
62
+ return this._cache.currentStep;
66
63
  }
67
64
  get visibleFields() {
68
- if (this.isMultiStep) {
69
- const step = this.currentStep;
70
- if (!step)
71
- return [];
72
- return step.fields.filter((f) => isVisible(f, this.values));
73
- }
74
- return (this.schema.fields ?? []).filter((f) => isVisible(f, this.values));
65
+ this._recomputeIfDirty();
66
+ return this._cache.visibleFields;
75
67
  }
76
68
  get isFirstStep() {
77
69
  return this.currentStepIndex === 0;
@@ -80,13 +72,8 @@ export class FormEngine {
80
72
  return this.currentStepIndex >= this.visibleSteps.length - 1;
81
73
  }
82
74
  get canGoNext() {
83
- if (!this.isMultiStep)
84
- return false;
85
- const step = this.currentStep;
86
- if (!step)
87
- return false;
88
- const stepErrors = validateStep(step, this.values, this._validators);
89
- return Object.keys(stepErrors).length === 0;
75
+ this._recomputeIfDirty();
76
+ return this._cache.canGoNext;
90
77
  }
91
78
  get progress() {
92
79
  const total = this.visibleSteps.length;
@@ -102,6 +89,7 @@ export class FormEngine {
102
89
  this._notify();
103
90
  }
104
91
  setErrors(errors) {
92
+ this.errors = {};
105
93
  this.topLevelErrors = [];
106
94
  for (const [key, message] of Object.entries(errors)) {
107
95
  // Check if field is visible
@@ -256,10 +244,21 @@ export class FormEngine {
256
244
  return this.schema.fields ?? [];
257
245
  }
258
246
  _cascadeClearHiddenFields() {
247
+ const hiddenStepFields = new Set();
248
+ if (this.isMultiStep) {
249
+ for (const step of this.schema.steps ?? []) {
250
+ if (!isStepVisible(step, this.values)) {
251
+ for (const field of step.fields) {
252
+ hiddenStepFields.add(field.key);
253
+ }
254
+ }
255
+ }
256
+ }
259
257
  for (let i = 0; i < MAX_CASCADE_ITERATIONS; i++) {
260
258
  let changed = false;
261
259
  for (const field of this._allFields) {
262
- if (!isVisible(field, this.values) && this.values[field.key] !== undefined) {
260
+ const fieldHidden = !isVisible(field, this.values) || hiddenStepFields.has(field.key);
261
+ if (fieldHidden && this.values[field.key] !== undefined) {
263
262
  delete this.values[field.key];
264
263
  delete this.errors[field.key];
265
264
  changed = true;
@@ -269,7 +268,28 @@ export class FormEngine {
269
268
  break;
270
269
  }
271
270
  }
271
+ _recomputeIfDirty() {
272
+ if (!this._isDirty)
273
+ return;
274
+ this._isDirty = false;
275
+ if (!this.isMultiStep) {
276
+ this._cache.visibleSteps = [];
277
+ this._cache.currentStep = null;
278
+ this._cache.visibleFields = (this.schema.fields ?? []).filter((f) => isVisible(f, this.values));
279
+ this._cache.canGoNext = false;
280
+ return;
281
+ }
282
+ const steps = this.schema.steps ?? [];
283
+ this._cache.visibleSteps = steps.filter((s) => isStepVisible(s, this.values));
284
+ const vs = this._cache.visibleSteps;
285
+ this._cache.currentStep = vs[this.currentStepIndex] ?? vs[vs.length - 1] ?? null;
286
+ const step = this._cache.currentStep;
287
+ this._cache.visibleFields = step ? step.fields.filter((f) => isVisible(f, this.values)) : [];
288
+ const stepErrors = step ? validateStep(step, this.values, this._validators) : {};
289
+ this._cache.canGoNext = Object.keys(stepErrors).length === 0;
290
+ }
272
291
  _notify() {
292
+ this._isDirty = true;
273
293
  this._version++;
274
294
  for (const listener of this._listeners) {
275
295
  listener();
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { FieldOption, FieldType, FieldValidation, FormAction, FormAnalyticsEvent, FormField, FormSchema, FormStep, ShowCondition, } from './types';
1
+ export type { DefaultFieldType, FieldOption, FieldType, FieldValidation, FormAction, FormAnalyticsEvent, FormField, FormSchema, FormStep, ShowCondition, } from './types';
2
2
  export { FormEngine, type FormEngineOptions } from './engine';
3
3
  export { evaluateCondition, isStepVisible, isVisible } from './visibility';
4
4
  export { type ValidatorFn, getDefaultMessage, validateField, validateFields, validateStep, } from './validation';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,SAAS,EACT,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,SAAS,EACT,UAAU,EACV,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG3E,OAAO,EACL,KAAK,WAAW,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,YAAY,GACb,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,SAAS,EACT,UAAU,EACV,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG3E,OAAO,EACL,KAAK,WAAW,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,YAAY,GACb,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"validate-schema.d.ts","sourceRoot":"","sources":["../../src/schema-validation/validate-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,UAAU,EAAiB,MAAM,UAAU,CAAC;AA+CrE,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAwC3D"}
1
+ {"version":3,"file":"validate-schema.d.ts","sourceRoot":"","sources":["../../src/schema-validation/validate-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,UAAU,EAAiB,MAAM,UAAU,CAAC;AA+CrE,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAqD3D"}
@@ -4,7 +4,7 @@ function extractDependencies(conditions) {
4
4
  return conditions.map((c) => c.field);
5
5
  }
6
6
  function getAllFields(schema) {
7
- if (schema.steps && schema.steps.length > 0) {
7
+ if ((schema.steps?.length ?? 0) > 0) {
8
8
  return schema.steps.flatMap((s) => s.fields);
9
9
  }
10
10
  return schema.fields ?? [];
@@ -38,29 +38,39 @@ function detectCycles(graph) {
38
38
  }
39
39
  export function validateSchema(schema) {
40
40
  const warnings = [];
41
- // Check fields/steps mutual exclusivity
42
- const hasFields = schema.fields && schema.fields.length > 0;
43
- const hasSteps = schema.steps && schema.steps.length > 0;
41
+ const hasFields = (schema.fields?.length ?? 0) > 0;
42
+ const hasSteps = (schema.steps?.length ?? 0) > 0;
44
43
  if (hasFields && hasSteps) {
45
44
  warnings.push('Schema has both "fields" and "steps". Only "steps" will be used.');
46
45
  }
47
- // Build dependency graph from show conditions
48
46
  const allFields = getAllFields(schema);
49
- const fieldKeys = new Set(allFields.map((f) => f.key));
47
+ const fieldKeys = new Set();
50
48
  const graph = new Map();
49
+ for (const field of allFields) {
50
+ if (fieldKeys.has(field.key)) {
51
+ warnings.push(`Duplicate field key "${field.key}" — fields will share state and overwrite each other`);
52
+ }
53
+ fieldKeys.add(field.key);
54
+ }
51
55
  for (const field of allFields) {
52
56
  const deps = [...extractDependencies(field.show), ...extractDependencies(field.showAny)];
53
- // Warn about references to non-existent fields
54
57
  for (const dep of deps) {
55
58
  if (!fieldKeys.has(dep)) {
56
59
  warnings.push(`Field "${field.key}" has show condition referencing non-existent field "${dep}"`);
57
60
  }
58
61
  }
62
+ if (field.validation?.pattern !== undefined) {
63
+ try {
64
+ new RegExp(field.validation.pattern);
65
+ }
66
+ catch {
67
+ warnings.push(`Field "${field.key}" has invalid regex pattern: "${field.validation.pattern}"`);
68
+ }
69
+ }
59
70
  if (deps.length > 0) {
60
71
  graph.set(field.key, deps);
61
72
  }
62
73
  }
63
- // Detect cycles
64
74
  const cycles = detectCycles(graph);
65
75
  for (const cycle of cycles) {
66
76
  warnings.push(`Circular show condition detected: ${cycle.join(' -> ')}`);
@@ -1,4 +1,5 @@
1
- export type FieldType = 'text' | 'email' | 'phone' | 'number' | 'password' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'switch' | 'file' | 'date' | 'textarea';
1
+ export type DefaultFieldType = 'text' | 'email' | 'phone' | 'number' | 'password' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'switch' | 'file' | 'date' | 'textarea';
2
+ export type FieldType = DefaultFieldType | (string & {});
2
3
  export interface FieldOption {
3
4
  value: string;
4
5
  label: string;
@@ -1 +1 @@
1
- {"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../src/types/field.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,OAAO,GACP,OAAO,GACP,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,aAAa,GACb,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,MAAM,GACN,UAAU,CAAC;AAEf,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,aAAa,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAChC,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../src/types/field.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,OAAO,GACP,OAAO,GACP,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,aAAa,GACb,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,MAAM,GACN,UAAU,CAAC;AAEf,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,aAAa,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAChC,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
@@ -1,5 +1,5 @@
1
1
  export type { FormAction } from './action';
2
2
  export type { FormAnalyticsEvent } from './analytics';
3
- export type { FieldOption, FieldType, FieldValidation, FormField, ShowCondition, } from './field';
3
+ export type { DefaultFieldType, FieldOption, FieldType, FieldValidation, FormField, ShowCondition, } from './field';
4
4
  export type { FormSchema, FormStep } from './schema';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EACV,WAAW,EACX,SAAS,EACT,eAAe,EACf,SAAS,EACT,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,eAAe,EACf,SAAS,EACT,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
@@ -3,9 +3,9 @@ export function getDefaultMessage(rule, params) {
3
3
  case 'required':
4
4
  return 'This field is required';
5
5
  case 'minLength':
6
- return `Must be at least ${params?.min} characters`;
6
+ return `Must be at least ${params?.min} ${params?.unit ?? 'characters'}`;
7
7
  case 'maxLength':
8
- return `Must be at most ${params?.max} characters`;
8
+ return `Must be at most ${params?.max} ${params?.unit ?? 'characters'}`;
9
9
  case 'min':
10
10
  return `Must be at least ${params?.min}`;
11
11
  case 'max':
@@ -1 +1 @@
1
- {"version":3,"file":"validate-field.d.ts","sourceRoot":"","sources":["../../src/validation/validate-field.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,UAAU,CAAC;AAG3D,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,GAAG,IAAI,CAAC;AAMhG,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACvC,MAAM,GAAG,IAAI,CA+Df"}
1
+ {"version":3,"file":"validate-field.d.ts","sourceRoot":"","sources":["../../src/validation/validate-field.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,UAAU,CAAC;AAG3D,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,GAAG,IAAI,CAAC;AAQhG,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACvC,MAAM,GAAG,IAAI,CAiEf"}
@@ -1,6 +1,10 @@
1
1
  import { getDefaultMessage } from './default-messages';
2
2
  function isEmpty(value) {
3
- return value === undefined || value === null || value === '';
3
+ if (value === undefined || value === null || value === '')
4
+ return true;
5
+ if (Array.isArray(value) && value.length === 0)
6
+ return true;
7
+ return false;
4
8
  }
5
9
  export function validateField(field, value, allValues, validators) {
6
10
  const rules = field.validation;
@@ -14,14 +18,16 @@ export function validateField(field, value, allValues, validators) {
14
18
  // Skip remaining rules if value is empty and not required
15
19
  if (isEmpty(value))
16
20
  return null;
17
- if (rules.minLength !== undefined && typeof value === 'string') {
21
+ if (rules.minLength !== undefined && (typeof value === 'string' || Array.isArray(value))) {
18
22
  if (value.length < rules.minLength) {
19
- return rules.minLengthMessage ?? getDefaultMessage('minLength', { min: rules.minLength });
23
+ const unit = Array.isArray(value) ? 'items' : undefined;
24
+ return rules.minLengthMessage ?? getDefaultMessage('minLength', { min: rules.minLength, unit });
20
25
  }
21
26
  }
22
- if (rules.maxLength !== undefined && typeof value === 'string') {
27
+ if (rules.maxLength !== undefined && (typeof value === 'string' || Array.isArray(value))) {
23
28
  if (value.length > rules.maxLength) {
24
- return rules.maxLengthMessage ?? getDefaultMessage('maxLength', { max: rules.maxLength });
29
+ const unit = Array.isArray(value) ? 'items' : undefined;
30
+ return rules.maxLengthMessage ?? getDefaultMessage('maxLength', { max: rules.maxLength, unit });
25
31
  }
26
32
  }
27
33
  if (rules.min !== undefined && typeof value === 'number') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formhaus/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Framework-agnostic JSON-schema-driven form engine with validation, conditional visibility, and multi-step support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",