@coherent.js/forms 1.1.2 → 2.0.0-rc.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.
@@ -5,273 +5,245 @@ function isEmailShaped(value) {
5
5
  return typeof value === "string" && value.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(value);
6
6
  }
7
7
 
8
- // src/validation.js
9
- var validators = {
10
- required: (message = "This field is required") => (value) => {
11
- if (value === null || value === void 0 || value === "") {
12
- return message;
13
- }
14
- return null;
15
- },
16
- minLength: (min, message = `Minimum length is ${min}`) => (value) => {
17
- if (value && value.length < min) {
18
- return message;
19
- }
20
- return null;
21
- },
22
- maxLength: (max, message = `Maximum length is ${max}`) => (value) => {
23
- if (value && value.length > max) {
24
- return message;
25
- }
26
- return null;
27
- },
28
- min: (min, message = `Minimum value is ${min}`) => (value) => {
29
- if (value !== null && value !== void 0 && Number(value) < min) {
30
- return message;
31
- }
32
- return null;
33
- },
34
- max: (max, message = `Maximum value is ${max}`) => (value) => {
35
- if (value !== null && value !== void 0 && Number(value) > max) {
36
- return message;
37
- }
38
- return null;
39
- },
40
- email: (message = "Invalid email address") => (value) => {
41
- if (value && !isEmailShaped(value)) {
42
- return message;
43
- }
44
- return null;
45
- },
46
- url: (message = "Invalid URL") => (value) => {
47
- if (value) {
48
- try {
49
- new URL(value);
50
- } catch {
51
- return message;
52
- }
53
- }
54
- return null;
55
- },
56
- pattern: (regex, message = "Invalid format") => (value) => {
57
- if (value && !regex.test(value)) {
58
- return message;
59
- }
60
- return null;
61
- },
62
- matches: (fieldName, message = "Fields do not match") => (value, formData) => {
63
- if (value !== formData[fieldName]) {
64
- return message;
65
- }
66
- return null;
67
- },
68
- oneOf: (options, message = "Invalid option") => (value) => {
69
- if (value && !options.includes(value)) {
70
- return message;
71
- }
72
- return null;
73
- },
74
- custom: (fn, message = "Validation failed") => (value, formData) => {
75
- if (!fn(value, formData)) {
76
- return message;
77
- }
78
- return null;
8
+ // src/rules.js
9
+ var BUILTIN = /* @__PURE__ */ Symbol("coherent.forms.builtin");
10
+ var DESCRIPTORS = /* @__PURE__ */ new WeakMap();
11
+ var HELPER_NAMES = /* @__PURE__ */ new Set(["get", "compose", "debounce", "cancellable", "when", "chain"]);
12
+ var isEmpty = (value) => value === null || value === void 0 || value === "";
13
+ var isOptionsObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof RegExp);
14
+ function argsFromString(params, kind, text) {
15
+ if (text === void 0 || text.trim() === "") return [];
16
+ if (params.length === 0) return [text.trim()];
17
+ if (kind === "list") {
18
+ return [text.split(",").map((part) => part.trim()).filter(Boolean)];
79
19
  }
80
- };
81
-
82
- // src/validators.js
83
- var validators2 = {
84
- required: (value, options = {}) => {
85
- if (value === null || value === void 0 || value === "") {
86
- return options.message || validators2.required.message || "This field is required";
87
- }
88
- return null;
89
- },
90
- email: (value) => {
91
- if (!value) return null;
92
- if (!isEmailShaped(value)) {
93
- return "Please enter a valid email address";
94
- }
95
- return null;
96
- },
97
- minLength: (value, options = {}) => {
98
- if (!value) return null;
99
- const min = options.min || 0;
100
- if (value.length < min) {
101
- return options.message || `Must be at least ${min} characters`;
102
- }
103
- return null;
104
- },
105
- maxLength: (value, options = {}) => {
106
- if (!value) return null;
107
- const max = options.max || Infinity;
108
- if (value.length > max) {
109
- return options.message || `Must be no more than ${max} characters`;
110
- }
111
- return null;
112
- },
113
- min: (value, options = {}) => {
114
- if (value === null || value === void 0 || value === "") return null;
115
- const num = Number(value);
116
- const minValue = options.min || 0;
117
- if (isNaN(num) || num < minValue) {
118
- return options.message || `Must be at least ${minValue}`;
119
- }
120
- return null;
121
- },
122
- max: (value, options = {}) => {
123
- if (value === null || value === void 0 || value === "") return null;
124
- const num = Number(value);
125
- const maxValue = options.max || Infinity;
126
- if (isNaN(num) || num > maxValue) {
127
- return options.message || `Must be no more than ${maxValue}`;
128
- }
129
- return null;
130
- },
131
- pattern: (value, options = {}) => {
132
- if (!value) return null;
133
- const regex = options.pattern || options.regex;
134
- if (regex && !regex.test(value)) {
135
- return options.message || "Invalid format";
136
- }
137
- return null;
138
- },
139
- url: (value) => {
140
- if (!value) return null;
20
+ if (kind === "regexp") {
141
21
  try {
142
- new URL(value);
143
- return null;
22
+ return [new RegExp(text)];
144
23
  } catch {
145
- return "Please enter a valid URL";
146
- }
147
- },
148
- number: (value) => {
149
- if (value === null || value === void 0 || value === "") return null;
150
- if (isNaN(Number(value))) {
151
- return "Must be a valid number";
152
- }
153
- return null;
154
- },
155
- integer: (value) => {
156
- if (value === null || value === void 0 || value === "") return null;
157
- const num = Number(value);
158
- if (isNaN(num) || !Number.isInteger(num)) {
159
- return "Must be a whole number";
160
- }
161
- return null;
162
- },
163
- phone: (value) => {
164
- if (!value) return null;
165
- const phoneRegex = /^[\d\s\-\+\(\)]+$/;
166
- if (!phoneRegex.test(value) || value.replace(/\D/g, "").length < 10) {
167
- return "Please enter a valid phone number";
168
- }
169
- return null;
170
- },
171
- date: (value) => {
172
- if (!value) return null;
173
- const date = new Date(value);
174
- if (isNaN(date.getTime())) {
175
- return "Please enter a valid date";
176
- }
177
- return null;
178
- },
179
- match: (value, options = {}, translator, allValues = {}) => {
180
- if (!value) return null;
181
- const fieldName = options.field || options.fieldName;
182
- if (value !== allValues[fieldName]) {
183
- return options.message || `Must match ${fieldName}`;
184
- }
185
- return null;
186
- },
187
- custom: (value, options = {}, translator, allValues) => {
188
- const validatorFn = options.validator || options.fn;
189
- if (!validatorFn) return null;
190
- const isValid = validatorFn(value, allValues);
191
- return isValid ? null : options.message || "Validation failed";
192
- },
193
- fileType: (value, options = {}) => {
194
- if (!value) return null;
195
- const allowedTypes = options.accept || options.types || [];
196
- if (value.type !== void 0) {
197
- const fileType = value.type;
198
- const fileExt = value.name ? value.name.split(".").pop().toLowerCase() : "";
199
- const isValid = allowedTypes.some((type) => {
200
- if (type.startsWith(".")) {
201
- return fileExt === type.slice(1).toLowerCase();
202
- }
203
- if (type.includes("/")) {
204
- if (type.endsWith("/*")) {
205
- return fileType.startsWith(type.replace("/*", "/"));
206
- }
207
- return fileType === type;
208
- }
209
- return fileExt === type.toLowerCase();
210
- });
211
- if (!isValid) {
212
- return options.message || `File type must be one of: ${allowedTypes.join(", ")}`;
213
- }
214
24
  return null;
215
25
  }
216
- return null;
217
- },
218
- fileSize: (value, options = {}) => {
219
- if (!value) return null;
220
- const maxSize = options.maxSize || Infinity;
221
- if (value.size !== void 0) {
222
- if (value.size > maxSize) {
223
- const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
224
- return options.message || `File size must be less than ${maxSizeMB}MB`;
225
- }
226
- return null;
26
+ }
27
+ const comma = text.indexOf(",");
28
+ const raw = (comma === -1 ? text : text.slice(0, comma)).trim();
29
+ const message = comma === -1 ? "" : text.slice(comma + 1).trim();
30
+ let arg = raw;
31
+ if (kind === "number") {
32
+ arg = Number(raw);
33
+ if (raw === "" || Number.isNaN(arg)) return null;
34
+ }
35
+ return message ? [arg, message] : [arg];
36
+ }
37
+ function defineRule(name, { params = [], stringArg = "string", valid, message }) {
38
+ const fromOptions = (options = {}) => {
39
+ const config = { message: options.message };
40
+ for (const [key, ...aliases] of params) {
41
+ config[key] = [key, ...aliases].map((alias) => options[alias]).find((v) => v !== void 0);
227
42
  }
228
- return null;
229
- },
230
- fileExtension: (value, options = {}) => {
231
- if (!value) return null;
232
- const allowedExtensions = options.extensions || [];
233
- const fileName = value.name || value;
234
- const ext = `.${fileName.split(".").pop().toLowerCase()}`;
235
- const isValid = allowedExtensions.some((allowed) => {
236
- return ext === allowed.toLowerCase();
43
+ return config;
44
+ };
45
+ const check = (value, config, formData) => {
46
+ if (valid(value, config, formData || {})) return null;
47
+ return config.message || builtin.message || message(config);
48
+ };
49
+ const factory = (...args) => {
50
+ const config = { message: args[params.length] };
51
+ params.forEach(([key], index) => {
52
+ config[key] = args[index];
237
53
  });
238
- if (!isValid) {
239
- return options.message || `File extension must be one of: ${allowedExtensions.join(", ")}`;
54
+ const rule = (value, formData, _translator, allValues) => check(value, config, allValues ?? formData);
55
+ DESCRIPTORS.set(rule, { name, args });
56
+ return rule;
57
+ };
58
+ const isDirectCall = (args) => {
59
+ if (args.length >= 2) {
60
+ return params.length === 0 || isOptionsObject(args[1]);
240
61
  }
241
- return null;
242
- },
243
- alpha: (value) => {
244
- if (!value) return null;
245
- const alphaRegex = /^[a-zA-Z]+$/;
246
- if (!alphaRegex.test(value)) {
247
- return "Must contain only letters";
62
+ if (args.length === 1 && params.length === 0) {
63
+ const [arg] = args;
64
+ return arg === "" || arg !== void 0 && arg !== null && typeof arg !== "string";
248
65
  }
249
- return null;
250
- },
251
- alphanumeric: (value) => {
252
- if (!value) return null;
253
- const alphanumericRegex = /^[a-zA-Z0-9]+$/;
254
- if (!alphanumericRegex.test(value)) {
255
- return "Must contain only letters and numbers";
66
+ return false;
67
+ };
68
+ function builtin(...args) {
69
+ if (isDirectCall(args)) {
70
+ const [value, options, , allValues] = args;
71
+ return check(value, fromOptions(isOptionsObject(options) ? options : {}), allValues);
256
72
  }
257
- return null;
258
- },
259
- uppercase: (value) => {
260
- if (!value) return null;
261
- if (value !== value.toUpperCase()) {
262
- return "Must be uppercase";
73
+ return factory(...args);
74
+ }
75
+ Object.defineProperty(builtin, "name", { value: name });
76
+ builtin[BUILTIN] = {
77
+ name,
78
+ factory,
79
+ argsFromString: (text) => argsFromString(params, stringArg, text)
80
+ };
81
+ return builtin;
82
+ }
83
+ var toNumber = (value) => typeof value === "number" ? value : Number(value);
84
+ function testRegExp(regex, value) {
85
+ if (!(regex instanceof RegExp)) return true;
86
+ regex.lastIndex = 0;
87
+ return regex.test(String(value));
88
+ }
89
+ function fileMatchesType(file, allowedTypes) {
90
+ const fileType = file.type;
91
+ const fileExt = file.name ? file.name.split(".").pop().toLowerCase() : "";
92
+ return allowedTypes.some((type) => {
93
+ if (type.startsWith(".")) {
94
+ return fileExt === type.slice(1).toLowerCase();
95
+ }
96
+ if (type.includes("/")) {
97
+ if (type.endsWith("/*")) {
98
+ return fileType.startsWith(type.replace("/*", "/"));
99
+ }
100
+ return fileType === type;
263
101
  }
264
- return null;
265
- },
266
- // Get a registered validator
267
- get: (name) => {
268
- return validators2[name];
269
- },
270
- // Compose multiple validators
102
+ return fileExt === type.toLowerCase();
103
+ });
104
+ }
105
+ var validators = {
106
+ required: defineRule("required", {
107
+ valid: (value) => !isEmpty(value),
108
+ message: () => "This field is required"
109
+ }),
110
+ email: defineRule("email", {
111
+ valid: (value) => isEmpty(value) || isEmailShaped(value),
112
+ message: () => "Invalid email address"
113
+ }),
114
+ url: defineRule("url", {
115
+ valid: (value) => {
116
+ if (isEmpty(value)) return true;
117
+ try {
118
+ new URL(value);
119
+ return true;
120
+ } catch {
121
+ return false;
122
+ }
123
+ },
124
+ message: () => "Invalid URL"
125
+ }),
126
+ minLength: defineRule("minLength", {
127
+ params: [["min", "minLength"]],
128
+ stringArg: "number",
129
+ valid: (value, { min }) => !value || !(value.length < (min ?? 0)),
130
+ message: ({ min }) => `Minimum length is ${min}`
131
+ }),
132
+ maxLength: defineRule("maxLength", {
133
+ params: [["max", "maxLength"]],
134
+ stringArg: "number",
135
+ valid: (value, { max }) => !value || !(value.length > (max ?? Infinity)),
136
+ message: ({ max }) => `Maximum length is ${max}`
137
+ }),
138
+ // Empty values pass (combine with `required`); a non-numeric value fails.
139
+ min: defineRule("min", {
140
+ params: [["min"]],
141
+ stringArg: "number",
142
+ valid: (value, { min }) => {
143
+ if (isEmpty(value)) return true;
144
+ const number = toNumber(value);
145
+ return !Number.isNaN(number) && !(number < (min ?? -Infinity));
146
+ },
147
+ message: ({ min }) => `Minimum value is ${min}`
148
+ }),
149
+ max: defineRule("max", {
150
+ params: [["max"]],
151
+ stringArg: "number",
152
+ valid: (value, { max }) => {
153
+ if (isEmpty(value)) return true;
154
+ const number = toNumber(value);
155
+ return !Number.isNaN(number) && !(number > (max ?? Infinity));
156
+ },
157
+ message: ({ max }) => `Maximum value is ${max}`
158
+ }),
159
+ pattern: defineRule("pattern", {
160
+ params: [["pattern", "regex"]],
161
+ stringArg: "regexp",
162
+ valid: (value, { pattern }) => isEmpty(value) || testRegExp(pattern, value),
163
+ message: () => "Invalid format"
164
+ }),
165
+ /** Equal to another field, even when empty */
166
+ matches: defineRule("matches", {
167
+ params: [["field", "fieldName"]],
168
+ valid: (value, { field }, formData) => value === formData[field],
169
+ message: () => "Fields do not match"
170
+ }),
171
+ /** Equal to another field; an empty value passes */
172
+ match: defineRule("match", {
173
+ params: [["field", "fieldName"]],
174
+ valid: (value, { field }, formData) => !value || value === formData[field],
175
+ message: ({ field }) => `Must match ${field}`
176
+ }),
177
+ oneOf: defineRule("oneOf", {
178
+ params: [["options", "values"]],
179
+ stringArg: "list",
180
+ valid: (value, { options }) => !value || !Array.isArray(options) || options.includes(value),
181
+ message: () => "Invalid option"
182
+ }),
183
+ custom: defineRule("custom", {
184
+ params: [["validator", "fn"]],
185
+ valid: (value, { validator }, formData) => typeof validator !== "function" || Boolean(validator(value, formData)),
186
+ message: () => "Validation failed"
187
+ }),
188
+ number: defineRule("number", {
189
+ valid: (value) => isEmpty(value) || !Number.isNaN(Number(value)),
190
+ message: () => "Must be a valid number"
191
+ }),
192
+ integer: defineRule("integer", {
193
+ valid: (value) => isEmpty(value) || Number.isInteger(Number(value)),
194
+ message: () => "Must be a whole number"
195
+ }),
196
+ phone: defineRule("phone", {
197
+ valid: (value) => !value || /^[\d\s\-+()]+$/.test(value) && value.replace(/\D/g, "").length >= 10,
198
+ message: () => "Please enter a valid phone number"
199
+ }),
200
+ date: defineRule("date", {
201
+ valid: (value) => !value || !Number.isNaN(new Date(value).getTime()),
202
+ message: () => "Please enter a valid date"
203
+ }),
204
+ alpha: defineRule("alpha", {
205
+ valid: (value) => !value || /^[a-zA-Z]+$/.test(value),
206
+ message: () => "Must contain only letters"
207
+ }),
208
+ alphanumeric: defineRule("alphanumeric", {
209
+ valid: (value) => !value || /^[a-zA-Z0-9]+$/.test(value),
210
+ message: () => "Must contain only letters and numbers"
211
+ }),
212
+ uppercase: defineRule("uppercase", {
213
+ valid: (value) => !value || value === String(value).toUpperCase(),
214
+ message: () => "Must be uppercase"
215
+ }),
216
+ fileType: defineRule("fileType", {
217
+ params: [["accept", "types"]],
218
+ stringArg: "list",
219
+ valid: (value, { accept }) => !value || value.type === void 0 || fileMatchesType(value, accept || []),
220
+ message: ({ accept }) => `File type must be one of: ${(accept || []).join(", ")}`
221
+ }),
222
+ fileSize: defineRule("fileSize", {
223
+ params: [["maxSize"]],
224
+ stringArg: "number",
225
+ valid: (value, { maxSize }) => !value || value.size === void 0 || !(value.size > (maxSize ?? Infinity)),
226
+ message: ({ maxSize }) => `File size must be less than ${((maxSize ?? Infinity) / (1024 * 1024)).toFixed(2)}MB`
227
+ }),
228
+ fileExtension: defineRule("fileExtension", {
229
+ params: [["extensions"]],
230
+ stringArg: "list",
231
+ valid: (value, { extensions }) => {
232
+ if (!value) return true;
233
+ const fileName = value.name || value;
234
+ const ext = `.${String(fileName).split(".").pop().toLowerCase()}`;
235
+ return (extensions || []).some((allowed) => ext === allowed.toLowerCase());
236
+ },
237
+ message: ({ extensions }) => `File extension must be one of: ${(extensions || []).join(", ")}`
238
+ }),
239
+ /** A registered validator or built-in by name */
240
+ get: (name) => Object.hasOwn(validators, name) ? validators[name] : void 0,
241
+ /** Combine validators into one returning the first error */
271
242
  compose: (validatorList) => {
243
+ const rules = validatorList.map(resolveValidator);
272
244
  return (value, options, translator, allValues) => {
273
- for (const validator of validatorList) {
274
- const error = typeof validator === "function" ? validator(value, options, translator, allValues) : null;
245
+ for (const rule of rules) {
246
+ const error = rule ? rule(value, options, translator, allValues) : null;
275
247
  if (error) {
276
248
  return error;
277
249
  }
@@ -279,7 +251,7 @@ var validators2 = {
279
251
  return null;
280
252
  };
281
253
  },
282
- // Debounce async validator
254
+ /** Debounce an async validator */
283
255
  debounce: (validator, delay = 300) => {
284
256
  let timeoutId;
285
257
  return (value) => {
@@ -292,7 +264,7 @@ var validators2 = {
292
264
  });
293
265
  };
294
266
  },
295
- // Cancellable async validator
267
+ /** Cancellable async validator */
296
268
  cancellable: (validator) => {
297
269
  let abortController;
298
270
  const wrapped = async (value) => {
@@ -316,38 +288,31 @@ var validators2 = {
316
288
  };
317
289
  return wrapped;
318
290
  },
319
- // Conditional validator
291
+ /** Run `validator` only when `condition` holds */
320
292
  when: (condition, validator) => {
293
+ const rule = resolveValidator(validator);
321
294
  return (value, options = {}, translator, allValues = {}) => {
322
295
  const context = options.min !== void 0 || options.max !== void 0 ? allValues : options;
323
296
  const shouldValidate = typeof condition === "function" ? condition(value, context) : condition;
324
297
  if (!shouldValidate) {
325
298
  return null;
326
299
  }
327
- return typeof validator === "function" ? validator(value, options, translator, allValues) : null;
300
+ return rule ? rule(value, options, translator, allValues) : null;
328
301
  };
329
302
  },
330
- // Validator chain builder
303
+ /** Validator chain builder */
331
304
  chain: (options = {}) => {
332
305
  const validatorList = [];
333
306
  const stopOnFirstError = options.stopOnFirstError !== false;
307
+ const direct = (name) => (opts) => {
308
+ validatorList.push((v, o, t, a) => validators[name](v, opts || o || {}, t, a));
309
+ return chain;
310
+ };
334
311
  const chain = {
335
- required: (opts) => {
336
- validatorList.push((v, o, t, a) => validators2.required(v, opts || o, t, a));
337
- return chain;
338
- },
339
- email: (opts) => {
340
- validatorList.push((v, o, t, a) => validators2.email(v, opts || o, t, a));
341
- return chain;
342
- },
343
- minLength: (opts) => {
344
- validatorList.push((v, o, t, a) => validators2.minLength(v, opts || o, t, a));
345
- return chain;
346
- },
347
- maxLength: (opts) => {
348
- validatorList.push((v, o, t, a) => validators2.maxLength(v, opts || o, t, a));
349
- return chain;
350
- },
312
+ required: direct("required"),
313
+ email: direct("email"),
314
+ minLength: direct("minLength"),
315
+ maxLength: direct("maxLength"),
351
316
  custom: (fn, message) => {
352
317
  validatorList.push((v, o, t, a) => {
353
318
  const result = fn(v, a);
@@ -364,24 +329,50 @@ var validators2 = {
364
329
  }
365
330
  }
366
331
  return null;
367
- } else {
368
- const errors = [];
369
- for (const validator of validatorList) {
370
- const error = validator(value, opts, translator, allValues);
371
- if (error) {
372
- errors.push(error);
373
- }
332
+ }
333
+ const errors = [];
334
+ for (const validator of validatorList) {
335
+ const error = validator(value, opts, translator, allValues);
336
+ if (error) {
337
+ errors.push(error);
374
338
  }
375
- return errors.length > 0 ? errors : null;
376
339
  }
340
+ return errors.length > 0 ? errors : null;
377
341
  }
378
342
  };
379
343
  return chain;
380
344
  }
381
345
  };
346
+ function lookup(name) {
347
+ const found = Object.hasOwn(validators, name) && !HELPER_NAMES.has(name) ? validators[name] : null;
348
+ return typeof found === "function" ? found : null;
349
+ }
350
+ function resolveString(entry) {
351
+ const exact = lookup(entry);
352
+ const colon = exact ? -1 : entry.indexOf(":");
353
+ const name = colon === -1 ? entry : entry.slice(0, colon).trim();
354
+ const found = exact ?? lookup(name);
355
+ if (!found) return null;
356
+ const text = colon === -1 ? void 0 : entry.slice(colon + 1);
357
+ if (!found[BUILTIN]) {
358
+ return text === void 0 ? { name, args: [], validator: found } : null;
359
+ }
360
+ const args = found[BUILTIN].argsFromString(text);
361
+ if (!args) return null;
362
+ return { name: found[BUILTIN].name, args, validator: found[BUILTIN].factory(...args) };
363
+ }
364
+ function resolveValidator(entry) {
365
+ if (typeof entry === "string") {
366
+ return resolveString(entry)?.validator ?? null;
367
+ }
368
+ if (typeof entry !== "function") return null;
369
+ return entry[BUILTIN] ? entry[BUILTIN].factory() : entry;
370
+ }
382
371
  function validateField(value, validatorList, formData = {}) {
383
- for (const validator of validatorList) {
384
- const error = validator(value, formData);
372
+ const list = Array.isArray(validatorList) ? validatorList : [validatorList];
373
+ for (const entry of list) {
374
+ const validator = resolveValidator(entry);
375
+ const error = validator ? validator(value, formData) : null;
385
376
  if (error) {
386
377
  return error;
387
378
  }
@@ -399,7 +390,7 @@ function validateForm(formData, fieldValidators) {
399
390
  }
400
391
  return Object.keys(errors).length > 0 ? errors : null;
401
392
  }
402
- function createValidator(validatorFn, message) {
393
+ function wrapValidator(validatorFn, message) {
403
394
  return (value, options, translator, allValues) => {
404
395
  const result = validatorFn(value, options, translator, allValues);
405
396
  if (typeof result === "string") {
@@ -412,22 +403,119 @@ function createValidator(validatorFn, message) {
412
403
  };
413
404
  }
414
405
  function registerValidator(name, validatorFn) {
415
- validators2[name] = validatorFn;
416
406
  validators[name] = validatorFn;
407
+ if (typeof validatorFn === "function" && !validatorFn[BUILTIN]) {
408
+ DESCRIPTORS.set(validatorFn, { name, args: [] });
409
+ }
417
410
  }
418
411
  function composeValidators(...validatorFns) {
419
- return (value, options, translator, allValues) => {
420
- for (const validator of validatorFns) {
421
- const error = validator(value, options, translator, allValues);
412
+ return validators.compose(validatorFns);
413
+ }
414
+
415
+ // src/validation.js
416
+ var FormValidator = class {
417
+ constructor(schema = {}) {
418
+ this.schema = schema;
419
+ this.errors = {};
420
+ this.touched = {};
421
+ }
422
+ /**
423
+ * Validate a single field
424
+ */
425
+ validateField(name, value, formData = {}) {
426
+ const fieldValidators = this.schema[name];
427
+ if (!fieldValidators) {
428
+ return null;
429
+ }
430
+ const validatorArray = Array.isArray(fieldValidators) ? fieldValidators : [fieldValidators];
431
+ for (const entry of validatorArray) {
432
+ const validator = resolveValidator(entry);
433
+ const error = validator ? validator(value, formData) : null;
422
434
  if (error) {
423
435
  return error;
424
436
  }
425
437
  }
426
438
  return null;
427
- };
439
+ }
440
+ /**
441
+ * Validate entire form
442
+ */
443
+ validate(formData) {
444
+ const errors = {};
445
+ let isValid = true;
446
+ for (const [name, value] of Object.entries(formData)) {
447
+ const error = this.validateField(name, value, formData);
448
+ if (error) {
449
+ errors[name] = error;
450
+ isValid = false;
451
+ }
452
+ }
453
+ for (const name of Object.keys(this.schema)) {
454
+ if (!(name in formData)) {
455
+ const error = this.validateField(name, void 0, formData);
456
+ if (error) {
457
+ errors[name] = error;
458
+ isValid = false;
459
+ }
460
+ }
461
+ }
462
+ this.errors = errors;
463
+ return { isValid, errors };
464
+ }
465
+ /**
466
+ * Mark field as touched
467
+ */
468
+ touch(name) {
469
+ this.touched[name] = true;
470
+ }
471
+ /**
472
+ * Check if field is touched
473
+ */
474
+ isTouched(name) {
475
+ return this.touched[name] || false;
476
+ }
477
+ /**
478
+ * Get error for field
479
+ */
480
+ getError(name) {
481
+ return this.errors[name] || null;
482
+ }
483
+ /**
484
+ * Check if field has error
485
+ */
486
+ hasError(name) {
487
+ return !!this.errors[name];
488
+ }
489
+ /**
490
+ * Clear errors
491
+ */
492
+ clearErrors() {
493
+ this.errors = {};
494
+ }
495
+ /**
496
+ * Clear touched state
497
+ */
498
+ clearTouched() {
499
+ this.touched = {};
500
+ }
501
+ /**
502
+ * Reset validator
503
+ */
504
+ reset() {
505
+ this.clearErrors();
506
+ this.clearTouched();
507
+ }
508
+ };
509
+ function createValidator(schemaOrFn, message) {
510
+ if (typeof schemaOrFn === "function") {
511
+ return wrapValidator(schemaOrFn, message);
512
+ }
513
+ return new FormValidator(schemaOrFn);
428
514
  }
515
+
516
+ // src/validators.js
429
517
  var validators_default = {
430
- validators: validators2,
518
+ validators,
431
519
  validateField,
432
520
  validateForm,
433
521
  createValidator,
@@ -441,6 +529,6 @@ export {
441
529
  registerValidator,
442
530
  validateField,
443
531
  validateForm,
444
- validators2 as validators
532
+ validators
445
533
  };
446
534
  //# sourceMappingURL=validators.js.map