@tachui/forms 0.7.1-alpha → 0.8.1-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -272
- package/dist/DatePicker-D5nRFTUm.js +475 -0
- package/dist/DatePicker-D5nRFTUm.js.map +1 -0
- package/dist/Select-yZyKooXk.js +945 -0
- package/dist/Select-yZyKooXk.js.map +1 -0
- package/dist/Slider-0-oal5YR.js +644 -0
- package/dist/Slider-0-oal5YR.js.map +1 -0
- package/dist/TextField-hX15dY3U.js +509 -0
- package/dist/TextField-hX15dY3U.js.map +1 -0
- package/dist/components/advanced/Slider.d.ts +190 -0
- package/dist/components/advanced/Slider.d.ts.map +1 -0
- package/dist/components/advanced/Stepper.d.ts +161 -0
- package/dist/components/advanced/Stepper.d.ts.map +1 -0
- package/dist/components/advanced/index.d.ts +15 -0
- package/dist/components/advanced/index.d.ts.map +1 -0
- package/dist/components/advanced/index.js +6 -0
- package/dist/components/advanced/index.js.map +1 -0
- package/dist/components/date-picker/DatePicker.d.ts +126 -0
- package/dist/components/date-picker/DatePicker.d.ts.map +1 -0
- package/dist/components/date-picker/index.d.ts +14 -0
- package/dist/components/date-picker/index.d.ts.map +1 -0
- package/dist/components/date-picker/index.js +5 -0
- package/dist/components/date-picker/index.js.map +1 -0
- package/dist/components/form-container/index.d.ts +58 -0
- package/dist/components/form-container/index.d.ts.map +1 -0
- package/dist/components/selection/Checkbox.d.ts.map +1 -0
- package/dist/components/selection/Radio.d.ts.map +1 -0
- package/dist/components/selection/Select.d.ts.map +1 -0
- package/dist/components/selection/index.d.ts +68 -0
- package/dist/components/selection/index.d.ts.map +1 -0
- package/dist/components/selection/index.js +12 -0
- package/dist/components/selection/index.js.map +1 -0
- package/dist/components/text-input/TextField.d.ts.map +1 -0
- package/dist/components/text-input/index.d.ts +8 -0
- package/dist/components/text-input/index.d.ts.map +1 -0
- package/dist/components/text-input/index.js +18 -0
- package/dist/components/text-input/index.js.map +1 -0
- package/dist/{state/index.js → index-D3WfkqVv.js} +15 -8
- package/dist/index-D3WfkqVv.js.map +1 -0
- package/dist/index.d.ts +10 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -376
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/validation/component-validation.d.ts +11 -2
- package/dist/validation/component-validation.d.ts.map +1 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +282 -191
- package/dist/validation/index.js.map +1 -0
- package/package.json +53 -39
- package/src/components/advanced/Slider.ts +722 -0
- package/src/components/advanced/Stepper.ts +715 -0
- package/src/components/advanced/index.ts +20 -0
- package/src/components/date-picker/DatePicker.ts +925 -0
- package/src/components/date-picker/index.ts +20 -0
- package/src/components/form-container/index.ts +266 -0
- package/src/components/selection/Checkbox.ts +478 -0
- package/src/components/selection/Radio.ts +470 -0
- package/src/components/selection/Select.ts +620 -0
- package/src/components/selection/index.ts +81 -0
- package/src/components/text-input/TextField.ts +728 -0
- package/src/components/text-input/index.ts +35 -0
- package/src/index.ts +48 -0
- package/src/state/index.ts +544 -0
- package/src/types/index.ts +579 -0
- package/src/utils/formatters.ts +184 -0
- package/src/utils/index.ts +57 -0
- package/src/validation/component-validation.ts +429 -0
- package/src/validation/index.ts +641 -0
- package/dist/TextField-CGBM3x7K.js +0 -1799
- package/dist/components/Form.d.ts +0 -76
- package/dist/components/Form.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -9
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -28
- package/dist/components/input/Checkbox.d.ts.map +0 -1
- package/dist/components/input/Radio.d.ts.map +0 -1
- package/dist/components/input/Select.d.ts.map +0 -1
- package/dist/components/input/TextField.d.ts.map +0 -1
- package/dist/components/input/index.d.ts +0 -11
- package/dist/components/input/index.d.ts.map +0 -1
- package/dist/utils/validators.d.ts +0 -101
- package/dist/utils/validators.d.ts.map +0 -1
- /package/dist/components/{input → selection}/Checkbox.d.ts +0 -0
- /package/dist/components/{input → selection}/Radio.d.ts +0 -0
- /package/dist/components/{input → selection}/Select.d.ts +0 -0
- /package/dist/components/{input → text-input}/TextField.d.ts +0 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TachUI Forms Validation System
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive validation engine with built-in rules, custom validators,
|
|
5
|
+
* async validation support, and i18n-ready error messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CustomValidationRule,
|
|
10
|
+
FieldState,
|
|
11
|
+
FieldValidation,
|
|
12
|
+
ValidationResult,
|
|
13
|
+
ValidationRule,
|
|
14
|
+
} from '../types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Built-in validation rules
|
|
18
|
+
*/
|
|
19
|
+
const VALIDATION_RULES: Record<
|
|
20
|
+
string,
|
|
21
|
+
(value: any, options?: any) => ValidationResult
|
|
22
|
+
> = {
|
|
23
|
+
required: (value: any): ValidationResult => ({
|
|
24
|
+
valid: value !== null && value !== undefined && value !== '',
|
|
25
|
+
message: 'This field is required',
|
|
26
|
+
code: 'REQUIRED',
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
email: (value: string): ValidationResult => {
|
|
30
|
+
if (!value) return { valid: true }
|
|
31
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
32
|
+
return {
|
|
33
|
+
valid: emailRegex.test(value),
|
|
34
|
+
message: 'Please enter a valid email address',
|
|
35
|
+
code: 'INVALID_EMAIL',
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
url: (value: string): ValidationResult => {
|
|
40
|
+
if (!value) return { valid: true }
|
|
41
|
+
try {
|
|
42
|
+
new URL(value)
|
|
43
|
+
return { valid: true }
|
|
44
|
+
} catch {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
message: 'Please enter a valid URL',
|
|
48
|
+
code: 'INVALID_URL',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
number: (value: any): ValidationResult => {
|
|
54
|
+
if (!value) return { valid: true }
|
|
55
|
+
const num = Number(value)
|
|
56
|
+
return {
|
|
57
|
+
valid: !Number.isNaN(num) && Number.isFinite(num),
|
|
58
|
+
message: 'Please enter a valid number',
|
|
59
|
+
code: 'INVALID_NUMBER',
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
integer: (value: any): ValidationResult => {
|
|
64
|
+
if (!value) return { valid: true }
|
|
65
|
+
const num = Number(value)
|
|
66
|
+
return {
|
|
67
|
+
valid:
|
|
68
|
+
!Number.isNaN(num) && Number.isFinite(num) && Number.isInteger(num),
|
|
69
|
+
message: 'Please enter a valid integer',
|
|
70
|
+
code: 'INVALID_INTEGER',
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
min: (value: any, options: { min: number }): ValidationResult => {
|
|
75
|
+
if (!value) return { valid: true }
|
|
76
|
+
const num = Number(value)
|
|
77
|
+
if (Number.isNaN(num)) return { valid: true } // Let number validation handle this
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
valid: num >= options.min,
|
|
81
|
+
message: `Value must be at least ${options.min}`,
|
|
82
|
+
code: 'MIN_VALUE',
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
max: (value: any, options: { max: number }): ValidationResult => {
|
|
87
|
+
if (!value) return { valid: true }
|
|
88
|
+
const num = Number(value)
|
|
89
|
+
if (Number.isNaN(num)) return { valid: true } // Let number validation handle this
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
valid: num <= options.max,
|
|
93
|
+
message: `Value must be at most ${options.max}`,
|
|
94
|
+
code: 'MAX_VALUE',
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
minLength: (
|
|
99
|
+
value: string,
|
|
100
|
+
options: { minLength: number }
|
|
101
|
+
): ValidationResult => {
|
|
102
|
+
if (!value) return { valid: true }
|
|
103
|
+
return {
|
|
104
|
+
valid: value.length >= options.minLength,
|
|
105
|
+
message: `Must be at least ${options.minLength} characters`,
|
|
106
|
+
code: 'MIN_LENGTH',
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
maxLength: (
|
|
111
|
+
value: string,
|
|
112
|
+
options: { maxLength: number }
|
|
113
|
+
): ValidationResult => {
|
|
114
|
+
if (!value) return { valid: true }
|
|
115
|
+
return {
|
|
116
|
+
valid: value.length <= options.maxLength,
|
|
117
|
+
message: `Must be at most ${options.maxLength} characters`,
|
|
118
|
+
code: 'MAX_LENGTH',
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
pattern: (
|
|
123
|
+
value: string,
|
|
124
|
+
options: { pattern: string | RegExp; message?: string }
|
|
125
|
+
): ValidationResult => {
|
|
126
|
+
if (!value) return { valid: true }
|
|
127
|
+
const regex =
|
|
128
|
+
typeof options.pattern === 'string'
|
|
129
|
+
? new RegExp(options.pattern)
|
|
130
|
+
: options.pattern
|
|
131
|
+
return {
|
|
132
|
+
valid: regex.test(value),
|
|
133
|
+
message: options.message || 'Value does not match required pattern',
|
|
134
|
+
code: 'PATTERN_MISMATCH',
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// Additional validation rules expected by tests
|
|
139
|
+
numeric: (value: any): ValidationResult => {
|
|
140
|
+
if (!value) return { valid: true }
|
|
141
|
+
const num = Number(value)
|
|
142
|
+
return {
|
|
143
|
+
valid: !Number.isNaN(num) && Number.isFinite(num),
|
|
144
|
+
message: 'Please enter a valid number',
|
|
145
|
+
code: 'INVALID_NUMERIC',
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
phone: (value: string): ValidationResult => {
|
|
150
|
+
if (!value) return { valid: true }
|
|
151
|
+
const phoneRegex = /^\+?[\d\s\-().]+$/
|
|
152
|
+
const digits = value.replace(/[\s\-().]/g, '')
|
|
153
|
+
return {
|
|
154
|
+
valid: phoneRegex.test(value) && digits.length >= 10,
|
|
155
|
+
message: 'Please enter a valid phone number',
|
|
156
|
+
code: 'INVALID_PHONE',
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
creditCard: (value: string): ValidationResult => {
|
|
161
|
+
if (!value) return { valid: true }
|
|
162
|
+
const digits = value.replace(/\s/g, '')
|
|
163
|
+
// Basic Luhn algorithm check
|
|
164
|
+
let sum = 0
|
|
165
|
+
let alternate = false
|
|
166
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
167
|
+
let n = parseInt(digits.charAt(i))
|
|
168
|
+
if (alternate) {
|
|
169
|
+
n *= 2
|
|
170
|
+
if (n > 9) n = (n % 10) + 1
|
|
171
|
+
}
|
|
172
|
+
sum += n
|
|
173
|
+
alternate = !alternate
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
valid: /^\d{13,19}$/.test(digits) && sum % 10 === 0,
|
|
177
|
+
message: 'Please enter a valid credit card number',
|
|
178
|
+
code: 'INVALID_CREDIT_CARD',
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
ssn: (value: string): ValidationResult => {
|
|
183
|
+
if (!value) return { valid: true }
|
|
184
|
+
const digits = value.replace(/[-\s]/g, '')
|
|
185
|
+
return {
|
|
186
|
+
valid: /^\d{9}$/.test(digits),
|
|
187
|
+
message: 'Please enter a valid Social Security Number',
|
|
188
|
+
code: 'INVALID_SSN',
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
postalCode: (value: string): ValidationResult => {
|
|
193
|
+
if (!value) return { valid: true }
|
|
194
|
+
// US ZIP code (5 or 9 digits) or international postal codes
|
|
195
|
+
const usZip = /^\d{5}(-\d{4})?$/
|
|
196
|
+
const intlPostal = /^[A-Z0-9]{3,10}$/i
|
|
197
|
+
return {
|
|
198
|
+
valid: usZip.test(value) || intlPostal.test(value),
|
|
199
|
+
message: 'Please enter a valid postal code',
|
|
200
|
+
code: 'INVALID_POSTAL_CODE',
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
zipCode: (value: string): ValidationResult => {
|
|
205
|
+
if (!value) return { valid: true }
|
|
206
|
+
// US ZIP code (5 or 9 digits)
|
|
207
|
+
const usZip = /^\d{5}(-\d{4})?$/
|
|
208
|
+
return {
|
|
209
|
+
valid: usZip.test(value),
|
|
210
|
+
message: 'Please enter a valid ZIP code',
|
|
211
|
+
code: 'INVALID_ZIP_CODE',
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
date: (value: string): ValidationResult => {
|
|
216
|
+
if (!value) return { valid: true }
|
|
217
|
+
const date = new Date(value)
|
|
218
|
+
return {
|
|
219
|
+
valid:
|
|
220
|
+
!Number.isNaN(date.getTime()) && !!value.match(/^\d{4}-\d{2}-\d{2}$/),
|
|
221
|
+
message: 'Please enter a valid date (YYYY-MM-DD)',
|
|
222
|
+
code: 'INVALID_DATE',
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
time: (value: string): ValidationResult => {
|
|
227
|
+
if (!value) return { valid: true }
|
|
228
|
+
return {
|
|
229
|
+
valid: /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test(value),
|
|
230
|
+
message: 'Please enter a valid time (HH:MM)',
|
|
231
|
+
code: 'INVALID_TIME',
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
strongPassword: (value: string): ValidationResult => {
|
|
236
|
+
if (!value) return { valid: true }
|
|
237
|
+
const hasUpperCase = /[A-Z]/.test(value)
|
|
238
|
+
const hasLowerCase = /[a-z]/.test(value)
|
|
239
|
+
const hasNumbers = /\d/.test(value)
|
|
240
|
+
const hasNonalphas = /\W/.test(value)
|
|
241
|
+
const isLongEnough = value.length >= 8
|
|
242
|
+
|
|
243
|
+
const valid =
|
|
244
|
+
hasUpperCase && hasLowerCase && hasNumbers && hasNonalphas && isLongEnough
|
|
245
|
+
return {
|
|
246
|
+
valid,
|
|
247
|
+
message: valid
|
|
248
|
+
? undefined
|
|
249
|
+
: 'Password must be at least 8 characters with uppercase, lowercase, number and special character',
|
|
250
|
+
code: 'WEAK_PASSWORD',
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Custom validation rule registry
|
|
257
|
+
*/
|
|
258
|
+
const customRules = new Map<string, CustomValidationRule>()
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Register a custom validation rule
|
|
262
|
+
*/
|
|
263
|
+
export function registerValidationRule(rule: CustomValidationRule): void {
|
|
264
|
+
customRules.set(rule.name, rule)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Unregister a custom validation rule
|
|
269
|
+
*/
|
|
270
|
+
export function unregisterValidationRule(name: string): void {
|
|
271
|
+
customRules.delete(name)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all registered validation rules
|
|
276
|
+
*/
|
|
277
|
+
export function getValidationRules(): string[] {
|
|
278
|
+
return [...Object.keys(VALIDATION_RULES), ...customRules.keys()]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Validate a single value against validation rules
|
|
283
|
+
*/
|
|
284
|
+
export function validateValue(
|
|
285
|
+
value: any,
|
|
286
|
+
rules: ValidationRule[],
|
|
287
|
+
options?: Record<string, any>
|
|
288
|
+
): ValidationResult {
|
|
289
|
+
for (const rule of rules) {
|
|
290
|
+
let result: ValidationResult
|
|
291
|
+
|
|
292
|
+
if (typeof rule === 'string') {
|
|
293
|
+
// Built-in rule
|
|
294
|
+
const validator = VALIDATION_RULES[rule]
|
|
295
|
+
if (validator) {
|
|
296
|
+
result = validator(value, options?.[rule])
|
|
297
|
+
} else {
|
|
298
|
+
// Check custom rules
|
|
299
|
+
const customRule = customRules.get(rule)
|
|
300
|
+
if (customRule) {
|
|
301
|
+
result = customRule.validate(value, customRule.options)
|
|
302
|
+
} else {
|
|
303
|
+
console.warn(`Unknown validation rule: ${rule}`)
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else if ('validate' in rule) {
|
|
308
|
+
// Custom validation rule object
|
|
309
|
+
result = rule.validate(value, rule.options)
|
|
310
|
+
} else {
|
|
311
|
+
// Built-in validation rule object (BuiltInValidationRule)
|
|
312
|
+
const validator = VALIDATION_RULES[rule.name]
|
|
313
|
+
if (validator) {
|
|
314
|
+
result = validator(value, rule.options)
|
|
315
|
+
} else {
|
|
316
|
+
console.warn(`Unknown built-in validation rule: ${rule.name}`)
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!result.valid) {
|
|
322
|
+
return result
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { valid: true }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Validate a field state
|
|
331
|
+
*/
|
|
332
|
+
export function validateField(
|
|
333
|
+
field: FieldState,
|
|
334
|
+
validation?: FieldValidation
|
|
335
|
+
): ValidationResult {
|
|
336
|
+
if (!validation?.rules || validation.rules.length === 0) {
|
|
337
|
+
return { valid: true }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return validateValue(field.value, validation.rules)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Async validation function
|
|
345
|
+
*/
|
|
346
|
+
export async function validateValueAsync(
|
|
347
|
+
value: any,
|
|
348
|
+
rules: ValidationRule[],
|
|
349
|
+
options?: Record<string, any>
|
|
350
|
+
): Promise<ValidationResult> {
|
|
351
|
+
for (const rule of rules) {
|
|
352
|
+
let result: ValidationResult | Promise<ValidationResult>
|
|
353
|
+
|
|
354
|
+
if (typeof rule === 'string') {
|
|
355
|
+
const validator = VALIDATION_RULES[rule]
|
|
356
|
+
if (validator) {
|
|
357
|
+
result = validator(value, options?.[rule])
|
|
358
|
+
} else {
|
|
359
|
+
const customRule = customRules.get(rule)
|
|
360
|
+
if (customRule) {
|
|
361
|
+
result = customRule.validate(value, customRule.options)
|
|
362
|
+
} else {
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} else if ('validate' in rule) {
|
|
367
|
+
// Custom validation rule object
|
|
368
|
+
result = rule.validate(value, rule.options)
|
|
369
|
+
} else {
|
|
370
|
+
// Built-in validation rule object (BuiltInValidationRule)
|
|
371
|
+
const validator = VALIDATION_RULES[rule.name]
|
|
372
|
+
if (validator) {
|
|
373
|
+
result = validator(value, rule.options)
|
|
374
|
+
} else {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Handle async validation
|
|
380
|
+
const validationResult = await Promise.resolve(result)
|
|
381
|
+
|
|
382
|
+
if (!validationResult.valid) {
|
|
383
|
+
return validationResult
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { valid: true }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Debounced validation
|
|
392
|
+
*/
|
|
393
|
+
export function createDebouncedValidator(
|
|
394
|
+
validator: (value: any) => ValidationResult | Promise<ValidationResult>,
|
|
395
|
+
delayMs: number = 300
|
|
396
|
+
) {
|
|
397
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
398
|
+
let lastValue: any
|
|
399
|
+
let lastResult: ValidationResult | Promise<ValidationResult>
|
|
400
|
+
|
|
401
|
+
return (value: any): Promise<ValidationResult> => {
|
|
402
|
+
return new Promise(resolve => {
|
|
403
|
+
// If value hasn't changed, return cached result
|
|
404
|
+
if (value === lastValue && lastResult) {
|
|
405
|
+
Promise.resolve(lastResult).then(resolve)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
lastValue = value
|
|
410
|
+
|
|
411
|
+
// Clear previous timeout
|
|
412
|
+
clearTimeout(timeoutId)
|
|
413
|
+
|
|
414
|
+
// Set new timeout
|
|
415
|
+
timeoutId = setTimeout(async () => {
|
|
416
|
+
try {
|
|
417
|
+
lastResult = validator(value)
|
|
418
|
+
const result = await Promise.resolve(lastResult)
|
|
419
|
+
resolve(result)
|
|
420
|
+
} catch (_error) {
|
|
421
|
+
resolve({
|
|
422
|
+
valid: false,
|
|
423
|
+
message: 'Validation error occurred',
|
|
424
|
+
code: 'VALIDATION_ERROR',
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
}, delayMs)
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Cross-field validation utilities
|
|
434
|
+
*/
|
|
435
|
+
export const CrossFieldValidators = {
|
|
436
|
+
/**
|
|
437
|
+
* Validate that two fields match (e.g., password confirmation)
|
|
438
|
+
*/
|
|
439
|
+
fieldMatch:
|
|
440
|
+
(field1: string, field2: string, message = 'Fields must match') =>
|
|
441
|
+
(values: Record<string, any>): ValidationResult => {
|
|
442
|
+
const isValid = values[field1] === values[field2]
|
|
443
|
+
if (isValid) {
|
|
444
|
+
return { valid: true }
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
valid: false,
|
|
448
|
+
message,
|
|
449
|
+
code: 'FIELD_MISMATCH',
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Validate that at least one field in a group is filled
|
|
455
|
+
*/
|
|
456
|
+
requireOneOf:
|
|
457
|
+
(fields: string[], message = 'At least one field is required') =>
|
|
458
|
+
(values: Record<string, any>): ValidationResult => {
|
|
459
|
+
const hasValue = fields.some(field => {
|
|
460
|
+
const value = values[field]
|
|
461
|
+
return value !== null && value !== undefined && value !== ''
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
if (hasValue) {
|
|
465
|
+
return { valid: true }
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
valid: false,
|
|
469
|
+
message,
|
|
470
|
+
code: 'REQUIRE_ONE_OF',
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Validate that a field is required when another field has a specific value
|
|
476
|
+
*/
|
|
477
|
+
requiredWhen:
|
|
478
|
+
(
|
|
479
|
+
targetField: string,
|
|
480
|
+
conditionalField: string,
|
|
481
|
+
conditionalValue: any,
|
|
482
|
+
message = `${targetField} is required`
|
|
483
|
+
) =>
|
|
484
|
+
(values: Record<string, any>): ValidationResult => {
|
|
485
|
+
if (values[conditionalField] === conditionalValue) {
|
|
486
|
+
const targetValue = values[targetField]
|
|
487
|
+
const isValid =
|
|
488
|
+
targetValue !== null &&
|
|
489
|
+
targetValue !== undefined &&
|
|
490
|
+
targetValue !== ''
|
|
491
|
+
if (isValid) {
|
|
492
|
+
return { valid: true }
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
valid: false,
|
|
496
|
+
message,
|
|
497
|
+
code: 'REQUIRED_WHEN',
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return { valid: true }
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Validate date range (start date before end date)
|
|
505
|
+
*/
|
|
506
|
+
dateRange:
|
|
507
|
+
(
|
|
508
|
+
startField: string,
|
|
509
|
+
endField: string,
|
|
510
|
+
message = 'End date must be after start date'
|
|
511
|
+
) =>
|
|
512
|
+
(values: Record<string, any>): ValidationResult => {
|
|
513
|
+
const startDate = values[startField]
|
|
514
|
+
const endDate = values[endField]
|
|
515
|
+
|
|
516
|
+
if (!startDate || !endDate) return { valid: true }
|
|
517
|
+
|
|
518
|
+
const start = new Date(startDate)
|
|
519
|
+
const end = new Date(endDate)
|
|
520
|
+
const isValid = end >= start
|
|
521
|
+
|
|
522
|
+
if (isValid) {
|
|
523
|
+
return { valid: true }
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
valid: false,
|
|
527
|
+
message,
|
|
528
|
+
code: 'INVALID_DATE_RANGE',
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Built-in validation rule presets
|
|
535
|
+
*/
|
|
536
|
+
export const ValidationPresets = {
|
|
537
|
+
email: ['required', 'email'],
|
|
538
|
+
password: ['required', { name: 'minLength', options: { minLength: 8 } }],
|
|
539
|
+
phone: [
|
|
540
|
+
'required',
|
|
541
|
+
{
|
|
542
|
+
name: 'pattern',
|
|
543
|
+
options: { pattern: /^\+?[\d\s-()]+$/, message: 'Invalid phone number' },
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
url: ['required', 'url'],
|
|
547
|
+
positiveNumber: ['required', 'number', { name: 'min', options: { min: 0 } }],
|
|
548
|
+
percentage: [
|
|
549
|
+
'required',
|
|
550
|
+
'number',
|
|
551
|
+
{ name: 'min', options: { min: 0 } },
|
|
552
|
+
{ name: 'max', options: { max: 100 } },
|
|
553
|
+
],
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Validation message formatter
|
|
558
|
+
*/
|
|
559
|
+
export class ValidationMessageFormatter {
|
|
560
|
+
private messages: Record<string, string> = {}
|
|
561
|
+
|
|
562
|
+
constructor(messages: Record<string, string> = {}) {
|
|
563
|
+
this.messages = messages
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
setMessage(code: string, message: string): void {
|
|
567
|
+
this.messages[code] = message
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
setMessages(messages: Record<string, string>): void {
|
|
571
|
+
this.messages = { ...this.messages, ...messages }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
format(result: ValidationResult, fieldName?: string): string {
|
|
575
|
+
if (result.code && this.messages[result.code]) {
|
|
576
|
+
return this.messages[result.code].replace(
|
|
577
|
+
`\${field}`,
|
|
578
|
+
fieldName || 'field'
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
return result.message || 'Validation failed'
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Default validation message formatter instance
|
|
587
|
+
*/
|
|
588
|
+
export const defaultMessageFormatter = new ValidationMessageFormatter()
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Validation utilities
|
|
592
|
+
*/
|
|
593
|
+
export const ValidationUtils = {
|
|
594
|
+
/**
|
|
595
|
+
* Check if a validation result represents an error
|
|
596
|
+
*/
|
|
597
|
+
isError: (result: ValidationResult): boolean => !result.valid,
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Check if a validation result is valid
|
|
601
|
+
*/
|
|
602
|
+
isValid: (result: ValidationResult): boolean => result.valid,
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Combine multiple validation results
|
|
606
|
+
*/
|
|
607
|
+
combineResults: (results: ValidationResult[]): ValidationResult => {
|
|
608
|
+
const errors = results.filter(r => !r.valid)
|
|
609
|
+
if (errors.length === 0) {
|
|
610
|
+
return { valid: true }
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
valid: false,
|
|
615
|
+
message: errors.map(e => e.message).join(', '),
|
|
616
|
+
code: 'MULTIPLE_ERRORS',
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Create a validation function from a rule configuration
|
|
622
|
+
*/
|
|
623
|
+
createValidator:
|
|
624
|
+
(rules: ValidationRule[], options?: Record<string, any>) =>
|
|
625
|
+
(value: any): ValidationResult =>
|
|
626
|
+
validateValue(value, rules, options),
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Create an async validation function
|
|
630
|
+
*/
|
|
631
|
+
createAsyncValidator:
|
|
632
|
+
(rules: ValidationRule[], options?: Record<string, any>) =>
|
|
633
|
+
(value: any): Promise<ValidationResult> =>
|
|
634
|
+
validateValueAsync(value, rules, options),
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Export all validation functionality
|
|
638
|
+
export { VALIDATION_RULES }
|
|
639
|
+
|
|
640
|
+
// Export component validation for plugin registration
|
|
641
|
+
export * from './component-validation'
|