@coherent.js/forms 1.0.0-beta.2
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/LICENSE +21 -0
- package/dist/index.js +1860 -0
- package/package.json +45 -0
- package/types/index.d.ts +134 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
// src/form-builder.js
|
|
2
|
+
var FormBuilder = class {
|
|
3
|
+
constructor(options = {}) {
|
|
4
|
+
this.options = {
|
|
5
|
+
validateOnChange: true,
|
|
6
|
+
validateOnBlur: true,
|
|
7
|
+
name: options.name || "form",
|
|
8
|
+
...options
|
|
9
|
+
};
|
|
10
|
+
this.fields = /* @__PURE__ */ new Map();
|
|
11
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
12
|
+
this.values = {};
|
|
13
|
+
this.errors = {};
|
|
14
|
+
this.touched = {};
|
|
15
|
+
this.initialValues = {};
|
|
16
|
+
this.submitHandler = null;
|
|
17
|
+
this.errorHandler = null;
|
|
18
|
+
this._isSubmitting = false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Add a field to the form (alias for field)
|
|
22
|
+
*/
|
|
23
|
+
addField(name, config = {}) {
|
|
24
|
+
return this.field(name, config);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Add a field to the form
|
|
28
|
+
*/
|
|
29
|
+
field(name, config = {}) {
|
|
30
|
+
const fieldConfig = {
|
|
31
|
+
name,
|
|
32
|
+
type: config.type || "text",
|
|
33
|
+
label: config.label || name,
|
|
34
|
+
placeholder: config.placeholder || "",
|
|
35
|
+
defaultValue: config.defaultValue || "",
|
|
36
|
+
validators: config.validators || [],
|
|
37
|
+
required: config.required || false,
|
|
38
|
+
visible: config.visible !== false,
|
|
39
|
+
showWhen: config.showWhen,
|
|
40
|
+
...config
|
|
41
|
+
};
|
|
42
|
+
this.fields.set(name, fieldConfig);
|
|
43
|
+
if (config.defaultValue !== void 0) {
|
|
44
|
+
this.values[name] = config.defaultValue;
|
|
45
|
+
this.initialValues[name] = config.defaultValue;
|
|
46
|
+
}
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Remove a field from the form
|
|
51
|
+
*/
|
|
52
|
+
removeField(name) {
|
|
53
|
+
this.fields.delete(name);
|
|
54
|
+
delete this.values[name];
|
|
55
|
+
delete this.errors[name];
|
|
56
|
+
delete this.touched[name];
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Update field configuration
|
|
61
|
+
*/
|
|
62
|
+
updateField(name, config) {
|
|
63
|
+
const field = this.fields.get(name);
|
|
64
|
+
if (field) {
|
|
65
|
+
this.fields.set(name, { ...field, ...config });
|
|
66
|
+
}
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get all fields as array
|
|
71
|
+
*/
|
|
72
|
+
getFields() {
|
|
73
|
+
return Array.from(this.fields.values());
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Add a field group
|
|
77
|
+
*/
|
|
78
|
+
addGroup(name, config = {}) {
|
|
79
|
+
this.groups.set(name, {
|
|
80
|
+
name,
|
|
81
|
+
label: config.label || name,
|
|
82
|
+
fields: config.fields || [],
|
|
83
|
+
...config
|
|
84
|
+
});
|
|
85
|
+
if (config.fields) {
|
|
86
|
+
config.fields.forEach((fieldConfig) => {
|
|
87
|
+
this.addField(fieldConfig.name, fieldConfig);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get field configuration
|
|
94
|
+
*/
|
|
95
|
+
getField(name) {
|
|
96
|
+
return this.fields.get(name);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Set field value
|
|
100
|
+
*/
|
|
101
|
+
setValue(name, value) {
|
|
102
|
+
this.values[name] = value;
|
|
103
|
+
this.touched[name] = true;
|
|
104
|
+
const field = this.fields.get(name);
|
|
105
|
+
if (field && (field.validateOnChange || this.options.validateOnChange)) {
|
|
106
|
+
this.validateField(name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Set multiple values
|
|
111
|
+
*/
|
|
112
|
+
setValues(values) {
|
|
113
|
+
Object.assign(this.values, values);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get field value
|
|
118
|
+
*/
|
|
119
|
+
getValue(name) {
|
|
120
|
+
return this.values[name];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get all values
|
|
124
|
+
*/
|
|
125
|
+
getValues() {
|
|
126
|
+
return { ...this.values };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get field error
|
|
130
|
+
*/
|
|
131
|
+
getFieldError(name) {
|
|
132
|
+
return this.errors[name];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if form has errors
|
|
136
|
+
*/
|
|
137
|
+
hasErrors() {
|
|
138
|
+
return Object.keys(this.errors).length > 0;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Clear all errors
|
|
142
|
+
*/
|
|
143
|
+
clearErrors() {
|
|
144
|
+
this.errors = {};
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if form is dirty (values changed from initial)
|
|
149
|
+
*/
|
|
150
|
+
isDirty() {
|
|
151
|
+
return Object.keys(this.values).some((key) => {
|
|
152
|
+
return this.values[key] !== this.initialValues[key];
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if form is valid
|
|
157
|
+
*/
|
|
158
|
+
isValid() {
|
|
159
|
+
const result = this.validate();
|
|
160
|
+
return Object.keys(result).length === 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Validate a field
|
|
164
|
+
*/
|
|
165
|
+
validateField(name) {
|
|
166
|
+
const field = this.fields.get(name);
|
|
167
|
+
if (!field) return null;
|
|
168
|
+
const showCondition = field.showWhen || field.showIf;
|
|
169
|
+
if (showCondition && !showCondition(this.values)) {
|
|
170
|
+
delete this.errors[name];
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const value = this.values[name];
|
|
174
|
+
if (field.required && (value === void 0 || value === null || value === "")) {
|
|
175
|
+
const error = "This field is required";
|
|
176
|
+
this.errors[name] = error;
|
|
177
|
+
return error;
|
|
178
|
+
}
|
|
179
|
+
if (!value && !field.required) {
|
|
180
|
+
delete this.errors[name];
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (value) {
|
|
184
|
+
if (field.type === "email") {
|
|
185
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
186
|
+
if (!emailRegex.test(value)) {
|
|
187
|
+
const error = "Please enter a valid email address";
|
|
188
|
+
this.errors[name] = error;
|
|
189
|
+
return error;
|
|
190
|
+
}
|
|
191
|
+
} else if (field.type === "url") {
|
|
192
|
+
try {
|
|
193
|
+
new URL(value);
|
|
194
|
+
} catch {
|
|
195
|
+
const error = "Please enter a valid URL";
|
|
196
|
+
this.errors[name] = error;
|
|
197
|
+
return error;
|
|
198
|
+
}
|
|
199
|
+
} else if (field.type === "number") {
|
|
200
|
+
if (isNaN(Number(value))) {
|
|
201
|
+
const error = "Please enter a valid number";
|
|
202
|
+
this.errors[name] = error;
|
|
203
|
+
return error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (field.validate) {
|
|
208
|
+
const error = field.validate(value, this.values);
|
|
209
|
+
if (error) {
|
|
210
|
+
this.errors[name] = error;
|
|
211
|
+
return error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const validator of field.validators || []) {
|
|
215
|
+
const error = validator(value, this.values);
|
|
216
|
+
if (error) {
|
|
217
|
+
this.errors[name] = error;
|
|
218
|
+
return error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
delete this.errors[name];
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Validate all fields
|
|
226
|
+
*/
|
|
227
|
+
validate() {
|
|
228
|
+
const errors = {};
|
|
229
|
+
for (const [name, field] of this.fields) {
|
|
230
|
+
const showCondition = field.showWhen || field.showIf;
|
|
231
|
+
if (showCondition && !showCondition(this.values)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const error = this.validateField(name);
|
|
235
|
+
if (error) {
|
|
236
|
+
errors[name] = error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.errors = errors;
|
|
240
|
+
return errors;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Set submit handler
|
|
244
|
+
*/
|
|
245
|
+
onSubmit(handler) {
|
|
246
|
+
this.submitHandler = handler;
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Set error handler
|
|
251
|
+
*/
|
|
252
|
+
onError(handler) {
|
|
253
|
+
this.errorHandler = handler;
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Submit the form
|
|
258
|
+
*/
|
|
259
|
+
async submit() {
|
|
260
|
+
const errors = this.validate();
|
|
261
|
+
if (Object.keys(errors).length > 0) {
|
|
262
|
+
return { success: false, errors };
|
|
263
|
+
}
|
|
264
|
+
if (!this.submitHandler) {
|
|
265
|
+
return { success: true, data: this.values };
|
|
266
|
+
}
|
|
267
|
+
this._isSubmitting = true;
|
|
268
|
+
try {
|
|
269
|
+
const result = await this.submitHandler(this.values);
|
|
270
|
+
this._isSubmitting = false;
|
|
271
|
+
return { success: true, data: result };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
this._isSubmitting = false;
|
|
274
|
+
if (this.errorHandler) {
|
|
275
|
+
this.errorHandler(error);
|
|
276
|
+
}
|
|
277
|
+
return { success: false, error };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Serialize form data
|
|
282
|
+
*/
|
|
283
|
+
serialize() {
|
|
284
|
+
return { ...this.values };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Convert form to HTML string
|
|
288
|
+
*/
|
|
289
|
+
toHTML() {
|
|
290
|
+
const fields = this.getFields();
|
|
291
|
+
let html = `<form name="${this.options.name}">`;
|
|
292
|
+
fields.forEach((field) => {
|
|
293
|
+
html += `<div class="form-field">`;
|
|
294
|
+
html += `<label for="${field.name}">${field.label}</label>`;
|
|
295
|
+
html += `<input type="${field.type}" name="${field.name}" id="${field.name}">`;
|
|
296
|
+
html += `</div>`;
|
|
297
|
+
});
|
|
298
|
+
html += `</form>`;
|
|
299
|
+
return html;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Mark field as touched
|
|
303
|
+
*/
|
|
304
|
+
touch(name) {
|
|
305
|
+
this.touched[name] = true;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Build input component with validation metadata for hydration
|
|
309
|
+
*/
|
|
310
|
+
buildInput(name) {
|
|
311
|
+
const field = this.fields.get(name);
|
|
312
|
+
if (!field) return null;
|
|
313
|
+
const value = this.values[name] || "";
|
|
314
|
+
const error = this.errors[name];
|
|
315
|
+
const isTouched = this.touched[name];
|
|
316
|
+
const validatorNames = field.validators.map((v) => {
|
|
317
|
+
if (typeof v === "function") return v.name || "custom";
|
|
318
|
+
if (typeof v === "string") return v;
|
|
319
|
+
return null;
|
|
320
|
+
}).filter(Boolean).join(",");
|
|
321
|
+
const inputProps = {
|
|
322
|
+
type: field.type,
|
|
323
|
+
name: field.name,
|
|
324
|
+
id: field.name,
|
|
325
|
+
value,
|
|
326
|
+
placeholder: field.placeholder,
|
|
327
|
+
"aria-invalid": error ? "true" : "false",
|
|
328
|
+
"aria-describedby": error ? `${name}-error` : void 0,
|
|
329
|
+
className: error && isTouched ? "error" : ""
|
|
330
|
+
};
|
|
331
|
+
if (field.required) {
|
|
332
|
+
inputProps.required = true;
|
|
333
|
+
inputProps["data-required"] = "true";
|
|
334
|
+
}
|
|
335
|
+
if (validatorNames) {
|
|
336
|
+
inputProps["data-validators"] = validatorNames;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
input: inputProps
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Build label component
|
|
344
|
+
*/
|
|
345
|
+
buildLabel(name) {
|
|
346
|
+
const field = this.fields.get(name);
|
|
347
|
+
if (!field) return null;
|
|
348
|
+
return {
|
|
349
|
+
label: {
|
|
350
|
+
for: field.name,
|
|
351
|
+
text: field.label
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Build error component
|
|
357
|
+
*/
|
|
358
|
+
buildError(name) {
|
|
359
|
+
const error = this.errors[name];
|
|
360
|
+
const isTouched = this.touched[name];
|
|
361
|
+
if (!error || !isTouched) return null;
|
|
362
|
+
return {
|
|
363
|
+
div: {
|
|
364
|
+
id: `${name}-error`,
|
|
365
|
+
className: "error-message",
|
|
366
|
+
role: "alert",
|
|
367
|
+
text: error
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Build complete field component
|
|
373
|
+
*/
|
|
374
|
+
buildField(name) {
|
|
375
|
+
const field = this.fields.get(name);
|
|
376
|
+
if (!field) return null;
|
|
377
|
+
const children = [
|
|
378
|
+
this.buildLabel(name),
|
|
379
|
+
this.buildInput(name)
|
|
380
|
+
];
|
|
381
|
+
const error = this.buildError(name);
|
|
382
|
+
if (error) {
|
|
383
|
+
children.push(error);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
div: {
|
|
387
|
+
className: "form-field",
|
|
388
|
+
"data-field": name,
|
|
389
|
+
children
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Build entire form
|
|
395
|
+
*/
|
|
396
|
+
buildForm(options = {}) {
|
|
397
|
+
const fields = [];
|
|
398
|
+
for (const [name] of this.fields) {
|
|
399
|
+
fields.push(this.buildField(name));
|
|
400
|
+
}
|
|
401
|
+
if (options.submitButton !== false) {
|
|
402
|
+
fields.push({
|
|
403
|
+
button: {
|
|
404
|
+
type: "submit",
|
|
405
|
+
text: options.submitText || "Submit",
|
|
406
|
+
className: "submit-button"
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
form: {
|
|
412
|
+
onsubmit: "handleSubmit(event)",
|
|
413
|
+
novalidate: true,
|
|
414
|
+
children: fields
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if form is currently submitting
|
|
420
|
+
*/
|
|
421
|
+
isSubmitting() {
|
|
422
|
+
return this._isSubmitting;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get a field group
|
|
426
|
+
*/
|
|
427
|
+
getGroup(name) {
|
|
428
|
+
return this.groups.get(name);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Check if a field is visible
|
|
432
|
+
*/
|
|
433
|
+
isFieldVisible(name) {
|
|
434
|
+
const field = this.fields.get(name);
|
|
435
|
+
if (!field) return false;
|
|
436
|
+
const showCondition = field.showWhen || field.showIf;
|
|
437
|
+
if (showCondition) {
|
|
438
|
+
return showCondition(this.values);
|
|
439
|
+
}
|
|
440
|
+
return field.visible !== false;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Reset form
|
|
444
|
+
*/
|
|
445
|
+
reset() {
|
|
446
|
+
this.values = {};
|
|
447
|
+
for (const [name, field] of this.fields) {
|
|
448
|
+
if (field.defaultValue !== void 0) {
|
|
449
|
+
this.values[name] = field.defaultValue;
|
|
450
|
+
} else {
|
|
451
|
+
this.values[name] = "";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
this.errors = {};
|
|
455
|
+
this.touched = {};
|
|
456
|
+
this._isSubmitting = false;
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function createFormBuilder(options = {}) {
|
|
461
|
+
return new FormBuilder(options);
|
|
462
|
+
}
|
|
463
|
+
function buildForm(fields, options = {}) {
|
|
464
|
+
const builder = new FormBuilder(options);
|
|
465
|
+
for (const [name, config] of Object.entries(fields)) {
|
|
466
|
+
builder.field(name, config);
|
|
467
|
+
}
|
|
468
|
+
return builder;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/validators.js
|
|
472
|
+
var validators = {
|
|
473
|
+
required: (value, options = {}) => {
|
|
474
|
+
if (value === null || value === void 0 || value === "") {
|
|
475
|
+
return options.message || validators.required.message || "This field is required";
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
},
|
|
479
|
+
email: (value) => {
|
|
480
|
+
if (!value) return null;
|
|
481
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
482
|
+
if (!emailRegex.test(value)) {
|
|
483
|
+
return "Please enter a valid email address";
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
},
|
|
487
|
+
minLength: (value, options = {}) => {
|
|
488
|
+
if (!value) return null;
|
|
489
|
+
const min = options.min || 0;
|
|
490
|
+
if (value.length < min) {
|
|
491
|
+
return options.message || `Must be at least ${min} characters`;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
},
|
|
495
|
+
maxLength: (value, options = {}) => {
|
|
496
|
+
if (!value) return null;
|
|
497
|
+
const max = options.max || Infinity;
|
|
498
|
+
if (value.length > max) {
|
|
499
|
+
return options.message || `Must be no more than ${max} characters`;
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
},
|
|
503
|
+
min: (value, options = {}) => {
|
|
504
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
505
|
+
const num = Number(value);
|
|
506
|
+
const minValue = options.min || 0;
|
|
507
|
+
if (isNaN(num) || num < minValue) {
|
|
508
|
+
return options.message || `Must be at least ${minValue}`;
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
},
|
|
512
|
+
max: (value, options = {}) => {
|
|
513
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
514
|
+
const num = Number(value);
|
|
515
|
+
const maxValue = options.max || Infinity;
|
|
516
|
+
if (isNaN(num) || num > maxValue) {
|
|
517
|
+
return options.message || `Must be no more than ${maxValue}`;
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
},
|
|
521
|
+
pattern: (value, options = {}) => {
|
|
522
|
+
if (!value) return null;
|
|
523
|
+
const regex = options.pattern || options.regex;
|
|
524
|
+
if (regex && !regex.test(value)) {
|
|
525
|
+
return options.message || "Invalid format";
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
},
|
|
529
|
+
url: (value) => {
|
|
530
|
+
if (!value) return null;
|
|
531
|
+
try {
|
|
532
|
+
new URL(value);
|
|
533
|
+
return null;
|
|
534
|
+
} catch {
|
|
535
|
+
return "Please enter a valid URL";
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
number: (value) => {
|
|
539
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
540
|
+
if (isNaN(Number(value))) {
|
|
541
|
+
return "Must be a valid number";
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
},
|
|
545
|
+
integer: (value) => {
|
|
546
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
547
|
+
const num = Number(value);
|
|
548
|
+
if (isNaN(num) || !Number.isInteger(num)) {
|
|
549
|
+
return "Must be a whole number";
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
},
|
|
553
|
+
phone: (value) => {
|
|
554
|
+
if (!value) return null;
|
|
555
|
+
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
|
556
|
+
if (!phoneRegex.test(value) || value.replace(/\D/g, "").length < 10) {
|
|
557
|
+
return "Please enter a valid phone number";
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
},
|
|
561
|
+
date: (value) => {
|
|
562
|
+
if (!value) return null;
|
|
563
|
+
const date = new Date(value);
|
|
564
|
+
if (isNaN(date.getTime())) {
|
|
565
|
+
return "Please enter a valid date";
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
},
|
|
569
|
+
match: (value, options = {}, translator, allValues = {}) => {
|
|
570
|
+
if (!value) return null;
|
|
571
|
+
const fieldName = options.field || options.fieldName;
|
|
572
|
+
if (value !== allValues[fieldName]) {
|
|
573
|
+
return options.message || `Must match ${fieldName}`;
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
},
|
|
577
|
+
custom: (value, options = {}, translator, allValues) => {
|
|
578
|
+
const validatorFn = options.validator || options.fn;
|
|
579
|
+
if (!validatorFn) return null;
|
|
580
|
+
const isValid = validatorFn(value, allValues);
|
|
581
|
+
return isValid ? null : options.message || "Validation failed";
|
|
582
|
+
},
|
|
583
|
+
fileType: (value, options = {}) => {
|
|
584
|
+
if (!value) return null;
|
|
585
|
+
const allowedTypes = options.accept || options.types || [];
|
|
586
|
+
if (value.type !== void 0) {
|
|
587
|
+
const fileType = value.type;
|
|
588
|
+
const fileExt = value.name ? value.name.split(".").pop().toLowerCase() : "";
|
|
589
|
+
const isValid = allowedTypes.some((type) => {
|
|
590
|
+
if (type.startsWith(".")) {
|
|
591
|
+
return fileExt === type.slice(1).toLowerCase();
|
|
592
|
+
}
|
|
593
|
+
if (type.includes("/")) {
|
|
594
|
+
if (type.endsWith("/*")) {
|
|
595
|
+
return fileType.startsWith(type.replace("/*", "/"));
|
|
596
|
+
}
|
|
597
|
+
return fileType === type;
|
|
598
|
+
}
|
|
599
|
+
return fileExt === type.toLowerCase();
|
|
600
|
+
});
|
|
601
|
+
if (!isValid) {
|
|
602
|
+
return options.message || `File type must be one of: ${allowedTypes.join(", ")}`;
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
},
|
|
608
|
+
fileSize: (value, options = {}) => {
|
|
609
|
+
if (!value) return null;
|
|
610
|
+
const maxSize = options.maxSize || Infinity;
|
|
611
|
+
if (value.size !== void 0) {
|
|
612
|
+
if (value.size > maxSize) {
|
|
613
|
+
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
|
|
614
|
+
return options.message || `File size must be less than ${maxSizeMB}MB`;
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
},
|
|
620
|
+
fileExtension: (value, options = {}) => {
|
|
621
|
+
if (!value) return null;
|
|
622
|
+
const allowedExtensions = options.extensions || [];
|
|
623
|
+
const fileName = value.name || value;
|
|
624
|
+
const ext = `.${fileName.split(".").pop().toLowerCase()}`;
|
|
625
|
+
const isValid = allowedExtensions.some((allowed) => {
|
|
626
|
+
return ext === allowed.toLowerCase();
|
|
627
|
+
});
|
|
628
|
+
if (!isValid) {
|
|
629
|
+
return options.message || `File extension must be one of: ${allowedExtensions.join(", ")}`;
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
},
|
|
633
|
+
alpha: (value) => {
|
|
634
|
+
if (!value) return null;
|
|
635
|
+
const alphaRegex = /^[a-zA-Z]+$/;
|
|
636
|
+
if (!alphaRegex.test(value)) {
|
|
637
|
+
return "Must contain only letters";
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
},
|
|
641
|
+
alphanumeric: (value) => {
|
|
642
|
+
if (!value) return null;
|
|
643
|
+
const alphanumericRegex = /^[a-zA-Z0-9]+$/;
|
|
644
|
+
if (!alphanumericRegex.test(value)) {
|
|
645
|
+
return "Must contain only letters and numbers";
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
},
|
|
649
|
+
uppercase: (value) => {
|
|
650
|
+
if (!value) return null;
|
|
651
|
+
if (value !== value.toUpperCase()) {
|
|
652
|
+
return "Must be uppercase";
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
},
|
|
656
|
+
// Get a registered validator
|
|
657
|
+
get: (name) => {
|
|
658
|
+
return validators[name];
|
|
659
|
+
},
|
|
660
|
+
// Compose multiple validators
|
|
661
|
+
compose: (validatorList) => {
|
|
662
|
+
return (value, options, translator, allValues) => {
|
|
663
|
+
for (const validator of validatorList) {
|
|
664
|
+
const error = typeof validator === "function" ? validator(value, options, translator, allValues) : null;
|
|
665
|
+
if (error) {
|
|
666
|
+
return error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
};
|
|
671
|
+
},
|
|
672
|
+
// Debounce async validator
|
|
673
|
+
debounce: (validator, delay = 300) => {
|
|
674
|
+
let timeoutId;
|
|
675
|
+
return (value) => {
|
|
676
|
+
return new Promise((resolve) => {
|
|
677
|
+
clearTimeout(timeoutId);
|
|
678
|
+
timeoutId = setTimeout(async () => {
|
|
679
|
+
const result = await validator(value);
|
|
680
|
+
resolve(result);
|
|
681
|
+
}, delay);
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
// Cancellable async validator
|
|
686
|
+
cancellable: (validator) => {
|
|
687
|
+
let abortController;
|
|
688
|
+
const wrapped = async (value) => {
|
|
689
|
+
if (abortController) {
|
|
690
|
+
abortController.abort();
|
|
691
|
+
}
|
|
692
|
+
abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
693
|
+
try {
|
|
694
|
+
return await validator(value, abortController ? abortController.signal : null);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (error.name === "AbortError") {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
wrapped.cancel = () => {
|
|
703
|
+
if (abortController) {
|
|
704
|
+
abortController.abort();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
return wrapped;
|
|
708
|
+
},
|
|
709
|
+
// Conditional validator
|
|
710
|
+
when: (condition, validator) => {
|
|
711
|
+
return (value, options = {}, translator, allValues = {}) => {
|
|
712
|
+
const context = options.min !== void 0 || options.max !== void 0 ? allValues : options;
|
|
713
|
+
const shouldValidate = typeof condition === "function" ? condition(value, context) : condition;
|
|
714
|
+
if (!shouldValidate) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
return typeof validator === "function" ? validator(value, options, translator, allValues) : null;
|
|
718
|
+
};
|
|
719
|
+
},
|
|
720
|
+
// Validator chain builder
|
|
721
|
+
chain: (options = {}) => {
|
|
722
|
+
const validatorList = [];
|
|
723
|
+
const stopOnFirstError = options.stopOnFirstError !== false;
|
|
724
|
+
const chain = {
|
|
725
|
+
required: (opts) => {
|
|
726
|
+
validatorList.push((v, o, t, a) => validators.required(v, opts || o, t, a));
|
|
727
|
+
return chain;
|
|
728
|
+
},
|
|
729
|
+
email: (opts) => {
|
|
730
|
+
validatorList.push((v, o, t, a) => validators.email(v, opts || o, t, a));
|
|
731
|
+
return chain;
|
|
732
|
+
},
|
|
733
|
+
minLength: (opts) => {
|
|
734
|
+
validatorList.push((v, o, t, a) => validators.minLength(v, opts || o, t, a));
|
|
735
|
+
return chain;
|
|
736
|
+
},
|
|
737
|
+
maxLength: (opts) => {
|
|
738
|
+
validatorList.push((v, o, t, a) => validators.maxLength(v, opts || o, t, a));
|
|
739
|
+
return chain;
|
|
740
|
+
},
|
|
741
|
+
custom: (fn, message) => {
|
|
742
|
+
validatorList.push((v, o, t, a) => {
|
|
743
|
+
const result = fn(v, a);
|
|
744
|
+
return result === null || result === true || result === void 0 ? null : message || result;
|
|
745
|
+
});
|
|
746
|
+
return chain;
|
|
747
|
+
},
|
|
748
|
+
validate: (value, opts, translator, allValues) => {
|
|
749
|
+
if (stopOnFirstError) {
|
|
750
|
+
for (const validator of validatorList) {
|
|
751
|
+
const error = validator(value, opts, translator, allValues);
|
|
752
|
+
if (error) {
|
|
753
|
+
return error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
} else {
|
|
758
|
+
const errors = [];
|
|
759
|
+
for (const validator of validatorList) {
|
|
760
|
+
const error = validator(value, opts, translator, allValues);
|
|
761
|
+
if (error) {
|
|
762
|
+
errors.push(error);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return errors.length > 0 ? errors : null;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
return chain;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
function validateField(value, validatorList, formData = {}) {
|
|
773
|
+
for (const validator of validatorList) {
|
|
774
|
+
const error = validator(value, formData);
|
|
775
|
+
if (error) {
|
|
776
|
+
return error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
function validateForm(formData, fieldValidators) {
|
|
782
|
+
const errors = {};
|
|
783
|
+
for (const [fieldName, validatorList] of Object.entries(fieldValidators)) {
|
|
784
|
+
const value = formData[fieldName];
|
|
785
|
+
const error = validateField(value, validatorList, formData);
|
|
786
|
+
if (error) {
|
|
787
|
+
errors[fieldName] = error;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
791
|
+
}
|
|
792
|
+
function registerValidator(name, validatorFn) {
|
|
793
|
+
validators[name] = validatorFn;
|
|
794
|
+
}
|
|
795
|
+
function composeValidators(...validatorFns) {
|
|
796
|
+
return (value, options, translator, allValues) => {
|
|
797
|
+
for (const validator of validatorFns) {
|
|
798
|
+
const error = validator(value, options, translator, allValues);
|
|
799
|
+
if (error) {
|
|
800
|
+
return error;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/form-hydration.js
|
|
808
|
+
function hydrateForm(formSelector, options = {}) {
|
|
809
|
+
if (typeof document === "undefined") {
|
|
810
|
+
console.warn("hydrateForm can only run in browser environment");
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
const form = typeof formSelector === "string" ? document.querySelector(formSelector) : formSelector;
|
|
814
|
+
if (!form) {
|
|
815
|
+
console.warn(`Form not found: ${formSelector}`);
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
const opts = {
|
|
819
|
+
validateOnBlur: true,
|
|
820
|
+
validateOnChange: false,
|
|
821
|
+
validateOnSubmit: true,
|
|
822
|
+
showErrorsOnTouch: true,
|
|
823
|
+
debounce: 300,
|
|
824
|
+
...options
|
|
825
|
+
};
|
|
826
|
+
const state = {
|
|
827
|
+
values: {},
|
|
828
|
+
errors: {},
|
|
829
|
+
touched: {},
|
|
830
|
+
isSubmitting: false,
|
|
831
|
+
fields: /* @__PURE__ */ new Map()
|
|
832
|
+
};
|
|
833
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
834
|
+
function parseValidators(validatorString) {
|
|
835
|
+
if (!validatorString) return [];
|
|
836
|
+
return validatorString.split(",").map((v) => {
|
|
837
|
+
const trimmed = v.trim();
|
|
838
|
+
const [name, ...params] = trimmed.split(":");
|
|
839
|
+
if (validators[name]) {
|
|
840
|
+
return params.length > 0 ? validators[name](...params.map((p) => isNaN(p) ? p : Number(p))) : validators[name];
|
|
841
|
+
}
|
|
842
|
+
return null;
|
|
843
|
+
}).filter(Boolean);
|
|
844
|
+
}
|
|
845
|
+
function discoverFields() {
|
|
846
|
+
const inputs = form.querySelectorAll("[name]");
|
|
847
|
+
inputs.forEach((input) => {
|
|
848
|
+
const name = input.getAttribute("name");
|
|
849
|
+
const field = {
|
|
850
|
+
name,
|
|
851
|
+
element: input,
|
|
852
|
+
type: input.getAttribute("type") || "text",
|
|
853
|
+
required: input.hasAttribute("required") || input.dataset.required === "true",
|
|
854
|
+
validators: parseValidators(input.dataset.validators),
|
|
855
|
+
errorElement: null
|
|
856
|
+
};
|
|
857
|
+
const errorId = `${name}-error`;
|
|
858
|
+
field.errorElement = document.getElementById(errorId) || createErrorElement(name, input);
|
|
859
|
+
state.fields.set(name, field);
|
|
860
|
+
state.values[name] = getFieldValue(input);
|
|
861
|
+
state.touched[name] = false;
|
|
862
|
+
state.errors[name] = null;
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function createErrorElement(name, inputElement) {
|
|
866
|
+
const errorDiv = document.createElement("div");
|
|
867
|
+
errorDiv.id = `${name}-error`;
|
|
868
|
+
errorDiv.className = "error-message";
|
|
869
|
+
errorDiv.setAttribute("role", "alert");
|
|
870
|
+
errorDiv.style.display = "none";
|
|
871
|
+
const fieldWrapper = inputElement.closest(".form-field") || inputElement.parentElement;
|
|
872
|
+
fieldWrapper.appendChild(errorDiv);
|
|
873
|
+
return errorDiv;
|
|
874
|
+
}
|
|
875
|
+
function getFieldValue(input) {
|
|
876
|
+
if (input.type === "checkbox") {
|
|
877
|
+
return input.checked;
|
|
878
|
+
} else if (input.type === "radio") {
|
|
879
|
+
const checked = form.querySelector(`[name="${input.name}"]:checked`);
|
|
880
|
+
return checked ? checked.value : null;
|
|
881
|
+
} else {
|
|
882
|
+
return input.value;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function setFieldValue(name, value) {
|
|
886
|
+
const field = state.fields.get(name);
|
|
887
|
+
if (!field) return;
|
|
888
|
+
const { element } = field;
|
|
889
|
+
if (element.type === "checkbox") {
|
|
890
|
+
element.checked = Boolean(value);
|
|
891
|
+
} else if (element.type === "radio") {
|
|
892
|
+
const radio = form.querySelector(`[name="${name}"][value="${value}"]`);
|
|
893
|
+
if (radio) radio.checked = true;
|
|
894
|
+
} else {
|
|
895
|
+
element.value = value;
|
|
896
|
+
}
|
|
897
|
+
state.values[name] = value;
|
|
898
|
+
}
|
|
899
|
+
function validateField2(name) {
|
|
900
|
+
const field = state.fields.get(name);
|
|
901
|
+
if (!field) return true;
|
|
902
|
+
const value = state.values[name];
|
|
903
|
+
let error = null;
|
|
904
|
+
if (field.required && (value === null || value === void 0 || value === "")) {
|
|
905
|
+
error = "This field is required";
|
|
906
|
+
}
|
|
907
|
+
if (!error && field.validators.length > 0) {
|
|
908
|
+
for (const validator of field.validators) {
|
|
909
|
+
const result = validator.validate ? validator.validate(value, state.values) : validator(value, state.values);
|
|
910
|
+
if (result !== true && result !== void 0 && result !== null) {
|
|
911
|
+
error = validator.message || result || "Validation failed";
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
state.errors[name] = error;
|
|
917
|
+
displayError(name, error);
|
|
918
|
+
return !error;
|
|
919
|
+
}
|
|
920
|
+
function displayError(name, error) {
|
|
921
|
+
const field = state.fields.get(name);
|
|
922
|
+
if (!field) return;
|
|
923
|
+
const { element, errorElement } = field;
|
|
924
|
+
if (error && state.touched[name] && opts.showErrorsOnTouch) {
|
|
925
|
+
errorElement.textContent = error;
|
|
926
|
+
errorElement.style.display = "block";
|
|
927
|
+
element.setAttribute("aria-invalid", "true");
|
|
928
|
+
element.classList.add("error");
|
|
929
|
+
} else {
|
|
930
|
+
errorElement.textContent = "";
|
|
931
|
+
errorElement.style.display = "none";
|
|
932
|
+
element.setAttribute("aria-invalid", "false");
|
|
933
|
+
element.classList.remove("error");
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function validateForm2() {
|
|
937
|
+
let isValid = true;
|
|
938
|
+
for (const name of state.fields.keys()) {
|
|
939
|
+
const fieldValid = validateField2(name);
|
|
940
|
+
if (!fieldValid) isValid = false;
|
|
941
|
+
}
|
|
942
|
+
return isValid;
|
|
943
|
+
}
|
|
944
|
+
function handleChange(event2) {
|
|
945
|
+
const input = event2.target;
|
|
946
|
+
const name = input.getAttribute("name");
|
|
947
|
+
if (!state.fields.has(name)) return;
|
|
948
|
+
state.values[name] = getFieldValue(input);
|
|
949
|
+
if (opts.validateOnChange) {
|
|
950
|
+
if (debounceTimers.has(name)) {
|
|
951
|
+
clearTimeout(debounceTimers.get(name));
|
|
952
|
+
}
|
|
953
|
+
const timer = setTimeout(() => {
|
|
954
|
+
validateField2(name);
|
|
955
|
+
debounceTimers.delete(name);
|
|
956
|
+
}, opts.debounce);
|
|
957
|
+
debounceTimers.set(name, timer);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function handleBlur(event2) {
|
|
961
|
+
const input = event2.target;
|
|
962
|
+
const name = input.getAttribute("name");
|
|
963
|
+
if (!state.fields.has(name)) return;
|
|
964
|
+
state.touched[name] = true;
|
|
965
|
+
if (opts.validateOnBlur) {
|
|
966
|
+
validateField2(name);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function handleSubmit(event2) {
|
|
970
|
+
event2.preventDefault();
|
|
971
|
+
for (const name of state.fields.keys()) {
|
|
972
|
+
state.touched[name] = true;
|
|
973
|
+
}
|
|
974
|
+
const isValid = validateForm2();
|
|
975
|
+
if (!isValid) {
|
|
976
|
+
const firstErrorField = Array.from(state.fields.values()).find((field) => state.errors[field.name]);
|
|
977
|
+
if (firstErrorField) {
|
|
978
|
+
firstErrorField.element.focus();
|
|
979
|
+
}
|
|
980
|
+
if (options.onError) {
|
|
981
|
+
options.onError(state.errors);
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
state.isSubmitting = true;
|
|
986
|
+
const submitData = { ...state.values };
|
|
987
|
+
if (options.onSubmit) {
|
|
988
|
+
const result = options.onSubmit(submitData, event2);
|
|
989
|
+
if (result === false) {
|
|
990
|
+
state.isSubmitting = false;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (result && typeof result.then === "function") {
|
|
994
|
+
result.then(() => {
|
|
995
|
+
state.isSubmitting = false;
|
|
996
|
+
if (options.onSuccess) {
|
|
997
|
+
options.onSuccess(submitData);
|
|
998
|
+
}
|
|
999
|
+
}).catch((error) => {
|
|
1000
|
+
state.isSubmitting = false;
|
|
1001
|
+
if (options.onError) {
|
|
1002
|
+
options.onError(error);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (!options.onSubmit) {
|
|
1009
|
+
form.submit();
|
|
1010
|
+
}
|
|
1011
|
+
state.isSubmitting = false;
|
|
1012
|
+
}
|
|
1013
|
+
function attachEventListeners() {
|
|
1014
|
+
state.fields.forEach((field) => {
|
|
1015
|
+
field.element.addEventListener("input", handleChange);
|
|
1016
|
+
field.element.addEventListener("blur", handleBlur);
|
|
1017
|
+
});
|
|
1018
|
+
form.addEventListener("submit", handleSubmit);
|
|
1019
|
+
}
|
|
1020
|
+
function detachEventListeners() {
|
|
1021
|
+
state.fields.forEach((field) => {
|
|
1022
|
+
field.element.removeEventListener("input", handleChange);
|
|
1023
|
+
field.element.removeEventListener("blur", handleBlur);
|
|
1024
|
+
});
|
|
1025
|
+
form.removeEventListener("submit", handleSubmit);
|
|
1026
|
+
debounceTimers.forEach((timer) => clearTimeout(timer));
|
|
1027
|
+
debounceTimers.clear();
|
|
1028
|
+
}
|
|
1029
|
+
function reset() {
|
|
1030
|
+
state.fields.forEach((field) => {
|
|
1031
|
+
setFieldValue(field.name, "");
|
|
1032
|
+
state.touched[field.name] = false;
|
|
1033
|
+
state.errors[field.name] = null;
|
|
1034
|
+
displayError(field.name, null);
|
|
1035
|
+
});
|
|
1036
|
+
state.isSubmitting = false;
|
|
1037
|
+
form.reset();
|
|
1038
|
+
}
|
|
1039
|
+
discoverFields();
|
|
1040
|
+
attachEventListeners();
|
|
1041
|
+
return {
|
|
1042
|
+
validateField: validateField2,
|
|
1043
|
+
validateForm: validateForm2,
|
|
1044
|
+
setFieldValue,
|
|
1045
|
+
getFieldValue: (name) => state.values[name],
|
|
1046
|
+
getError: (name) => state.errors[name],
|
|
1047
|
+
getErrors: () => ({ ...state.errors }),
|
|
1048
|
+
getValues: () => ({ ...state.values }),
|
|
1049
|
+
setTouched: (name, touched = true) => {
|
|
1050
|
+
state.touched[name] = touched;
|
|
1051
|
+
},
|
|
1052
|
+
reset,
|
|
1053
|
+
destroy: detachEventListeners,
|
|
1054
|
+
isValid: () => Object.values(state.errors).every((e) => !e),
|
|
1055
|
+
isSubmitting: () => state.isSubmitting,
|
|
1056
|
+
getState: () => ({
|
|
1057
|
+
values: { ...state.values },
|
|
1058
|
+
errors: { ...state.errors },
|
|
1059
|
+
touched: { ...state.touched },
|
|
1060
|
+
isSubmitting: state.isSubmitting
|
|
1061
|
+
})
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/validation.js
|
|
1066
|
+
var validators2 = {
|
|
1067
|
+
required: (message = "This field is required") => (value) => {
|
|
1068
|
+
if (value === null || value === void 0 || value === "") {
|
|
1069
|
+
return message;
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
},
|
|
1073
|
+
minLength: (min, message = `Minimum length is ${min}`) => (value) => {
|
|
1074
|
+
if (value && value.length < min) {
|
|
1075
|
+
return message;
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
},
|
|
1079
|
+
maxLength: (max, message = `Maximum length is ${max}`) => (value) => {
|
|
1080
|
+
if (value && value.length > max) {
|
|
1081
|
+
return message;
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
},
|
|
1085
|
+
min: (min, message = `Minimum value is ${min}`) => (value) => {
|
|
1086
|
+
if (value !== null && value !== void 0 && Number(value) < min) {
|
|
1087
|
+
return message;
|
|
1088
|
+
}
|
|
1089
|
+
return null;
|
|
1090
|
+
},
|
|
1091
|
+
max: (max, message = `Maximum value is ${max}`) => (value) => {
|
|
1092
|
+
if (value !== null && value !== void 0 && Number(value) > max) {
|
|
1093
|
+
return message;
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
},
|
|
1097
|
+
email: (message = "Invalid email address") => (value) => {
|
|
1098
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
1099
|
+
return message;
|
|
1100
|
+
}
|
|
1101
|
+
return null;
|
|
1102
|
+
},
|
|
1103
|
+
url: (message = "Invalid URL") => (value) => {
|
|
1104
|
+
if (value) {
|
|
1105
|
+
try {
|
|
1106
|
+
new URL(value);
|
|
1107
|
+
} catch {
|
|
1108
|
+
return message;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
},
|
|
1113
|
+
pattern: (regex, message = "Invalid format") => (value) => {
|
|
1114
|
+
if (value && !regex.test(value)) {
|
|
1115
|
+
return message;
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
},
|
|
1119
|
+
matches: (fieldName, message = "Fields do not match") => (value, formData) => {
|
|
1120
|
+
if (value !== formData[fieldName]) {
|
|
1121
|
+
return message;
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
},
|
|
1125
|
+
oneOf: (options, message = "Invalid option") => (value) => {
|
|
1126
|
+
if (value && !options.includes(value)) {
|
|
1127
|
+
return message;
|
|
1128
|
+
}
|
|
1129
|
+
return null;
|
|
1130
|
+
},
|
|
1131
|
+
custom: (fn, message = "Validation failed") => (value, formData) => {
|
|
1132
|
+
if (!fn(value, formData)) {
|
|
1133
|
+
return message;
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
var FormValidator = class {
|
|
1139
|
+
constructor(schema = {}) {
|
|
1140
|
+
this.schema = schema;
|
|
1141
|
+
this.errors = {};
|
|
1142
|
+
this.touched = {};
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Validate a single field
|
|
1146
|
+
*/
|
|
1147
|
+
validateField(name, value, formData = {}) {
|
|
1148
|
+
const fieldValidators = this.schema[name];
|
|
1149
|
+
if (!fieldValidators) {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
const validatorArray = Array.isArray(fieldValidators) ? fieldValidators : [fieldValidators];
|
|
1153
|
+
for (const validator of validatorArray) {
|
|
1154
|
+
const error = validator(value, formData);
|
|
1155
|
+
if (error) {
|
|
1156
|
+
return error;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Validate entire form
|
|
1163
|
+
*/
|
|
1164
|
+
validate(formData) {
|
|
1165
|
+
const errors = {};
|
|
1166
|
+
let isValid = true;
|
|
1167
|
+
for (const [name, value] of Object.entries(formData)) {
|
|
1168
|
+
const error = this.validateField(name, value, formData);
|
|
1169
|
+
if (error) {
|
|
1170
|
+
errors[name] = error;
|
|
1171
|
+
isValid = false;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
for (const name of Object.keys(this.schema)) {
|
|
1175
|
+
if (!(name in formData)) {
|
|
1176
|
+
const error = this.validateField(name, void 0, formData);
|
|
1177
|
+
if (error) {
|
|
1178
|
+
errors[name] = error;
|
|
1179
|
+
isValid = false;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
this.errors = errors;
|
|
1184
|
+
return { isValid, errors };
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Mark field as touched
|
|
1188
|
+
*/
|
|
1189
|
+
touch(name) {
|
|
1190
|
+
this.touched[name] = true;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Check if field is touched
|
|
1194
|
+
*/
|
|
1195
|
+
isTouched(name) {
|
|
1196
|
+
return this.touched[name] || false;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Get error for field
|
|
1200
|
+
*/
|
|
1201
|
+
getError(name) {
|
|
1202
|
+
return this.errors[name] || null;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Check if field has error
|
|
1206
|
+
*/
|
|
1207
|
+
hasError(name) {
|
|
1208
|
+
return !!this.errors[name];
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Clear errors
|
|
1212
|
+
*/
|
|
1213
|
+
clearErrors() {
|
|
1214
|
+
this.errors = {};
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Clear touched state
|
|
1218
|
+
*/
|
|
1219
|
+
clearTouched() {
|
|
1220
|
+
this.touched = {};
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Reset validator
|
|
1224
|
+
*/
|
|
1225
|
+
reset() {
|
|
1226
|
+
this.clearErrors();
|
|
1227
|
+
this.clearTouched();
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
function createValidator(schema) {
|
|
1231
|
+
return new FormValidator(schema);
|
|
1232
|
+
}
|
|
1233
|
+
function validate(formData, schema) {
|
|
1234
|
+
const validator = new FormValidator(schema);
|
|
1235
|
+
return validator.validate(formData);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/forms.js
|
|
1239
|
+
function createForm(options = {}) {
|
|
1240
|
+
const opts = {
|
|
1241
|
+
fields: {},
|
|
1242
|
+
validation: {
|
|
1243
|
+
strategy: "blur",
|
|
1244
|
+
debounce: 300,
|
|
1245
|
+
async: true,
|
|
1246
|
+
stopOnFirstError: false,
|
|
1247
|
+
revalidateOn: ["change", "blur"],
|
|
1248
|
+
...options.validation
|
|
1249
|
+
},
|
|
1250
|
+
errors: {
|
|
1251
|
+
format: "detailed",
|
|
1252
|
+
display: "inline",
|
|
1253
|
+
customFormatter: null,
|
|
1254
|
+
...options.errors
|
|
1255
|
+
},
|
|
1256
|
+
submission: {
|
|
1257
|
+
preventDefault: true,
|
|
1258
|
+
validateBeforeSubmit: true,
|
|
1259
|
+
disableOnSubmit: true,
|
|
1260
|
+
resetOnSuccess: false,
|
|
1261
|
+
onSuccess: null,
|
|
1262
|
+
onError: null,
|
|
1263
|
+
...options.submission
|
|
1264
|
+
},
|
|
1265
|
+
state: {
|
|
1266
|
+
trackDirty: true,
|
|
1267
|
+
trackTouched: true,
|
|
1268
|
+
trackVisited: true,
|
|
1269
|
+
initialValues: {},
|
|
1270
|
+
resetValues: null,
|
|
1271
|
+
...options.state
|
|
1272
|
+
},
|
|
1273
|
+
middleware: options.middleware || [],
|
|
1274
|
+
...options
|
|
1275
|
+
};
|
|
1276
|
+
const state = {
|
|
1277
|
+
values: { ...opts.state.initialValues },
|
|
1278
|
+
errors: {},
|
|
1279
|
+
touched: {},
|
|
1280
|
+
dirty: {},
|
|
1281
|
+
visited: {},
|
|
1282
|
+
isSubmitting: false,
|
|
1283
|
+
isValidating: false,
|
|
1284
|
+
submitCount: 0,
|
|
1285
|
+
asyncValidations: /* @__PURE__ */ new Map()
|
|
1286
|
+
};
|
|
1287
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1288
|
+
const stats = {
|
|
1289
|
+
validations: 0,
|
|
1290
|
+
asyncValidations: 0,
|
|
1291
|
+
submissions: 0,
|
|
1292
|
+
successfulSubmissions: 0,
|
|
1293
|
+
failedSubmissions: 0,
|
|
1294
|
+
middlewareExecutions: 0
|
|
1295
|
+
};
|
|
1296
|
+
function registerField(name, config) {
|
|
1297
|
+
fields.set(name, {
|
|
1298
|
+
name,
|
|
1299
|
+
type: config.type || "text",
|
|
1300
|
+
validators: config.validators || [],
|
|
1301
|
+
transform: config.transform || {},
|
|
1302
|
+
validateWhen: config.validateWhen,
|
|
1303
|
+
required: config.required || false,
|
|
1304
|
+
defaultValue: config.defaultValue,
|
|
1305
|
+
...config
|
|
1306
|
+
});
|
|
1307
|
+
if (!(name in state.values)) {
|
|
1308
|
+
state.values[name] = config.defaultValue !== void 0 ? config.defaultValue : "";
|
|
1309
|
+
}
|
|
1310
|
+
state.errors[name] = [];
|
|
1311
|
+
state.touched[name] = false;
|
|
1312
|
+
state.dirty[name] = false;
|
|
1313
|
+
state.visited[name] = false;
|
|
1314
|
+
}
|
|
1315
|
+
function getField(name) {
|
|
1316
|
+
return fields.get(name);
|
|
1317
|
+
}
|
|
1318
|
+
function setFieldValue(name, value, shouldValidate = true) {
|
|
1319
|
+
const field = fields.get(name);
|
|
1320
|
+
if (!field) {
|
|
1321
|
+
console.warn(`Field ${name} not registered`);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (field.transform?.input) {
|
|
1325
|
+
value = field.transform.input(value);
|
|
1326
|
+
}
|
|
1327
|
+
state.values[name] = value;
|
|
1328
|
+
if (opts.state.trackDirty) {
|
|
1329
|
+
const initialValue = opts.state.initialValues[name];
|
|
1330
|
+
state.dirty[name] = value !== initialValue;
|
|
1331
|
+
}
|
|
1332
|
+
if (shouldValidate && opts.validation.revalidateOn.includes("change")) {
|
|
1333
|
+
validateField2(name);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function getFieldValue(name) {
|
|
1337
|
+
return state.values[name];
|
|
1338
|
+
}
|
|
1339
|
+
function setFieldTouched(name, touched = true) {
|
|
1340
|
+
if (!opts.state.trackTouched) return;
|
|
1341
|
+
state.touched[name] = touched;
|
|
1342
|
+
if (touched && opts.validation.revalidateOn.includes("blur")) {
|
|
1343
|
+
validateField2(name);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function setFieldVisited(name, visited = true) {
|
|
1347
|
+
if (!opts.state.trackVisited) return;
|
|
1348
|
+
state.visited[name] = visited;
|
|
1349
|
+
}
|
|
1350
|
+
async function validateField2(name) {
|
|
1351
|
+
const field = fields.get(name);
|
|
1352
|
+
if (!field) return { valid: true, errors: [] };
|
|
1353
|
+
stats.validations++;
|
|
1354
|
+
if (field.validateWhen && !field.validateWhen(state.values)) {
|
|
1355
|
+
state.errors[name] = [];
|
|
1356
|
+
return { valid: true, errors: [] };
|
|
1357
|
+
}
|
|
1358
|
+
const value = state.values[name];
|
|
1359
|
+
const errors = [];
|
|
1360
|
+
if (state.asyncValidations.has(name)) {
|
|
1361
|
+
clearTimeout(state.asyncValidations.get(name));
|
|
1362
|
+
}
|
|
1363
|
+
if (field.required && (value === "" || value === null || value === void 0)) {
|
|
1364
|
+
errors.push({
|
|
1365
|
+
field: name,
|
|
1366
|
+
type: "required",
|
|
1367
|
+
message: `${name} is required`
|
|
1368
|
+
});
|
|
1369
|
+
state.errors[name] = errors;
|
|
1370
|
+
return { valid: false, errors };
|
|
1371
|
+
}
|
|
1372
|
+
for (const validator of field.validators) {
|
|
1373
|
+
try {
|
|
1374
|
+
let result;
|
|
1375
|
+
if (validator.validate.constructor.name === "AsyncFunction" || opts.validation.async) {
|
|
1376
|
+
stats.asyncValidations++;
|
|
1377
|
+
if (validator.debounce || opts.validation.debounce) {
|
|
1378
|
+
await new Promise((resolve) => {
|
|
1379
|
+
const timeoutId = setTimeout(resolve, validator.debounce || opts.validation.debounce);
|
|
1380
|
+
state.asyncValidations.set(name, timeoutId);
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
result = await validator.validate(value, state.values);
|
|
1384
|
+
} else {
|
|
1385
|
+
result = validator.validate(value, state.values);
|
|
1386
|
+
}
|
|
1387
|
+
if (result !== true && result !== void 0 && result !== null) {
|
|
1388
|
+
errors.push({
|
|
1389
|
+
field: name,
|
|
1390
|
+
type: validator.name || "custom",
|
|
1391
|
+
message: validator.message || result || "Validation failed"
|
|
1392
|
+
});
|
|
1393
|
+
if (opts.validation.stopOnFirstError) {
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
errors.push({
|
|
1399
|
+
field: name,
|
|
1400
|
+
type: "error",
|
|
1401
|
+
message: error.message || "Validation error"
|
|
1402
|
+
});
|
|
1403
|
+
if (opts.validation.stopOnFirstError) {
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
state.errors[name] = errors;
|
|
1409
|
+
return { valid: errors.length === 0, errors };
|
|
1410
|
+
}
|
|
1411
|
+
async function validateForm2() {
|
|
1412
|
+
state.isValidating = true;
|
|
1413
|
+
const validationPromises = Array.from(fields.keys()).map(
|
|
1414
|
+
(name) => validateField2(name)
|
|
1415
|
+
);
|
|
1416
|
+
const results = await Promise.all(validationPromises);
|
|
1417
|
+
state.isValidating = false;
|
|
1418
|
+
const allErrors = results.reduce((acc, result, index) => {
|
|
1419
|
+
const fieldName = Array.from(fields.keys())[index];
|
|
1420
|
+
if (result.errors.length > 0) {
|
|
1421
|
+
acc[fieldName] = result.errors;
|
|
1422
|
+
}
|
|
1423
|
+
return acc;
|
|
1424
|
+
}, {});
|
|
1425
|
+
const isValid2 = Object.keys(allErrors).length === 0;
|
|
1426
|
+
return {
|
|
1427
|
+
valid: isValid2,
|
|
1428
|
+
errors: allErrors
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
async function executeMiddleware(action, data) {
|
|
1432
|
+
if (opts.middleware.length === 0) return data;
|
|
1433
|
+
stats.middlewareExecutions++;
|
|
1434
|
+
let result = data;
|
|
1435
|
+
for (const middleware of opts.middleware) {
|
|
1436
|
+
try {
|
|
1437
|
+
const next = () => result;
|
|
1438
|
+
result = await middleware(action, result, next, state);
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
console.error("Middleware error:", error);
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return result;
|
|
1445
|
+
}
|
|
1446
|
+
function applyTransformations(values) {
|
|
1447
|
+
const transformed = { ...values };
|
|
1448
|
+
fields.forEach((field, name) => {
|
|
1449
|
+
if (field.transform?.output && name in transformed) {
|
|
1450
|
+
transformed[name] = field.transform.output(transformed[name]);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
return transformed;
|
|
1454
|
+
}
|
|
1455
|
+
async function handleSubmit(onSubmit) {
|
|
1456
|
+
stats.submissions++;
|
|
1457
|
+
try {
|
|
1458
|
+
if (opts.submission.preventDefault && typeof event !== "undefined") {
|
|
1459
|
+
event.preventDefault();
|
|
1460
|
+
}
|
|
1461
|
+
if (opts.submission.disableOnSubmit) {
|
|
1462
|
+
state.isSubmitting = true;
|
|
1463
|
+
}
|
|
1464
|
+
let values = { ...state.values };
|
|
1465
|
+
values = await executeMiddleware("beforeSubmit", values);
|
|
1466
|
+
if (opts.submission.validateBeforeSubmit) {
|
|
1467
|
+
const validation = await validateForm2();
|
|
1468
|
+
if (!validation.valid) {
|
|
1469
|
+
if (opts.submission.onError) {
|
|
1470
|
+
opts.submission.onError(validation.errors);
|
|
1471
|
+
}
|
|
1472
|
+
stats.failedSubmissions++;
|
|
1473
|
+
return { success: false, errors: validation.errors };
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
values = applyTransformations(values);
|
|
1477
|
+
values = await executeMiddleware("afterValidation", values);
|
|
1478
|
+
const result = await onSubmit(values);
|
|
1479
|
+
await executeMiddleware("afterSubmit", result);
|
|
1480
|
+
state.submitCount++;
|
|
1481
|
+
stats.successfulSubmissions++;
|
|
1482
|
+
if (opts.submission.resetOnSuccess) {
|
|
1483
|
+
reset();
|
|
1484
|
+
}
|
|
1485
|
+
if (opts.submission.onSuccess) {
|
|
1486
|
+
opts.submission.onSuccess(result);
|
|
1487
|
+
}
|
|
1488
|
+
return { success: true, data: result };
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
stats.failedSubmissions++;
|
|
1491
|
+
if (opts.submission.onError) {
|
|
1492
|
+
opts.submission.onError(error);
|
|
1493
|
+
}
|
|
1494
|
+
await executeMiddleware("onError", error);
|
|
1495
|
+
return { success: false, error };
|
|
1496
|
+
} finally {
|
|
1497
|
+
state.isSubmitting = false;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function reset(values) {
|
|
1501
|
+
const resetValues = values || opts.state.resetValues || opts.state.initialValues;
|
|
1502
|
+
state.values = { ...resetValues };
|
|
1503
|
+
state.errors = {};
|
|
1504
|
+
state.touched = {};
|
|
1505
|
+
state.dirty = {};
|
|
1506
|
+
state.visited = {};
|
|
1507
|
+
state.submitCount = 0;
|
|
1508
|
+
fields.forEach((field, name) => {
|
|
1509
|
+
if (!(name in state.values)) {
|
|
1510
|
+
state.values[name] = field.defaultValue !== void 0 ? field.defaultValue : "";
|
|
1511
|
+
}
|
|
1512
|
+
state.errors[name] = [];
|
|
1513
|
+
state.touched[name] = false;
|
|
1514
|
+
state.dirty[name] = false;
|
|
1515
|
+
state.visited[name] = false;
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
function getErrors(fieldName) {
|
|
1519
|
+
if (fieldName) {
|
|
1520
|
+
return state.errors[fieldName] || [];
|
|
1521
|
+
}
|
|
1522
|
+
if (opts.errors.customFormatter) {
|
|
1523
|
+
return opts.errors.customFormatter(state.errors);
|
|
1524
|
+
}
|
|
1525
|
+
if (opts.errors.format === "simple") {
|
|
1526
|
+
return Object.values(state.errors).flat().map((e) => e.message);
|
|
1527
|
+
}
|
|
1528
|
+
return state.errors;
|
|
1529
|
+
}
|
|
1530
|
+
function isValid() {
|
|
1531
|
+
return Object.values(state.errors).every((errors) => errors.length === 0);
|
|
1532
|
+
}
|
|
1533
|
+
function isDirty(fieldName) {
|
|
1534
|
+
if (fieldName) {
|
|
1535
|
+
return state.dirty[fieldName] || false;
|
|
1536
|
+
}
|
|
1537
|
+
return Object.values(state.dirty).some((dirty) => dirty);
|
|
1538
|
+
}
|
|
1539
|
+
function isTouched(fieldName) {
|
|
1540
|
+
if (fieldName) {
|
|
1541
|
+
return state.touched[fieldName] || false;
|
|
1542
|
+
}
|
|
1543
|
+
return Object.values(state.touched).some((touched) => touched);
|
|
1544
|
+
}
|
|
1545
|
+
function getValues() {
|
|
1546
|
+
return { ...state.values };
|
|
1547
|
+
}
|
|
1548
|
+
function setValues(values, shouldValidate = false) {
|
|
1549
|
+
Object.entries(values).forEach(([name, value]) => {
|
|
1550
|
+
setFieldValue(name, value, shouldValidate);
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
function getState() {
|
|
1554
|
+
return {
|
|
1555
|
+
values: { ...state.values },
|
|
1556
|
+
errors: { ...state.errors },
|
|
1557
|
+
touched: { ...state.touched },
|
|
1558
|
+
dirty: { ...state.dirty },
|
|
1559
|
+
visited: { ...state.visited },
|
|
1560
|
+
isSubmitting: state.isSubmitting,
|
|
1561
|
+
isValidating: state.isValidating,
|
|
1562
|
+
submitCount: state.submitCount,
|
|
1563
|
+
isValid: isValid(),
|
|
1564
|
+
isDirty: isDirty(),
|
|
1565
|
+
isTouched: isTouched()
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function getStats() {
|
|
1569
|
+
return {
|
|
1570
|
+
...stats,
|
|
1571
|
+
fieldsRegistered: fields.size,
|
|
1572
|
+
activeAsyncValidations: state.asyncValidations.size
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
Object.entries(opts.fields).forEach(([name, config]) => {
|
|
1576
|
+
registerField(name, config);
|
|
1577
|
+
});
|
|
1578
|
+
return {
|
|
1579
|
+
registerField,
|
|
1580
|
+
getField,
|
|
1581
|
+
setFieldValue,
|
|
1582
|
+
getFieldValue,
|
|
1583
|
+
setFieldTouched,
|
|
1584
|
+
setFieldVisited,
|
|
1585
|
+
validateField: validateField2,
|
|
1586
|
+
validateForm: validateForm2,
|
|
1587
|
+
handleSubmit,
|
|
1588
|
+
reset,
|
|
1589
|
+
getErrors,
|
|
1590
|
+
isValid,
|
|
1591
|
+
isDirty,
|
|
1592
|
+
isTouched,
|
|
1593
|
+
getValues,
|
|
1594
|
+
setValues,
|
|
1595
|
+
getState,
|
|
1596
|
+
getStats,
|
|
1597
|
+
// Expose state for testing
|
|
1598
|
+
_state: state
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
var formValidators = {
|
|
1602
|
+
required: {
|
|
1603
|
+
name: "required",
|
|
1604
|
+
validate: (value) => {
|
|
1605
|
+
return value !== "" && value !== null && value !== void 0;
|
|
1606
|
+
},
|
|
1607
|
+
message: "This field is required"
|
|
1608
|
+
},
|
|
1609
|
+
email: {
|
|
1610
|
+
name: "email",
|
|
1611
|
+
validate: (value) => {
|
|
1612
|
+
if (!value) return true;
|
|
1613
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1614
|
+
return regex.test(value);
|
|
1615
|
+
},
|
|
1616
|
+
message: "Please enter a valid email address"
|
|
1617
|
+
},
|
|
1618
|
+
minLength: (min) => ({
|
|
1619
|
+
name: "minLength",
|
|
1620
|
+
validate: (value) => {
|
|
1621
|
+
if (!value) return true;
|
|
1622
|
+
return String(value).length >= min;
|
|
1623
|
+
},
|
|
1624
|
+
message: `Must be at least ${min} characters`
|
|
1625
|
+
}),
|
|
1626
|
+
maxLength: (max) => ({
|
|
1627
|
+
name: "maxLength",
|
|
1628
|
+
validate: (value) => {
|
|
1629
|
+
if (!value) return true;
|
|
1630
|
+
return String(value).length <= max;
|
|
1631
|
+
},
|
|
1632
|
+
message: `Must be no more than ${max} characters`
|
|
1633
|
+
}),
|
|
1634
|
+
pattern: (regex, message) => ({
|
|
1635
|
+
name: "pattern",
|
|
1636
|
+
validate: (value) => {
|
|
1637
|
+
if (!value) return true;
|
|
1638
|
+
return regex.test(value);
|
|
1639
|
+
},
|
|
1640
|
+
message: message || "Invalid format"
|
|
1641
|
+
}),
|
|
1642
|
+
asyncEmail: {
|
|
1643
|
+
name: "asyncEmail",
|
|
1644
|
+
validate: async (value) => {
|
|
1645
|
+
if (!value) return true;
|
|
1646
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1647
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1648
|
+
return regex.test(value);
|
|
1649
|
+
},
|
|
1650
|
+
message: "Please enter a valid email address",
|
|
1651
|
+
debounce: 500
|
|
1652
|
+
},
|
|
1653
|
+
asyncUnique: (checkFn) => ({
|
|
1654
|
+
name: "asyncUnique",
|
|
1655
|
+
validate: async (value) => {
|
|
1656
|
+
if (!value) return true;
|
|
1657
|
+
return await checkFn(value);
|
|
1658
|
+
},
|
|
1659
|
+
message: "This value is already taken",
|
|
1660
|
+
debounce: 500
|
|
1661
|
+
})
|
|
1662
|
+
};
|
|
1663
|
+
var enhancedForm = createForm();
|
|
1664
|
+
|
|
1665
|
+
// src/advanced-validation.js
|
|
1666
|
+
import { ReactiveState, observable, computed } from "@coherent.js/state/src/reactive-state.js";
|
|
1667
|
+
import { globalErrorHandler } from "@coherent.js/core/src/utils/_error-handler.js";
|
|
1668
|
+
var validationRules = {
|
|
1669
|
+
required: (value, params = true) => {
|
|
1670
|
+
if (!params) return true;
|
|
1671
|
+
const isEmpty = value === null || value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
|
|
1672
|
+
return !isEmpty || "This field is required";
|
|
1673
|
+
},
|
|
1674
|
+
min: (value, minValue) => {
|
|
1675
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1676
|
+
const num = Number(value);
|
|
1677
|
+
return isNaN(num) || num >= minValue || `Value must be at least ${minValue}`;
|
|
1678
|
+
},
|
|
1679
|
+
max: (value, maxValue) => {
|
|
1680
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1681
|
+
const num = Number(value);
|
|
1682
|
+
return isNaN(num) || num <= maxValue || `Value must be no more than ${maxValue}`;
|
|
1683
|
+
},
|
|
1684
|
+
minLength: (value, minLen) => {
|
|
1685
|
+
if (value === null || value === void 0) return true;
|
|
1686
|
+
const str = String(value);
|
|
1687
|
+
return str.length >= minLen || `Must be at least ${minLen} characters`;
|
|
1688
|
+
},
|
|
1689
|
+
maxLength: (value, maxLen) => {
|
|
1690
|
+
if (value === null || value === void 0) return true;
|
|
1691
|
+
const str = String(value);
|
|
1692
|
+
return str.length <= maxLen || `Must be no more than ${maxLen} characters`;
|
|
1693
|
+
},
|
|
1694
|
+
pattern: (value, regex, message = "Invalid format") => {
|
|
1695
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1696
|
+
const pattern = typeof regex === "string" ? new RegExp(regex) : regex;
|
|
1697
|
+
return pattern.test(String(value)) || message;
|
|
1698
|
+
},
|
|
1699
|
+
email: (value) => {
|
|
1700
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1701
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1702
|
+
return emailRegex.test(String(value)) || "Please enter a valid email address";
|
|
1703
|
+
},
|
|
1704
|
+
url: (value) => {
|
|
1705
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1706
|
+
try {
|
|
1707
|
+
new URL(String(value));
|
|
1708
|
+
return true;
|
|
1709
|
+
} catch {
|
|
1710
|
+
return "Please enter a valid URL";
|
|
1711
|
+
}
|
|
1712
|
+
},
|
|
1713
|
+
numeric: (value) => {
|
|
1714
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1715
|
+
return !isNaN(Number(value)) || "Must be a valid number";
|
|
1716
|
+
},
|
|
1717
|
+
integer: (value) => {
|
|
1718
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1719
|
+
const num = Number(value);
|
|
1720
|
+
return Number.isInteger(num) || "Must be a whole number";
|
|
1721
|
+
},
|
|
1722
|
+
alpha: (value) => {
|
|
1723
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1724
|
+
return /^[a-zA-Z]+$/.test(String(value)) || "Must contain only letters";
|
|
1725
|
+
},
|
|
1726
|
+
alphanumeric: (value) => {
|
|
1727
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1728
|
+
return /^[a-zA-Z0-9]+$/.test(String(value)) || "Must contain only letters and numbers";
|
|
1729
|
+
},
|
|
1730
|
+
equals: (value, otherValue, fieldName = "other field") => {
|
|
1731
|
+
return value === otherValue || `Must match ${fieldName}`;
|
|
1732
|
+
},
|
|
1733
|
+
oneOf: (value, options, message = "Invalid selection") => {
|
|
1734
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1735
|
+
return options.includes(value) || message;
|
|
1736
|
+
},
|
|
1737
|
+
custom: (value, validator, ...args) => {
|
|
1738
|
+
if (typeof validator !== "function") {
|
|
1739
|
+
throw new Error("Custom validator must be a function");
|
|
1740
|
+
}
|
|
1741
|
+
return validator(value, ...args);
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
var binding = {
|
|
1745
|
+
/**
|
|
1746
|
+
* Two-way data binding for input elements
|
|
1747
|
+
*/
|
|
1748
|
+
model(form, fieldName, options = {}) {
|
|
1749
|
+
return {
|
|
1750
|
+
value: form.getField(fieldName),
|
|
1751
|
+
oninput: (event2) => {
|
|
1752
|
+
const value = options.number ? Number(event2.target.value) : event2.target.value;
|
|
1753
|
+
form.setField(fieldName, value);
|
|
1754
|
+
},
|
|
1755
|
+
onblur: () => {
|
|
1756
|
+
form.handleBlur(fieldName);
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
},
|
|
1760
|
+
/**
|
|
1761
|
+
* Checkbox binding
|
|
1762
|
+
*/
|
|
1763
|
+
checkbox(form, fieldName) {
|
|
1764
|
+
return {
|
|
1765
|
+
checked: Boolean(form.getField(fieldName)),
|
|
1766
|
+
onchange: (event2) => {
|
|
1767
|
+
form.setField(fieldName, event2.target.checked);
|
|
1768
|
+
},
|
|
1769
|
+
onblur: () => {
|
|
1770
|
+
form.handleBlur(fieldName);
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
},
|
|
1774
|
+
/**
|
|
1775
|
+
* Select dropdown binding
|
|
1776
|
+
*/
|
|
1777
|
+
select(form, fieldName, options = {}) {
|
|
1778
|
+
return {
|
|
1779
|
+
value: form.getField(fieldName),
|
|
1780
|
+
onchange: (event2) => {
|
|
1781
|
+
const value = options.multiple ? Array.from(event2.target.selectedOptions, (opt) => opt.value) : event2.target.value;
|
|
1782
|
+
form.setField(fieldName, value);
|
|
1783
|
+
},
|
|
1784
|
+
onblur: () => {
|
|
1785
|
+
form.handleBlur(fieldName);
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
},
|
|
1789
|
+
/**
|
|
1790
|
+
* Radio button binding
|
|
1791
|
+
*/
|
|
1792
|
+
radio(form, fieldName, value) {
|
|
1793
|
+
return {
|
|
1794
|
+
checked: form.getField(fieldName) === value,
|
|
1795
|
+
value,
|
|
1796
|
+
onchange: (event2) => {
|
|
1797
|
+
if (event2.target.checked) {
|
|
1798
|
+
form.setField(fieldName, value);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
var formComponents = {
|
|
1805
|
+
/**
|
|
1806
|
+
* Validation _error display
|
|
1807
|
+
*/
|
|
1808
|
+
ValidationError({ form, field, className = "validation-_error" }) {
|
|
1809
|
+
const validator = form.getValidator(field);
|
|
1810
|
+
if (!validator || validator.errors.value.length === 0) {
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
return {
|
|
1814
|
+
div: {
|
|
1815
|
+
className,
|
|
1816
|
+
children: validator.errors.value.map((_error) => ({
|
|
1817
|
+
span: { text: _error, className: "_error-message" }
|
|
1818
|
+
}))
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
},
|
|
1822
|
+
/**
|
|
1823
|
+
* Form field wrapper with validation
|
|
1824
|
+
*/
|
|
1825
|
+
FormField({ form, field, label, children, showErrors = true }) {
|
|
1826
|
+
const validator = form.getValidator(field);
|
|
1827
|
+
const state = validator ? validator.getState() : {};
|
|
1828
|
+
return {
|
|
1829
|
+
div: {
|
|
1830
|
+
className: `form-field ${state.hasError ? "has-_error" : ""} ${state.isTouched ? "touched" : ""}`,
|
|
1831
|
+
children: [
|
|
1832
|
+
label ? { label: { text: label, htmlFor: field } } : null,
|
|
1833
|
+
children,
|
|
1834
|
+
showErrors && state.hasError ? formComponents.ValidationError({ form, field }) : null
|
|
1835
|
+
].filter(Boolean)
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
export {
|
|
1841
|
+
FormBuilder,
|
|
1842
|
+
FormValidator,
|
|
1843
|
+
binding,
|
|
1844
|
+
buildForm,
|
|
1845
|
+
composeValidators,
|
|
1846
|
+
createForm,
|
|
1847
|
+
createFormBuilder,
|
|
1848
|
+
createValidator,
|
|
1849
|
+
enhancedForm,
|
|
1850
|
+
formComponents,
|
|
1851
|
+
formValidators,
|
|
1852
|
+
hydrateForm,
|
|
1853
|
+
registerValidator,
|
|
1854
|
+
validate,
|
|
1855
|
+
validateField,
|
|
1856
|
+
validateForm,
|
|
1857
|
+
validationRules,
|
|
1858
|
+
validators2 as validators
|
|
1859
|
+
};
|
|
1860
|
+
//# sourceMappingURL=index.js.map
|