@coherent.js/forms 1.1.0 → 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.
- package/README.md +162 -243
- package/dist/csrf.js +57 -0
- package/dist/csrf.js.map +7 -0
- package/dist/form-builder.js +483 -27
- package/dist/form-builder.js.map +3 -3
- package/dist/form-hydration.js +317 -222
- package/dist/form-hydration.js.map +3 -3
- package/dist/index.js +675 -555
- package/dist/index.js.map +4 -4
- package/dist/validation.js +376 -61
- package/dist/validation.js.map +3 -3
- package/dist/validators.js +388 -294
- package/dist/validators.js.map +4 -4
- package/package.json +6 -3
- package/types/csrf.d.ts +45 -0
- package/types/index.d.ts +185 -41
package/dist/validators.js
CHANGED
|
@@ -1,271 +1,249 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return null;
|
|
8
|
-
},
|
|
9
|
-
minLength: (min, message = `Minimum length is ${min}`) => (value) => {
|
|
10
|
-
if (value && value.length < min) {
|
|
11
|
-
return message;
|
|
12
|
-
}
|
|
13
|
-
return null;
|
|
14
|
-
},
|
|
15
|
-
maxLength: (max, message = `Maximum length is ${max}`) => (value) => {
|
|
16
|
-
if (value && value.length > max) {
|
|
17
|
-
return message;
|
|
18
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
},
|
|
21
|
-
min: (min, message = `Minimum value is ${min}`) => (value) => {
|
|
22
|
-
if (value !== null && value !== void 0 && Number(value) < min) {
|
|
23
|
-
return message;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
},
|
|
27
|
-
max: (max, message = `Maximum value is ${max}`) => (value) => {
|
|
28
|
-
if (value !== null && value !== void 0 && Number(value) > max) {
|
|
29
|
-
return message;
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
},
|
|
33
|
-
email: (message = "Invalid email address") => (value) => {
|
|
34
|
-
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
35
|
-
return message;
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
},
|
|
39
|
-
url: (message = "Invalid URL") => (value) => {
|
|
40
|
-
if (value) {
|
|
41
|
-
try {
|
|
42
|
-
new URL(value);
|
|
43
|
-
} catch {
|
|
44
|
-
return message;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
},
|
|
49
|
-
pattern: (regex, message = "Invalid format") => (value) => {
|
|
50
|
-
if (value && !regex.test(value)) {
|
|
51
|
-
return message;
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
},
|
|
55
|
-
matches: (fieldName, message = "Fields do not match") => (value, formData) => {
|
|
56
|
-
if (value !== formData[fieldName]) {
|
|
57
|
-
return message;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
},
|
|
61
|
-
oneOf: (options, message = "Invalid option") => (value) => {
|
|
62
|
-
if (value && !options.includes(value)) {
|
|
63
|
-
return message;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
},
|
|
67
|
-
custom: (fn, message = "Validation failed") => (value, formData) => {
|
|
68
|
-
if (!fn(value, formData)) {
|
|
69
|
-
return message;
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
1
|
+
// src/patterns.js
|
|
2
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
|
|
3
|
+
var EMAIL_MAX_LENGTH = 254;
|
|
4
|
+
function isEmailShaped(value) {
|
|
5
|
+
return typeof value === "string" && value.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(value);
|
|
6
|
+
}
|
|
74
7
|
|
|
75
|
-
// src/
|
|
76
|
-
var
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
},
|
|
91
|
-
minLength: (value, options = {}) => {
|
|
92
|
-
if (!value) return null;
|
|
93
|
-
const min = options.min || 0;
|
|
94
|
-
if (value.length < min) {
|
|
95
|
-
return options.message || `Must be at least ${min} characters`;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
},
|
|
99
|
-
maxLength: (value, options = {}) => {
|
|
100
|
-
if (!value) return null;
|
|
101
|
-
const max = options.max || Infinity;
|
|
102
|
-
if (value.length > max) {
|
|
103
|
-
return options.message || `Must be no more than ${max} characters`;
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
},
|
|
107
|
-
min: (value, options = {}) => {
|
|
108
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
109
|
-
const num = Number(value);
|
|
110
|
-
const minValue = options.min || 0;
|
|
111
|
-
if (isNaN(num) || num < minValue) {
|
|
112
|
-
return options.message || `Must be at least ${minValue}`;
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
},
|
|
116
|
-
max: (value, options = {}) => {
|
|
117
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
118
|
-
const num = Number(value);
|
|
119
|
-
const maxValue = options.max || Infinity;
|
|
120
|
-
if (isNaN(num) || num > maxValue) {
|
|
121
|
-
return options.message || `Must be no more than ${maxValue}`;
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
},
|
|
125
|
-
pattern: (value, options = {}) => {
|
|
126
|
-
if (!value) return null;
|
|
127
|
-
const regex = options.pattern || options.regex;
|
|
128
|
-
if (regex && !regex.test(value)) {
|
|
129
|
-
return options.message || "Invalid format";
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
},
|
|
133
|
-
url: (value) => {
|
|
134
|
-
if (!value) 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)];
|
|
19
|
+
}
|
|
20
|
+
if (kind === "regexp") {
|
|
135
21
|
try {
|
|
136
|
-
new
|
|
137
|
-
return null;
|
|
22
|
+
return [new RegExp(text)];
|
|
138
23
|
} catch {
|
|
139
|
-
return "Please enter a valid URL";
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
number: (value) => {
|
|
143
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
144
|
-
if (isNaN(Number(value))) {
|
|
145
|
-
return "Must be a valid number";
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
},
|
|
149
|
-
integer: (value) => {
|
|
150
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
151
|
-
const num = Number(value);
|
|
152
|
-
if (isNaN(num) || !Number.isInteger(num)) {
|
|
153
|
-
return "Must be a whole number";
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
},
|
|
157
|
-
phone: (value) => {
|
|
158
|
-
if (!value) return null;
|
|
159
|
-
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
|
160
|
-
if (!phoneRegex.test(value) || value.replace(/\D/g, "").length < 10) {
|
|
161
|
-
return "Please enter a valid phone number";
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
},
|
|
165
|
-
date: (value) => {
|
|
166
|
-
if (!value) return null;
|
|
167
|
-
const date = new Date(value);
|
|
168
|
-
if (isNaN(date.getTime())) {
|
|
169
|
-
return "Please enter a valid date";
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
172
|
-
},
|
|
173
|
-
match: (value, options = {}, translator, allValues = {}) => {
|
|
174
|
-
if (!value) return null;
|
|
175
|
-
const fieldName = options.field || options.fieldName;
|
|
176
|
-
if (value !== allValues[fieldName]) {
|
|
177
|
-
return options.message || `Must match ${fieldName}`;
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
},
|
|
181
|
-
custom: (value, options = {}, translator, allValues) => {
|
|
182
|
-
const validatorFn = options.validator || options.fn;
|
|
183
|
-
if (!validatorFn) return null;
|
|
184
|
-
const isValid = validatorFn(value, allValues);
|
|
185
|
-
return isValid ? null : options.message || "Validation failed";
|
|
186
|
-
},
|
|
187
|
-
fileType: (value, options = {}) => {
|
|
188
|
-
if (!value) return null;
|
|
189
|
-
const allowedTypes = options.accept || options.types || [];
|
|
190
|
-
if (value.type !== void 0) {
|
|
191
|
-
const fileType = value.type;
|
|
192
|
-
const fileExt = value.name ? value.name.split(".").pop().toLowerCase() : "";
|
|
193
|
-
const isValid = allowedTypes.some((type) => {
|
|
194
|
-
if (type.startsWith(".")) {
|
|
195
|
-
return fileExt === type.slice(1).toLowerCase();
|
|
196
|
-
}
|
|
197
|
-
if (type.includes("/")) {
|
|
198
|
-
if (type.endsWith("/*")) {
|
|
199
|
-
return fileType.startsWith(type.replace("/*", "/"));
|
|
200
|
-
}
|
|
201
|
-
return fileType === type;
|
|
202
|
-
}
|
|
203
|
-
return fileExt === type.toLowerCase();
|
|
204
|
-
});
|
|
205
|
-
if (!isValid) {
|
|
206
|
-
return options.message || `File type must be one of: ${allowedTypes.join(", ")}`;
|
|
207
|
-
}
|
|
208
24
|
return null;
|
|
209
25
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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);
|
|
221
42
|
}
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
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];
|
|
231
53
|
});
|
|
232
|
-
|
|
233
|
-
|
|
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]);
|
|
234
61
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!value) return null;
|
|
239
|
-
const alphaRegex = /^[a-zA-Z]+$/;
|
|
240
|
-
if (!alphaRegex.test(value)) {
|
|
241
|
-
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";
|
|
242
65
|
}
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
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);
|
|
250
72
|
}
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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;
|
|
257
101
|
}
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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 */
|
|
265
242
|
compose: (validatorList) => {
|
|
243
|
+
const rules = validatorList.map(resolveValidator);
|
|
266
244
|
return (value, options, translator, allValues) => {
|
|
267
|
-
for (const
|
|
268
|
-
const error =
|
|
245
|
+
for (const rule of rules) {
|
|
246
|
+
const error = rule ? rule(value, options, translator, allValues) : null;
|
|
269
247
|
if (error) {
|
|
270
248
|
return error;
|
|
271
249
|
}
|
|
@@ -273,7 +251,7 @@ var validators2 = {
|
|
|
273
251
|
return null;
|
|
274
252
|
};
|
|
275
253
|
},
|
|
276
|
-
|
|
254
|
+
/** Debounce an async validator */
|
|
277
255
|
debounce: (validator, delay = 300) => {
|
|
278
256
|
let timeoutId;
|
|
279
257
|
return (value) => {
|
|
@@ -286,7 +264,7 @@ var validators2 = {
|
|
|
286
264
|
});
|
|
287
265
|
};
|
|
288
266
|
},
|
|
289
|
-
|
|
267
|
+
/** Cancellable async validator */
|
|
290
268
|
cancellable: (validator) => {
|
|
291
269
|
let abortController;
|
|
292
270
|
const wrapped = async (value) => {
|
|
@@ -310,38 +288,31 @@ var validators2 = {
|
|
|
310
288
|
};
|
|
311
289
|
return wrapped;
|
|
312
290
|
},
|
|
313
|
-
|
|
291
|
+
/** Run `validator` only when `condition` holds */
|
|
314
292
|
when: (condition, validator) => {
|
|
293
|
+
const rule = resolveValidator(validator);
|
|
315
294
|
return (value, options = {}, translator, allValues = {}) => {
|
|
316
295
|
const context = options.min !== void 0 || options.max !== void 0 ? allValues : options;
|
|
317
296
|
const shouldValidate = typeof condition === "function" ? condition(value, context) : condition;
|
|
318
297
|
if (!shouldValidate) {
|
|
319
298
|
return null;
|
|
320
299
|
}
|
|
321
|
-
return
|
|
300
|
+
return rule ? rule(value, options, translator, allValues) : null;
|
|
322
301
|
};
|
|
323
302
|
},
|
|
324
|
-
|
|
303
|
+
/** Validator chain builder */
|
|
325
304
|
chain: (options = {}) => {
|
|
326
305
|
const validatorList = [];
|
|
327
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
|
+
};
|
|
328
311
|
const chain = {
|
|
329
|
-
required: (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
email: (opts) => {
|
|
334
|
-
validatorList.push((v, o, t, a) => validators2.email(v, opts || o, t, a));
|
|
335
|
-
return chain;
|
|
336
|
-
},
|
|
337
|
-
minLength: (opts) => {
|
|
338
|
-
validatorList.push((v, o, t, a) => validators2.minLength(v, opts || o, t, a));
|
|
339
|
-
return chain;
|
|
340
|
-
},
|
|
341
|
-
maxLength: (opts) => {
|
|
342
|
-
validatorList.push((v, o, t, a) => validators2.maxLength(v, opts || o, t, a));
|
|
343
|
-
return chain;
|
|
344
|
-
},
|
|
312
|
+
required: direct("required"),
|
|
313
|
+
email: direct("email"),
|
|
314
|
+
minLength: direct("minLength"),
|
|
315
|
+
maxLength: direct("maxLength"),
|
|
345
316
|
custom: (fn, message) => {
|
|
346
317
|
validatorList.push((v, o, t, a) => {
|
|
347
318
|
const result = fn(v, a);
|
|
@@ -358,24 +329,50 @@ var validators2 = {
|
|
|
358
329
|
}
|
|
359
330
|
}
|
|
360
331
|
return null;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
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);
|
|
368
338
|
}
|
|
369
|
-
return errors.length > 0 ? errors : null;
|
|
370
339
|
}
|
|
340
|
+
return errors.length > 0 ? errors : null;
|
|
371
341
|
}
|
|
372
342
|
};
|
|
373
343
|
return chain;
|
|
374
344
|
}
|
|
375
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
|
+
}
|
|
376
371
|
function validateField(value, validatorList, formData = {}) {
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
379
376
|
if (error) {
|
|
380
377
|
return error;
|
|
381
378
|
}
|
|
@@ -393,7 +390,7 @@ function validateForm(formData, fieldValidators) {
|
|
|
393
390
|
}
|
|
394
391
|
return Object.keys(errors).length > 0 ? errors : null;
|
|
395
392
|
}
|
|
396
|
-
function
|
|
393
|
+
function wrapValidator(validatorFn, message) {
|
|
397
394
|
return (value, options, translator, allValues) => {
|
|
398
395
|
const result = validatorFn(value, options, translator, allValues);
|
|
399
396
|
if (typeof result === "string") {
|
|
@@ -406,22 +403,119 @@ function createValidator(validatorFn, message) {
|
|
|
406
403
|
};
|
|
407
404
|
}
|
|
408
405
|
function registerValidator(name, validatorFn) {
|
|
409
|
-
validators2[name] = validatorFn;
|
|
410
406
|
validators[name] = validatorFn;
|
|
407
|
+
if (typeof validatorFn === "function" && !validatorFn[BUILTIN]) {
|
|
408
|
+
DESCRIPTORS.set(validatorFn, { name, args: [] });
|
|
409
|
+
}
|
|
411
410
|
}
|
|
412
411
|
function composeValidators(...validatorFns) {
|
|
413
|
-
return (
|
|
414
|
-
|
|
415
|
-
|
|
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;
|
|
416
434
|
if (error) {
|
|
417
435
|
return error;
|
|
418
436
|
}
|
|
419
437
|
}
|
|
420
438
|
return null;
|
|
421
|
-
}
|
|
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);
|
|
422
514
|
}
|
|
515
|
+
|
|
516
|
+
// src/validators.js
|
|
423
517
|
var validators_default = {
|
|
424
|
-
validators
|
|
518
|
+
validators,
|
|
425
519
|
validateField,
|
|
426
520
|
validateForm,
|
|
427
521
|
createValidator,
|
|
@@ -435,6 +529,6 @@ export {
|
|
|
435
529
|
registerValidator,
|
|
436
530
|
validateField,
|
|
437
531
|
validateForm,
|
|
438
|
-
|
|
532
|
+
validators
|
|
439
533
|
};
|
|
440
534
|
//# sourceMappingURL=validators.js.map
|