@prosdevlab/experience-sdk-plugins 0.2.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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +120 -0
- package/README.md +141 -79
- package/dist/index.d.ts +206 -35
- package/dist/index.js +1229 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +63 -62
- package/src/exit-intent/exit-intent.ts +2 -3
- package/src/index.ts +2 -0
- package/src/inline/index.ts +3 -0
- package/src/inline/inline.test.ts +620 -0
- package/src/inline/inline.ts +269 -0
- package/src/inline/insertion.ts +66 -0
- package/src/inline/types.ts +52 -0
- package/src/integration.test.ts +356 -297
- package/src/modal/form-rendering.ts +262 -0
- package/src/modal/form-styles.ts +212 -0
- package/src/modal/form-validation.test.ts +413 -0
- package/src/modal/form-validation.ts +126 -0
- package/src/modal/index.ts +3 -0
- package/src/modal/modal-styles.ts +204 -0
- package/src/modal/modal.browser.test.ts +164 -0
- package/src/modal/modal.test.ts +1294 -0
- package/src/modal/modal.ts +685 -0
- package/src/modal/types.ts +114 -0
- package/src/scroll-depth/scroll-depth.test.ts +35 -0
- package/src/scroll-depth/scroll-depth.ts +2 -4
- package/src/time-delay/time-delay.test.ts +2 -2
- package/src/time-delay/time-delay.ts +2 -3
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +4 -1
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateField, validateForm } from './form-validation';
|
|
3
|
+
import type { FormConfig, FormField } from './types';
|
|
4
|
+
|
|
5
|
+
describe('validateField', () => {
|
|
6
|
+
describe('required validation', () => {
|
|
7
|
+
it('should pass when required field has value', () => {
|
|
8
|
+
const field: FormField = {
|
|
9
|
+
name: 'email',
|
|
10
|
+
type: 'email',
|
|
11
|
+
required: true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const result = validateField(field, 'user@example.com');
|
|
15
|
+
|
|
16
|
+
expect(result.valid).toBe(true);
|
|
17
|
+
expect(result.errors).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should fail when required field is empty', () => {
|
|
21
|
+
const field: FormField = {
|
|
22
|
+
name: 'email',
|
|
23
|
+
type: 'email',
|
|
24
|
+
required: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const result = validateField(field, '');
|
|
28
|
+
|
|
29
|
+
expect(result.valid).toBe(false);
|
|
30
|
+
expect(result.errors).toEqual({ email: 'email is required' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should fail when required field is whitespace', () => {
|
|
34
|
+
const field: FormField = {
|
|
35
|
+
name: 'name',
|
|
36
|
+
type: 'text',
|
|
37
|
+
label: 'Name',
|
|
38
|
+
required: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = validateField(field, ' ');
|
|
42
|
+
|
|
43
|
+
expect(result.valid).toBe(false);
|
|
44
|
+
expect(result.errors).toEqual({ name: 'Name is required' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should use custom error message for required field', () => {
|
|
48
|
+
const field: FormField = {
|
|
49
|
+
name: 'email',
|
|
50
|
+
type: 'email',
|
|
51
|
+
required: true,
|
|
52
|
+
errorMessage: 'Email address is required',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const result = validateField(field, '');
|
|
56
|
+
|
|
57
|
+
expect(result.valid).toBe(false);
|
|
58
|
+
expect(result.errors).toEqual({ email: 'Email address is required' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should pass when optional field is empty', () => {
|
|
62
|
+
const field: FormField = {
|
|
63
|
+
name: 'phone',
|
|
64
|
+
type: 'tel',
|
|
65
|
+
required: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = validateField(field, '');
|
|
69
|
+
|
|
70
|
+
expect(result.valid).toBe(true);
|
|
71
|
+
expect(result.errors).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('email validation', () => {
|
|
76
|
+
const field: FormField = {
|
|
77
|
+
name: 'email',
|
|
78
|
+
type: 'email',
|
|
79
|
+
required: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
it('should pass with valid email', () => {
|
|
83
|
+
const result = validateField(field, 'user@example.com');
|
|
84
|
+
expect(result.valid).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should pass with subdomain email', () => {
|
|
88
|
+
const result = validateField(field, 'user@mail.example.com');
|
|
89
|
+
expect(result.valid).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should fail with invalid email (no @)', () => {
|
|
93
|
+
const result = validateField(field, 'userexample.com');
|
|
94
|
+
expect(result.valid).toBe(false);
|
|
95
|
+
expect(result.errors).toEqual({ email: 'Please enter a valid email address' });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should fail with invalid email (no domain)', () => {
|
|
99
|
+
const result = validateField(field, 'user@');
|
|
100
|
+
expect(result.valid).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should fail with invalid email (no TLD)', () => {
|
|
104
|
+
const result = validateField(field, 'user@example');
|
|
105
|
+
expect(result.valid).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should use custom error message for invalid email', () => {
|
|
109
|
+
const customField: FormField = {
|
|
110
|
+
...field,
|
|
111
|
+
errorMessage: 'Invalid email format',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = validateField(customField, 'invalid');
|
|
115
|
+
expect(result.errors).toEqual({ email: 'Invalid email format' });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('url validation', () => {
|
|
120
|
+
const field: FormField = {
|
|
121
|
+
name: 'website',
|
|
122
|
+
type: 'url',
|
|
123
|
+
required: false,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
it('should pass with valid HTTP URL', () => {
|
|
127
|
+
const result = validateField(field, 'http://example.com');
|
|
128
|
+
expect(result.valid).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should pass with valid HTTPS URL', () => {
|
|
132
|
+
const result = validateField(field, 'https://example.com/path?query=1');
|
|
133
|
+
expect(result.valid).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should fail with invalid URL (no protocol)', () => {
|
|
137
|
+
const result = validateField(field, 'example.com');
|
|
138
|
+
expect(result.valid).toBe(false);
|
|
139
|
+
expect(result.errors).toEqual({ website: 'Please enter a valid URL' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should fail with invalid URL format', () => {
|
|
143
|
+
const result = validateField(field, 'not a url');
|
|
144
|
+
expect(result.valid).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('tel validation', () => {
|
|
149
|
+
const field: FormField = {
|
|
150
|
+
name: 'phone',
|
|
151
|
+
type: 'tel',
|
|
152
|
+
required: false,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
it('should pass with digits only', () => {
|
|
156
|
+
const result = validateField(field, '5551234567');
|
|
157
|
+
expect(result.valid).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should pass with dashes', () => {
|
|
161
|
+
const result = validateField(field, '555-123-4567');
|
|
162
|
+
expect(result.valid).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should pass with parentheses and spaces', () => {
|
|
166
|
+
const result = validateField(field, '(555) 123-4567');
|
|
167
|
+
expect(result.valid).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should pass with plus and country code', () => {
|
|
171
|
+
const result = validateField(field, '+1 555-123-4567');
|
|
172
|
+
expect(result.valid).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should fail with letters', () => {
|
|
176
|
+
const result = validateField(field, '555-CALL-NOW');
|
|
177
|
+
expect(result.valid).toBe(false);
|
|
178
|
+
expect(result.errors).toEqual({ phone: 'Please enter a valid phone number' });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('number validation', () => {
|
|
183
|
+
const field: FormField = {
|
|
184
|
+
name: 'age',
|
|
185
|
+
type: 'number',
|
|
186
|
+
required: false,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
it('should pass with integer', () => {
|
|
190
|
+
const result = validateField(field, '25');
|
|
191
|
+
expect(result.valid).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should pass with decimal', () => {
|
|
195
|
+
const result = validateField(field, '25.5');
|
|
196
|
+
expect(result.valid).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should pass with negative number', () => {
|
|
200
|
+
const result = validateField(field, '-10');
|
|
201
|
+
expect(result.valid).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should fail with non-numeric value', () => {
|
|
205
|
+
const result = validateField(field, 'twenty');
|
|
206
|
+
expect(result.valid).toBe(false);
|
|
207
|
+
expect(result.errors).toEqual({ age: 'Please enter a valid number' });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('pattern validation', () => {
|
|
212
|
+
it('should pass when value matches pattern', () => {
|
|
213
|
+
const field: FormField = {
|
|
214
|
+
name: 'zipcode',
|
|
215
|
+
type: 'text',
|
|
216
|
+
pattern: '^\\d{5}$',
|
|
217
|
+
required: false,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = validateField(field, '12345');
|
|
221
|
+
expect(result.valid).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should fail when value does not match pattern', () => {
|
|
225
|
+
const field: FormField = {
|
|
226
|
+
name: 'zipcode',
|
|
227
|
+
type: 'text',
|
|
228
|
+
label: 'ZIP Code',
|
|
229
|
+
pattern: '^\\d{5}$',
|
|
230
|
+
required: false,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = validateField(field, '1234');
|
|
234
|
+
expect(result.valid).toBe(false);
|
|
235
|
+
expect(result.errors).toEqual({ zipcode: 'Invalid format for ZIP Code' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should use custom error message for pattern mismatch', () => {
|
|
239
|
+
const field: FormField = {
|
|
240
|
+
name: 'zipcode',
|
|
241
|
+
type: 'text',
|
|
242
|
+
pattern: '^\\d{5}$',
|
|
243
|
+
errorMessage: 'ZIP code must be 5 digits',
|
|
244
|
+
required: false,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = validateField(field, 'ABCDE');
|
|
248
|
+
expect(result.errors).toEqual({ zipcode: 'ZIP code must be 5 digits' });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should handle invalid regex pattern gracefully', () => {
|
|
252
|
+
const field: FormField = {
|
|
253
|
+
name: 'test',
|
|
254
|
+
type: 'text',
|
|
255
|
+
pattern: '[invalid(regex',
|
|
256
|
+
required: false,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Should not throw, just pass validation
|
|
260
|
+
const result = validateField(field, 'anything');
|
|
261
|
+
expect(result.valid).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('validateForm', () => {
|
|
267
|
+
it('should pass when all fields are valid', () => {
|
|
268
|
+
const config: FormConfig = {
|
|
269
|
+
fields: [
|
|
270
|
+
{ name: 'email', type: 'email', required: true },
|
|
271
|
+
{ name: 'name', type: 'text', required: true },
|
|
272
|
+
],
|
|
273
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const data = {
|
|
277
|
+
email: 'user@example.com',
|
|
278
|
+
name: 'John Doe',
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const result = validateForm(config, data);
|
|
282
|
+
expect(result.valid).toBe(true);
|
|
283
|
+
expect(result.errors).toBeUndefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should fail when any field is invalid', () => {
|
|
287
|
+
const config: FormConfig = {
|
|
288
|
+
fields: [
|
|
289
|
+
{ name: 'email', type: 'email', required: true },
|
|
290
|
+
{ name: 'name', type: 'text', required: true },
|
|
291
|
+
],
|
|
292
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const data = {
|
|
296
|
+
email: 'invalid-email',
|
|
297
|
+
name: 'John Doe',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const result = validateForm(config, data);
|
|
301
|
+
expect(result.valid).toBe(false);
|
|
302
|
+
expect(result.errors).toHaveProperty('email');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should collect errors from multiple fields', () => {
|
|
306
|
+
const config: FormConfig = {
|
|
307
|
+
fields: [
|
|
308
|
+
{ name: 'email', type: 'email', required: true },
|
|
309
|
+
{ name: 'name', type: 'text', required: true },
|
|
310
|
+
{ name: 'phone', type: 'tel', required: true },
|
|
311
|
+
],
|
|
312
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const data = {
|
|
316
|
+
email: '',
|
|
317
|
+
name: '',
|
|
318
|
+
phone: '',
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const result = validateForm(config, data);
|
|
322
|
+
expect(result.valid).toBe(false);
|
|
323
|
+
expect(Object.keys(result.errors || {})).toHaveLength(3);
|
|
324
|
+
expect(result.errors).toHaveProperty('email');
|
|
325
|
+
expect(result.errors).toHaveProperty('name');
|
|
326
|
+
expect(result.errors).toHaveProperty('phone');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should run custom validation function', () => {
|
|
330
|
+
const config: FormConfig = {
|
|
331
|
+
fields: [{ name: 'email', type: 'email', required: true }],
|
|
332
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
333
|
+
validate: (data) => {
|
|
334
|
+
if (data.email.endsWith('@competitor.com')) {
|
|
335
|
+
return {
|
|
336
|
+
valid: false,
|
|
337
|
+
errors: { email: 'Competitor emails not allowed' },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { valid: true };
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const data = { email: 'user@competitor.com' };
|
|
345
|
+
|
|
346
|
+
const result = validateForm(config, data);
|
|
347
|
+
expect(result.valid).toBe(false);
|
|
348
|
+
expect(result.errors).toEqual({ email: 'Competitor emails not allowed' });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should merge custom validation errors with field errors', () => {
|
|
352
|
+
const config: FormConfig = {
|
|
353
|
+
fields: [
|
|
354
|
+
{ name: 'email', type: 'email', required: true },
|
|
355
|
+
{ name: 'password', type: 'text', required: true },
|
|
356
|
+
],
|
|
357
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
358
|
+
validate: (data) => {
|
|
359
|
+
if (data.password.length < 8) {
|
|
360
|
+
return {
|
|
361
|
+
valid: false,
|
|
362
|
+
errors: { password: 'Password must be at least 8 characters' },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return { valid: true };
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const data = {
|
|
370
|
+
email: 'invalid',
|
|
371
|
+
password: '123',
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = validateForm(config, data);
|
|
375
|
+
expect(result.valid).toBe(false);
|
|
376
|
+
expect(result.errors).toHaveProperty('email'); // Field validation error
|
|
377
|
+
expect(result.errors).toHaveProperty('password'); // Custom validation error
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle custom validation function errors gracefully', () => {
|
|
381
|
+
const config: FormConfig = {
|
|
382
|
+
fields: [{ name: 'email', type: 'email', required: true }],
|
|
383
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
384
|
+
validate: () => {
|
|
385
|
+
throw new Error('Custom validation broke!');
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const data = { email: 'user@example.com' };
|
|
390
|
+
|
|
391
|
+
// Should not throw, should pass validation
|
|
392
|
+
const result = validateForm(config, data);
|
|
393
|
+
expect(result.valid).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should pass when optional fields are empty', () => {
|
|
397
|
+
const config: FormConfig = {
|
|
398
|
+
fields: [
|
|
399
|
+
{ name: 'email', type: 'email', required: true },
|
|
400
|
+
{ name: 'phone', type: 'tel', required: false },
|
|
401
|
+
],
|
|
402
|
+
submitButton: { text: 'Submit', action: 'submit' },
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const data = {
|
|
406
|
+
email: 'user@example.com',
|
|
407
|
+
phone: '',
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const result = validateForm(config, data);
|
|
411
|
+
expect(result.valid).toBe(true);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validation functions for form fields
|
|
3
|
+
*
|
|
4
|
+
* These functions are intentionally pure (no side effects) to make them:
|
|
5
|
+
* - Easy to test
|
|
6
|
+
* - Easy to extract into a separate form plugin later
|
|
7
|
+
* - Reusable across different contexts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FormConfig, FormField, ValidationResult } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a single form field
|
|
14
|
+
*
|
|
15
|
+
* @param field - Field configuration
|
|
16
|
+
* @param value - Current field value
|
|
17
|
+
* @returns Validation result with errors if invalid
|
|
18
|
+
*/
|
|
19
|
+
export function validateField(field: FormField, value: string): ValidationResult {
|
|
20
|
+
const errors: Record<string, string> = {};
|
|
21
|
+
|
|
22
|
+
// Required field validation
|
|
23
|
+
if (field.required && (!value || value.trim() === '')) {
|
|
24
|
+
errors[field.name] = field.errorMessage || `${field.label || field.name} is required`;
|
|
25
|
+
return { valid: false, errors };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Skip further validation if field is empty and not required
|
|
29
|
+
if (!value || value.trim() === '') {
|
|
30
|
+
return { valid: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Type-specific validation
|
|
34
|
+
switch (field.type) {
|
|
35
|
+
case 'email': {
|
|
36
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
37
|
+
if (!emailRegex.test(value)) {
|
|
38
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid email address';
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'url': {
|
|
44
|
+
try {
|
|
45
|
+
new URL(value);
|
|
46
|
+
} catch {
|
|
47
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid URL';
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'tel': {
|
|
53
|
+
// Basic phone validation (allows digits, spaces, dashes, parentheses, plus)
|
|
54
|
+
const phoneRegex = /^[\d\s\-()+]+$/;
|
|
55
|
+
if (!phoneRegex.test(value)) {
|
|
56
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid phone number';
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'number': {
|
|
62
|
+
if (Number.isNaN(Number(value))) {
|
|
63
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid number';
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Custom pattern validation (regex)
|
|
70
|
+
if (field.pattern && value) {
|
|
71
|
+
try {
|
|
72
|
+
const regex = new RegExp(field.pattern);
|
|
73
|
+
if (!regex.test(value)) {
|
|
74
|
+
errors[field.name] =
|
|
75
|
+
field.errorMessage || `Invalid format for ${field.label || field.name}`;
|
|
76
|
+
}
|
|
77
|
+
} catch (_error) {
|
|
78
|
+
// Invalid regex pattern - log warning but don't break validation
|
|
79
|
+
console.warn(`Invalid regex pattern for field ${field.name}:`, field.pattern);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: Object.keys(errors).length === 0,
|
|
85
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate entire form
|
|
91
|
+
*
|
|
92
|
+
* @param config - Form configuration
|
|
93
|
+
* @param data - Current form data
|
|
94
|
+
* @returns Validation result with all field errors if invalid
|
|
95
|
+
*/
|
|
96
|
+
export function validateForm(config: FormConfig, data: Record<string, string>): ValidationResult {
|
|
97
|
+
const errors: Record<string, string> = {};
|
|
98
|
+
|
|
99
|
+
// Validate each field
|
|
100
|
+
config.fields.forEach((field) => {
|
|
101
|
+
const value = data[field.name] || '';
|
|
102
|
+
const result = validateField(field, value);
|
|
103
|
+
|
|
104
|
+
if (!result.valid && result.errors) {
|
|
105
|
+
Object.assign(errors, result.errors);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Custom validation function
|
|
110
|
+
if (config.validate) {
|
|
111
|
+
try {
|
|
112
|
+
const customResult = config.validate(data);
|
|
113
|
+
if (!customResult.valid && customResult.errors) {
|
|
114
|
+
Object.assign(errors, customResult.errors);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Custom validation function threw an error:', error);
|
|
118
|
+
// Don't prevent submission if custom validation has a bug
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
valid: Object.keys(errors).length === 0,
|
|
124
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|