@ajosecortes/form-guard 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +417 -0
- package/dist/FormGuard.d.ts +51 -0
- package/dist/dom.d.ts +11 -0
- package/dist/form-guard.cjs +2 -0
- package/dist/form-guard.cjs.map +1 -0
- package/dist/form-guard.js +580 -0
- package/dist/form-guard.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/locales/en.d.ts +3 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/rules.d.ts +19 -0
- package/dist/types.d.ts +102 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# Form Guard
|
|
2
|
+
|
|
3
|
+
A production-minded form validation library for plain HTML forms. Framework-agnostic, TypeScript-first.
|
|
4
|
+
|
|
5
|
+
Instance-based API, composable rules, clean error handling — with structured errors, first-class async support, debounce + cancellation, and accessible DOM output.
|
|
6
|
+
|
|
7
|
+
## Why Form Guard
|
|
8
|
+
|
|
9
|
+
Most form validation libraries fall into two extremes: too low-level (requiring repetitive boilerplate) or too framework-coupled (useless for plain HTML projects). Form Guard keeps the simple, familiar instance-based approach while adding:
|
|
10
|
+
|
|
11
|
+
- **Structured errors** — every error has a `code`, `rule`, `field`, `value`, and `message`
|
|
12
|
+
- **Composable rules** — combine built-in rules with custom sync and async logic
|
|
13
|
+
- **First-class async** — async rules participate naturally in the validation pipeline
|
|
14
|
+
- **Accessible by default** — `aria-invalid`, `aria-describedby`, `role="alert"`, and focus management
|
|
15
|
+
- **Localization** — switch locales at runtime or provide your own messages
|
|
16
|
+
- **Group validation** — validate checkbox/radio groups with the same rule API
|
|
17
|
+
- **Debouncing & cancellation** — built-in debounce for onChange/blur modes, AbortController cancellation for stale async requests
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install form-guard
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<script type="module">
|
|
27
|
+
import { FormGuard, required, email, minLength } from 'form-guard';
|
|
28
|
+
</script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<form id="signup">
|
|
35
|
+
<input name="email" type="email" />
|
|
36
|
+
<div id="email-errors"></div>
|
|
37
|
+
<button type="submit">Sign up</button>
|
|
38
|
+
</form>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { FormGuard, required, email } from 'form-guard';
|
|
43
|
+
|
|
44
|
+
const guard = new FormGuard('#signup');
|
|
45
|
+
|
|
46
|
+
guard.addField('email', [
|
|
47
|
+
required(),
|
|
48
|
+
email(),
|
|
49
|
+
], {
|
|
50
|
+
errorContainer: document.getElementById('email-errors'),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
guard.onSuccess(() => {
|
|
54
|
+
alert('Form is valid!');
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API Reference
|
|
59
|
+
|
|
60
|
+
### Constructor
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
new FormGuard(form, config?)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **form**: CSS selector string (`'#my-form'`) or `HTMLFormElement`
|
|
67
|
+
- **config**: Optional global configuration (see [Configuration](#configuration))
|
|
68
|
+
|
|
69
|
+
### Methods
|
|
70
|
+
|
|
71
|
+
| Method | Returns | Description |
|
|
72
|
+
|--------|---------|-------------|
|
|
73
|
+
| `addField(selector, rules, config?)` | `this` | Register a field for validation |
|
|
74
|
+
| `removeField(selector)` | `this` | Remove a field from validation |
|
|
75
|
+
| `addGroup(selector, rules, config?)` | `this` | Register a checkbox/radio group |
|
|
76
|
+
| `removeGroup(selector)` | `this` | Remove a group from validation |
|
|
77
|
+
| `validate(force?)` | `Promise<boolean>` | Validate all registered fields and groups |
|
|
78
|
+
| `validateField(selector)` | `Promise<boolean>` | Validate a single field |
|
|
79
|
+
| `validateGroup(selector)` | `Promise<boolean>` | Validate a single group |
|
|
80
|
+
| `revalidate()` | `Promise<boolean>` | Reset all validated flags and re-validate |
|
|
81
|
+
| `revalidateField(selector)` | `Promise<boolean>` | Re-validate a specific field |
|
|
82
|
+
| `revalidateGroup(selector)` | `Promise<boolean>` | Re-validate a specific group |
|
|
83
|
+
| `clearErrors()` | `this` | Clear all errors and DOM state |
|
|
84
|
+
| `refresh()` | `this` | Re-scan the DOM for added/removed elements |
|
|
85
|
+
| `destroy()` | `void` | Remove all listeners and clean up |
|
|
86
|
+
| `setLocale(locale)` | `this` | Merge locale messages at runtime |
|
|
87
|
+
| `onSuccess(callback)` | `this` | Register a success hook |
|
|
88
|
+
| `onFail(callback)` | `this` | Register a failure hook |
|
|
89
|
+
| `onValidate(callback)` | `this` | Register a hook that fires on every validation |
|
|
90
|
+
| `getErrors()` | `ValidationError[]` | Get all current errors |
|
|
91
|
+
| `getFieldErrors(selector)` | `ValidationError[]` | Get errors for a specific field |
|
|
92
|
+
| `getGroupErrors(selector)` | `ValidationError[]` | Get errors for a specific group |
|
|
93
|
+
| `isFieldPending(selector)` | `boolean` | Whether a field has async validation in progress |
|
|
94
|
+
| `isPending()` | `boolean` | Whether any field has async validation in progress |
|
|
95
|
+
|
|
96
|
+
## Rules
|
|
97
|
+
|
|
98
|
+
Every rule helper returns a `Rule` object. Rules execute in declared order. Validation stops at the first failing rule by default; set `collectAll: true` in global config to collect all failures.
|
|
99
|
+
|
|
100
|
+
### Built-in Rules
|
|
101
|
+
|
|
102
|
+
| Rule | Usage | Description |
|
|
103
|
+
|------|-------|-------------|
|
|
104
|
+
| `required(msg?)` | `required('Name is required')` | Field must have a non-empty value |
|
|
105
|
+
| `email(msg?)` | `email()` | Value must match email format (skipped if empty) |
|
|
106
|
+
| `minLength(n, msg?)` | `minLength(8)` | String must be at least `n` characters (skipped if empty) |
|
|
107
|
+
| `maxLength(n, msg?)` | `maxLength(200)` | String must be at most `n` characters |
|
|
108
|
+
| `number(msg?)` | `number()` | Value must be a valid number (skipped if empty) |
|
|
109
|
+
| `integer(msg?)` | `integer()` | Value must be an integer (skipped if empty) |
|
|
110
|
+
| `min(n, msg?)` | `min(18)` | Number must be >= `n` (skipped if empty or NaN) |
|
|
111
|
+
| `max(n, msg?)` | `max(120)` | Number must be <= `n` (skipped if empty or NaN) |
|
|
112
|
+
| `pattern(regexp, msg?)` | `pattern(/^[A-Z]+$/)` | Value must match regex (skipped if empty) |
|
|
113
|
+
| `sameAs(fieldName, msg?)` | `sameAs('password')` | Value must match another field's value |
|
|
114
|
+
| `files(msg?)` | `files()` | File input must have at least one file |
|
|
115
|
+
| `minFiles(n, msg?)` | `minFiles(2)` | File input must have at least `n` files |
|
|
116
|
+
| `maxFiles(n, msg?)` | `maxFiles(5)` | File input must have at most `n` files |
|
|
117
|
+
| `strongPassword(msg?)` | `strongPassword()` | Must have uppercase, lowercase, digit, special char, and 8+ length |
|
|
118
|
+
| `custom(fn, name?)` | `custom(v => v > 0 \|\| 'Must be positive')` | Custom sync validation function |
|
|
119
|
+
| `asyncRule(fn, opts?)` | `asyncRule(checkUsername, { name: 'username' })` | Custom async validation function |
|
|
120
|
+
|
|
121
|
+
### Custom Rules
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
guard.addField('#age', [
|
|
125
|
+
required(),
|
|
126
|
+
custom((value, form) => {
|
|
127
|
+
const n = Number(value);
|
|
128
|
+
if (isNaN(n)) return 'Must be a number';
|
|
129
|
+
if (n < 0) return 'Age cannot be negative';
|
|
130
|
+
if (n > 150) return 'Age seems unrealistic';
|
|
131
|
+
return true;
|
|
132
|
+
}, 'valid-age'),
|
|
133
|
+
]);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Async Rules
|
|
137
|
+
|
|
138
|
+
Async rules run after sync rules pass. They receive the same `(value, form)` signature.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
guard.addField('#username', [
|
|
142
|
+
required(),
|
|
143
|
+
minLength(3),
|
|
144
|
+
asyncRule(async (value) => {
|
|
145
|
+
const res = await fetch(`/api/check-username?q=${value}`);
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
return data.available || 'Username is taken';
|
|
148
|
+
}, { name: 'username-available' }),
|
|
149
|
+
]);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
### Global Config
|
|
155
|
+
|
|
156
|
+
Passed as the second argument to the constructor:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const guard = new FormGuard('#form', {
|
|
160
|
+
mode: 'onSubmit', // 'onSubmit' | 'onBlur' | 'onChange'
|
|
161
|
+
locale: {}, // Custom locale messages
|
|
162
|
+
focusOnError: true, // Focus first invalid field
|
|
163
|
+
lockForm: false, // Lock form during async validation
|
|
164
|
+
errorClass: 'form-guard-error', // CSS class for invalid fields
|
|
165
|
+
successClass: 'form-guard-success', // CSS class for valid fields
|
|
166
|
+
pendingClass: 'form-guard-pending', // CSS class during async validation
|
|
167
|
+
debug: false, // Enable debug warnings
|
|
168
|
+
verbose: false, // Verbose debug output
|
|
169
|
+
errorContainer: '.errors', // Default error container selector
|
|
170
|
+
collectAll: false, // Collect all errors per field instead of stopping at first
|
|
171
|
+
debounceMs: 0, // Debounce delay in ms for onChange/onBlur modes
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Validation Modes
|
|
176
|
+
|
|
177
|
+
- **`onSubmit`** (default): Validates when the form is submitted. Prevents submission on failure.
|
|
178
|
+
- **`onBlur`**: Validates each field when it loses focus.
|
|
179
|
+
- **`onChange`**: Validates each field as the user types/changes it.
|
|
180
|
+
|
|
181
|
+
### Field Config
|
|
182
|
+
|
|
183
|
+
Passed as the third argument to `addField`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
guard.addField('#email', [required(), email()], {
|
|
187
|
+
name: 'email', // Alternative field identifier
|
|
188
|
+
label: 'Email Address', // Human-readable label
|
|
189
|
+
errorContainer: el, // HTMLElement or CSS selector for error messages
|
|
190
|
+
successClass: 'my-success', // Override global success class
|
|
191
|
+
errorClass: 'my-error', // Override global error class
|
|
192
|
+
debounceMs: 500, // Per-field debounce override
|
|
193
|
+
messages: { // Per-field rule messages
|
|
194
|
+
required: 'Email cannot be empty',
|
|
195
|
+
email: 'Enter a valid email',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Error Model
|
|
201
|
+
|
|
202
|
+
Every validation error is a structured object, not a plain string:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
interface ValidationError {
|
|
206
|
+
code: string; // Rule code: 'required', 'email', etc.
|
|
207
|
+
message: string; // Human-readable error message
|
|
208
|
+
field: string; // Field name or selector
|
|
209
|
+
rule: string; // Rule that triggered the error
|
|
210
|
+
value: unknown; // The value that failed
|
|
211
|
+
meta: Record<string, unknown>; // Additional metadata (reserved for future use)
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Access errors through hooks or the `getErrors()` method:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
guard.onFail((errors) => {
|
|
219
|
+
errors.forEach(e => {
|
|
220
|
+
console.log(`${e.field}: ${e.message} [${e.rule}]`);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Lifecycle Hooks
|
|
226
|
+
|
|
227
|
+
Hooks are chainable and support multiple callbacks:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
guard
|
|
231
|
+
.onValidate(({ valid, errors, fields, groups }) => {
|
|
232
|
+
console.log('Validated:', valid, errors.length, 'errors');
|
|
233
|
+
})
|
|
234
|
+
.onSuccess(() => {
|
|
235
|
+
console.log('All checks passed');
|
|
236
|
+
})
|
|
237
|
+
.onFail((errors) => {
|
|
238
|
+
console.log(errors.length, 'validation errors');
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- **`onValidate`**: Fires after every validation, regardless of result. Receives the full validation result.
|
|
243
|
+
- **`onSuccess`**: Fires when all fields and groups pass validation.
|
|
244
|
+
- **`onFail`**: Fires when any field or group fails validation. Receives the array of all errors.
|
|
245
|
+
|
|
246
|
+
## Group Validation
|
|
247
|
+
|
|
248
|
+
Validate checkbox/radio groups as a single unit:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
guard.addGroup('colors', [required('Select at least one color')], {
|
|
252
|
+
errorContainer: document.getElementById('colors-errors'),
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Group validation treats all elements with the same `name` attribute as a single logical field:
|
|
257
|
+
- **Checkbox groups**: At least one must be checked
|
|
258
|
+
- **Radio groups**: Exactly one must be selected
|
|
259
|
+
|
|
260
|
+
## Debouncing & Cancellation
|
|
261
|
+
|
|
262
|
+
Async validation (e.g., checking username availability) benefits from debouncing: waiting until the user stops typing before firing an API call, and cancelling stale requests when a new keystroke arrives.
|
|
263
|
+
|
|
264
|
+
### Configuration
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const guard = new FormGuard('#form', {
|
|
268
|
+
mode: 'onChange',
|
|
269
|
+
debounceMs: 300, // Wait 300ms after last keystroke before validating
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Per-field override
|
|
273
|
+
guard.addField('#username', [...rules], {
|
|
274
|
+
debounceMs: 500, // This field uses 500ms instead
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
When `debounceMs` is `0` (default), validation fires immediately on each event.
|
|
279
|
+
|
|
280
|
+
### Pending state
|
|
281
|
+
|
|
282
|
+
While async validation is in progress, the field gets a `form-guard-pending` CSS class (configurable via `pendingClass`). Check pending state programmatically:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
guard.isFieldPending('username'); // true while async validation runs
|
|
286
|
+
guard.isPending(); // true if any field is validating
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### AbortController cancellation
|
|
290
|
+
|
|
291
|
+
Every async validation run creates a new `AbortController`. If a new validation starts before the previous one finishes (e.g., user keeps typing), the previous controller is aborted. Async rules receive the `AbortSignal` as the third argument:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
guard.addField('#username', [
|
|
295
|
+
asyncRule(async (value, form, signal) => {
|
|
296
|
+
const res = await fetch(`/api/check?q=${value}`, { signal });
|
|
297
|
+
if (!res.ok) throw new Error('Network error');
|
|
298
|
+
const data = await res.json();
|
|
299
|
+
return data.available || 'Username is taken';
|
|
300
|
+
}),
|
|
301
|
+
]);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- **Debounce** prevents firing on every keystroke
|
|
305
|
+
- **AbortController** cancels stale requests that haven't resolved yet
|
|
306
|
+
- **Combined**, they prevent race conditions and wasted API calls
|
|
307
|
+
|
|
308
|
+
## Localization
|
|
309
|
+
|
|
310
|
+
Form Guard ships with English (`en`) built-in. Switch or extend locales at any time:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { FormGuard, en } from 'form-guard';
|
|
314
|
+
|
|
315
|
+
const es = {
|
|
316
|
+
required: 'Este campo es obligatorio',
|
|
317
|
+
email: 'Ingrese un email válido',
|
|
318
|
+
minLength: 'Debe tener al menos {0} caracteres',
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const guard = new FormGuard('#form', { locale: es });
|
|
322
|
+
|
|
323
|
+
// Switch later
|
|
324
|
+
guard.setLocale({
|
|
325
|
+
required: 'Ce champ est obligatoire',
|
|
326
|
+
email: 'Veuillez entrer un email valide',
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Per-field messages take precedence over locale messages.
|
|
331
|
+
|
|
332
|
+
## Accessibility
|
|
333
|
+
|
|
334
|
+
Form Guard sets DOM attributes for accessible error handling:
|
|
335
|
+
|
|
336
|
+
- **`aria-invalid="true"`** on fields with errors
|
|
337
|
+
- **`aria-describedby`** linking the field to its error message element
|
|
338
|
+
- **`role="alert"`** on error containers for screen reader announcements
|
|
339
|
+
- **Focus management**: When `focusOnError` is `true` (default), the first invalid field receives focus after validation
|
|
340
|
+
|
|
341
|
+
### CSS Classes
|
|
342
|
+
|
|
343
|
+
- **`.form-guard-error`**: Added to fields with validation errors
|
|
344
|
+
- **`.form-guard-success`**: Added to fields that pass validation
|
|
345
|
+
- **`.form-guard-message`**: Error message span inside the error container
|
|
346
|
+
|
|
347
|
+
Customize these via global or per-field config.
|
|
348
|
+
|
|
349
|
+
## Examples
|
|
350
|
+
|
|
351
|
+
The `examples/` directory contains runnable HTML files:
|
|
352
|
+
|
|
353
|
+
| Example | Demonstrates |
|
|
354
|
+
|---------|-------------|
|
|
355
|
+
| `signup.html` | Full signup form with multiple rules, password confirmation, checkbox |
|
|
356
|
+
| `async-username.html` | Async validation simulating a username availability API |
|
|
357
|
+
| `group-validation.html` | Radio and checkbox group validation |
|
|
358
|
+
| `custom-rules.html` | Custom sync validation rules |
|
|
359
|
+
| `localization.html` | Runtime locale switching (English, Spanish, French) |
|
|
360
|
+
|
|
361
|
+
To run an example:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npm run dev
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Then open one of these in your browser:
|
|
368
|
+
- `http://localhost:5173/examples/signup.html`
|
|
369
|
+
- `http://localhost:5173/examples/async-username.html`
|
|
370
|
+
- `http://localhost:5173/examples/group-validation.html`
|
|
371
|
+
- `http://localhost:5173/examples/custom-rules.html`
|
|
372
|
+
- `http://localhost:5173/examples/localization.html`
|
|
373
|
+
|
|
374
|
+
Vite's dev server compiles TypeScript on the fly — no build step needed.
|
|
375
|
+
|
|
376
|
+
## Testing
|
|
377
|
+
|
|
378
|
+
The test suite uses Vitest with jsdom. Tests cover observable behavior, not internals.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npm test # Run once
|
|
382
|
+
npm run test:watch # Watch mode
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Coverage areas
|
|
386
|
+
|
|
387
|
+
- Constructor and initialization
|
|
388
|
+
- Field registration, removal, and validation
|
|
389
|
+
- All built-in rules (required, email, length, pattern, numeric, file, password, sameAs)
|
|
390
|
+
- Custom sync and async rules
|
|
391
|
+
- Group validation (checkbox, radio)
|
|
392
|
+
- Lifecycle hooks (onSuccess, onFail, onValidate)
|
|
393
|
+
- Locale support (default, constructor, runtime switching)
|
|
394
|
+
- DOM rendering (error/success classes, aria attributes, error containers)
|
|
395
|
+
- Clear/reset/destroy lifecycle
|
|
396
|
+
- collectAll mode
|
|
397
|
+
|
|
398
|
+
## Design Decisions
|
|
399
|
+
|
|
400
|
+
### Stop-at-first vs. collect-all
|
|
401
|
+
|
|
402
|
+
By default, validation stops at the first failing rule per field. This avoids overwhelming users with multiple messages for a single field. Set `collectAll: true` in global config to collect all failures — useful for inline validation displays that show every rule violation at once.
|
|
403
|
+
|
|
404
|
+
### Error message precedence
|
|
405
|
+
|
|
406
|
+
1. Rule's inline message (`required('Custom message')`)
|
|
407
|
+
2. Field-level `messages` config
|
|
408
|
+
3. Locale message
|
|
409
|
+
4. Rule name fallback
|
|
410
|
+
|
|
411
|
+
### No framework adapters in v1
|
|
412
|
+
|
|
413
|
+
Form Guard targets plain HTML forms directly. Framework wrappers (React, Vue, Angular) are out of scope for v1 but the headless `getErrors()` API and hook callbacks make integration straightforward.
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FormGuardInstance, FormGuardConfig, Rule, FieldConfig, GroupConfig, ValidationError, Locale, SuccessCallback, FailCallback, ValidateCallback } from './types';
|
|
2
|
+
export declare class FormGuard implements FormGuardInstance {
|
|
3
|
+
private form;
|
|
4
|
+
private config;
|
|
5
|
+
private fields;
|
|
6
|
+
private groups;
|
|
7
|
+
private locale;
|
|
8
|
+
private successCallbacks;
|
|
9
|
+
private failCallbacks;
|
|
10
|
+
private validateCallbacks;
|
|
11
|
+
private boundSubmit;
|
|
12
|
+
private boundBlur;
|
|
13
|
+
private boundChange;
|
|
14
|
+
private destroyed;
|
|
15
|
+
constructor(form: string | HTMLFormElement, config?: FormGuardConfig);
|
|
16
|
+
addField(fieldSelector: string, rules: Rule[], config?: FieldConfig): FormGuardInstance;
|
|
17
|
+
removeField(fieldSelector: string): FormGuardInstance;
|
|
18
|
+
addGroup(groupSelector: string, rules: Rule[], config?: GroupConfig): FormGuardInstance;
|
|
19
|
+
removeGroup(groupSelector: string): FormGuardInstance;
|
|
20
|
+
validate(forceRevalidation?: boolean): Promise<boolean>;
|
|
21
|
+
validateField(fieldSelector: string): Promise<boolean>;
|
|
22
|
+
validateGroup(groupSelector: string): Promise<boolean>;
|
|
23
|
+
revalidate(): Promise<boolean>;
|
|
24
|
+
revalidateField(fieldSelector: string): Promise<boolean>;
|
|
25
|
+
revalidateGroup(groupSelector: string): Promise<boolean>;
|
|
26
|
+
clearErrors(): FormGuardInstance;
|
|
27
|
+
refresh(): FormGuardInstance;
|
|
28
|
+
destroy(): void;
|
|
29
|
+
setLocale(locale: Locale): FormGuardInstance;
|
|
30
|
+
onSuccess(callback: SuccessCallback): FormGuardInstance;
|
|
31
|
+
onFail(callback: FailCallback): FormGuardInstance;
|
|
32
|
+
onValidate(callback: ValidateCallback): FormGuardInstance;
|
|
33
|
+
getErrors(): ValidationError[];
|
|
34
|
+
getFieldErrors(fieldSelector: string): ValidationError[];
|
|
35
|
+
getGroupErrors(groupSelector: string): ValidationError[];
|
|
36
|
+
isFieldPending(fieldSelector: string): boolean;
|
|
37
|
+
isPending(): boolean;
|
|
38
|
+
private validateFieldState;
|
|
39
|
+
private validateGroupState;
|
|
40
|
+
private cancelFieldValidation;
|
|
41
|
+
private getMessage;
|
|
42
|
+
private getAllErrors;
|
|
43
|
+
private fireHooks;
|
|
44
|
+
private handleSubmit;
|
|
45
|
+
private handleBlur;
|
|
46
|
+
private handleChange;
|
|
47
|
+
private handleGroupEvent;
|
|
48
|
+
private debounceValidate;
|
|
49
|
+
private bindEvents;
|
|
50
|
+
private unbindEvents;
|
|
51
|
+
}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ValidationError, FieldConfig, FormGuardConfig } from './types';
|
|
2
|
+
export declare function resolveErrorContainer(fieldConfig?: FieldConfig, globalConfig?: FormGuardConfig, element?: HTMLElement): HTMLElement | null;
|
|
3
|
+
export declare function clearFieldDOM(element: HTMLElement, fieldConfig?: FieldConfig, globalConfig?: FormGuardConfig): void;
|
|
4
|
+
export declare function renderFieldPending(element: HTMLElement, fieldConfig?: FieldConfig, globalConfig?: FormGuardConfig): void;
|
|
5
|
+
export declare function renderFieldError(element: HTMLElement, error: ValidationError, fieldConfig?: FieldConfig, globalConfig?: FormGuardConfig): void;
|
|
6
|
+
export declare function renderFieldSuccess(element: HTMLElement, fieldConfig?: FieldConfig, globalConfig?: FormGuardConfig): void;
|
|
7
|
+
export declare function focusFirstInvalid(fields: {
|
|
8
|
+
element: HTMLElement;
|
|
9
|
+
errors: ValidationError[];
|
|
10
|
+
}[]): void;
|
|
11
|
+
export declare function escapeHtml(text: string): string;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var b=Object.defineProperty;var y=(r,e,t)=>e in r?b(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var u=(r,e,t)=>y(r,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const v={required:"This field is required",email:"Please enter a valid email address",minLength:"Must be at least {0} characters",maxLength:"Must be no more than {0} characters",number:"Must be a number",integer:"Must be an integer",min:"Must be at least {0}",max:"Must be no more than {0}",pattern:"Invalid format",sameAs:"Must match {0}",files:"Please select at least one file",minFiles:"Please select at least {0} files",maxFiles:"Please select at most {0} files",strongPassword:"Password must contain uppercase, lowercase, digit, and special character"};function d(r,e,t){const s=(r==null?void 0:r.errorContainer)??(e==null?void 0:e.errorContainer);if(!s)return null;if(s instanceof HTMLElement)return s;const i=s;if(t!=null&&t.closest){const n=t.closest(i);if(n)return n}return document.querySelector(i)}function h(r,e,t){const s=(e==null?void 0:e.errorClass)??(t==null?void 0:t.errorClass)??"form-guard-error",i=(e==null?void 0:e.successClass)??(t==null?void 0:t.successClass)??"form-guard-success",n=(t==null?void 0:t.pendingClass)??"form-guard-pending";r.classList.remove(s),r.classList.remove(i),r.classList.remove(n),r.removeAttribute("aria-invalid"),r.removeAttribute("aria-describedby");const a=d(e,t,r);a&&(a.innerHTML="",a.setAttribute("role","alert"))}function f(r,e,t){const s=(e==null?void 0:e.errorClass)??(t==null?void 0:t.errorClass)??"form-guard-error",i=(e==null?void 0:e.successClass)??(t==null?void 0:t.successClass)??"form-guard-success",n=(t==null?void 0:t.pendingClass)??"form-guard-pending";r.classList.remove(s),r.classList.remove(i),r.classList.add(n),r.removeAttribute("aria-invalid"),r.removeAttribute("aria-describedby");const a=d(e,t,r);a&&(a.innerHTML='<span class="form-guard-message form-guard-pending-text">Checking...</span>',a.setAttribute("role","status"),a.setAttribute("aria-live","polite"))}function E(r,e,t,s){const i=(t==null?void 0:t.errorClass)??(s==null?void 0:s.errorClass)??"form-guard-error",n=(t==null?void 0:t.successClass)??(s==null?void 0:s.successClass)??"form-guard-success",a=(s==null?void 0:s.pendingClass)??"form-guard-pending";r.classList.add(i),r.classList.remove(n),r.classList.remove(a),r.setAttribute("aria-invalid","true");const o=d(t,s,r);if(o){const c=`form-guard-error-${e.field}`;o.innerHTML=`<span id="${c}" class="form-guard-message">${A(e.message)}</span>`,o.setAttribute("role","alert"),r.setAttribute("aria-describedby",c)}}function L(r,e,t){const s=(e==null?void 0:e.errorClass)??(t==null?void 0:t.errorClass)??"form-guard-error",i=(e==null?void 0:e.successClass)??(t==null?void 0:t.successClass)??"form-guard-success",n=(t==null?void 0:t.pendingClass)??"form-guard-pending";r.classList.remove(s),r.classList.add(i),r.classList.remove(n),r.removeAttribute("aria-invalid"),r.removeAttribute("aria-describedby");const a=d(e,t,r);a&&(a.innerHTML="")}function g(r){for(const{element:e,errors:t}of r)if(t.length>0){e.focus();return}}function A(r){const e=document.createElement("div");return e.textContent=r,e.innerHTML}function m(r,e){return r.startsWith("#")||r.startsWith(".")||r.includes("[")?e.querySelector(r):e.querySelector(`[name="${r}"]`)}function p(r,e){return r.startsWith("#")||r.startsWith(".")||r.includes("[")?Array.from(e.querySelectorAll(r)):Array.from(e.querySelectorAll(`[name="${r}"]`))}function F(r){return r instanceof HTMLInputElement&&r.type==="file"?r.files:r instanceof HTMLInputElement&&(r.type==="checkbox"||r.type==="radio")?r.checked:r instanceof HTMLSelectElement&&r.multiple?Array.from(r.selectedOptions).map(t=>t.value):r.value}function k(r){const e=[];for(const t of r)t instanceof HTMLInputElement&&(t.type==="checkbox"||t.type==="radio")&&t.checked&&e.push(t.value);if(r.length>0){const t=r[0];if(t instanceof HTMLInputElement&&t.type==="radio")return e.length>0?e[0]:""}return e}function M(r,e){var t;return e!=null&&e.label?e.label:r.labels&&r.labels.length>0?((t=r.labels[0].textContent)==null?void 0:t.trim())??r.name:r.name||"field"}function l(r,e,t,s,i){return{code:r,message:e,field:t,rule:s,value:i,meta:{}}}class T{constructor(e,t){u(this,"form");u(this,"config");u(this,"fields",new Map);u(this,"groups",new Map);u(this,"locale");u(this,"successCallbacks",[]);u(this,"failCallbacks",[]);u(this,"validateCallbacks",[]);u(this,"boundSubmit",null);u(this,"boundBlur",null);u(this,"boundChange",null);u(this,"destroyed",!1);u(this,"handleSubmit",async e=>{if(this.config.mode!=="onSubmit")return;e.preventDefault(),await this.validate()&&this.form.submit()});if(this.config={mode:"onSubmit",focusOnError:!0,lockForm:!1,collectAll:!1,debounceMs:0,...t},this.locale={...v,...t==null?void 0:t.locale},typeof e=="string"){const s=document.querySelector(e);if(!s)throw new Error(`FormGuard: form not found for selector "${e}"`);this.form=s}else this.form=e;this.bindEvents()}addField(e,t,s){if(this.destroyed)return this;const i=e,n=m(e,this.form);if(!n)return this.config.debug&&console.warn(`FormGuard: field "${e}" not found`),this;if(this.fields.set(i,{field:n,rules:[...t],config:s??{},errors:[],validated:!1,pending:!1,abortController:null,debounceTimer:null}),this.config.mode==="onBlur"&&n.addEventListener("blur",this.handleBlur(i)),this.config.mode==="onChange"){const a=n instanceof HTMLInputElement&&(n.type==="checkbox"||n.type==="radio")?"change":"input";n.addEventListener(a,this.handleChange(i))}return this}removeField(e){const t=this.fields.get(e);return t&&(this.cancelFieldValidation(t),h(t.field,t.config,this.config),t.field.removeEventListener("blur",this.handleBlur(e)),t.field.removeEventListener("input",this.handleChange(e)),t.field.removeEventListener("change",this.handleChange(e))),this.fields.delete(e),this}addGroup(e,t,s){if(this.destroyed)return this;const i=p(e,this.form);if(i.length===0)return this.config.debug&&console.warn(`FormGuard: no elements found for group "${e}"`),this;if(this.groups.set(e,{elements:i,rules:[...t],config:s??{},errors:[],validated:!1,pending:!1,abortController:null,debounceTimer:null}),this.config.mode==="onBlur"||this.config.mode==="onChange")for(const n of i){n.addEventListener("blur",this.handleGroupEvent(e));const a=n instanceof HTMLInputElement&&(n.type==="checkbox"||n.type==="radio")?"change":"input";n.addEventListener(a,this.handleGroupEvent(e))}return this}removeGroup(e){const t=this.groups.get(e);if(t){this.cancelFieldValidation(t);for(const s of t.elements)s.removeEventListener("blur",this.handleGroupEvent(e)),s.removeEventListener("input",this.handleGroupEvent(e)),s.removeEventListener("change",this.handleGroupEvent(e))}return this.groups.delete(e),this}async validate(e=!1){if(this.destroyed)return!1;for(const[,i]of this.fields)!e&&i.validated||await this.validateFieldState(i);for(const[,i]of this.groups)!e&&i.validated||await this.validateGroupState(i);const t=this.getAllErrors(),s=t.length===0;if(this.fireHooks(s,t),!s&&this.config.focusOnError){const i=[];for(const[,n]of this.fields)n.errors.length>0&&i.push({element:n.field,errors:n.errors});g(i)}return s}async validateField(e){const t=this.fields.get(e);return t?(await this.validateFieldState(t),t.errors.length===0):!0}async validateGroup(e){const t=this.groups.get(e);return t?(await this.validateGroupState(t),t.errors.length===0):!0}async revalidate(){for(const[,e]of this.fields)e.validated=!1;for(const[,e]of this.groups)e.validated=!1;return this.validate(!0)}async revalidateField(e){const t=this.fields.get(e);return t?(t.validated=!1,await this.validateFieldState(t),t.errors.length===0):!0}async revalidateGroup(e){const t=this.groups.get(e);return t?(t.validated=!1,await this.validateGroupState(t),t.errors.length===0):!0}clearErrors(){for(const[,e]of this.fields)this.cancelFieldValidation(e),e.errors=[],e.validated=!1,e.pending=!1,h(e.field,e.config,this.config);for(const[,e]of this.groups)this.cancelFieldValidation(e),e.errors=[],e.validated=!1,e.pending=!1;return this}refresh(){const e=[...this.fields.entries()];for(const[s,i]of e){const n=m(s,this.form);n?i.field=n:this.fields.delete(s)}const t=[...this.groups.entries()];for(const[s,i]of t){const n=p(s,this.form);n.length>0?i.elements=n:this.groups.delete(s)}return this}destroy(){this.unbindEvents();for(const[e,t]of this.fields)this.cancelFieldValidation(t),h(t.field,t.config,this.config),t.field.removeEventListener("blur",this.handleBlur(e)),t.field.removeEventListener("input",this.handleChange(e)),t.field.removeEventListener("change",this.handleChange(e));for(const[e,t]of this.groups){this.cancelFieldValidation(t);for(const s of t.elements)s.removeEventListener("blur",this.handleGroupEvent(e)),s.removeEventListener("input",this.handleGroupEvent(e)),s.removeEventListener("change",this.handleGroupEvent(e))}this.fields.clear(),this.groups.clear(),this.successCallbacks.length=0,this.failCallbacks.length=0,this.validateCallbacks.length=0,this.destroyed=!0}setLocale(e){return this.locale={...this.locale,...e},this}onSuccess(e){return this.successCallbacks.push(e),this}onFail(e){return this.failCallbacks.push(e),this}onValidate(e){return this.validateCallbacks.push(e),this}getErrors(){return this.getAllErrors()}getFieldErrors(e){var t;return((t=this.fields.get(e))==null?void 0:t.errors)??[]}getGroupErrors(e){var t;return((t=this.groups.get(e))==null?void 0:t.errors)??[]}isFieldPending(e){var t;return((t=this.fields.get(e))==null?void 0:t.pending)??!1}isPending(){for(const[,e]of this.fields)if(e.pending)return!0;for(const[,e]of this.groups)if(e.pending)return!0;return!1}async validateFieldState(e){this.cancelFieldValidation(e);const t=new AbortController;e.abortController=t,e.pending=!0,e.errors=[],f(e.field,e.config,this.config);const s=F(e.field),i=M(e.field,e.config);try{for(const n of e.rules){if(t.signal.aborted)break;try{const a=n.validate(s,this.form,e.config,t.signal),o=a instanceof Promise?await a:a;if(t.signal.aborted)break;if(o===!0||o===void 0)continue;let c;if(typeof o=="string"?c=o:typeof o=="object"&&o!==null&&"message"in o?c=String(o.message):c=this.getMessage(n.name,e.config,i),e.errors.push(l(n.name,c,e.field.name||i,n.name,s)),!this.config.collectAll)break}catch(a){if(t.signal.aborted||(this.config.debug&&console.error(`FormGuard: rule "${n.name}" threw:`,a),e.errors.push(l(n.name,this.getMessage(n.name,e.config,i),e.field.name||i,n.name,s)),!this.config.collectAll))break}}}finally{e.abortController===t&&(e.pending=!1,e.validated=!0,e.abortController=null,t.signal.aborted||(e.errors.length>0?E(e.field,e.errors[0],e.config,this.config):L(e.field,e.config,this.config)))}}async validateGroupState(e){this.cancelFieldValidation(e);const t=new AbortController;e.abortController=t,e.pending=!0,e.errors=[];const s=k(e.elements),i=e.config.label??e.config.name??"group";try{for(const n of e.rules){if(t.signal.aborted)break;try{const a=n.validate(s,this.form,e.config,t.signal),o=a instanceof Promise?await a:a;if(t.signal.aborted)break;if(o===!0||o===void 0)continue;let c;if(typeof o=="string"?c=o:c=this.getMessage(n.name,e.config,i),e.errors.push(l(n.name,c,i,n.name,s)),!this.config.collectAll)break}catch(a){if(t.signal.aborted||(this.config.debug&&console.error(`FormGuard: rule "${n.name}" threw:`,a),e.errors.push(l(n.name,this.getMessage(n.name,e.config,i),i,n.name,s)),!this.config.collectAll))break}}}finally{e.abortController===t&&(e.pending=!1,e.validated=!0,e.abortController=null)}}cancelFieldValidation(e){e.debounceTimer!==null&&(clearTimeout(e.debounceTimer),e.debounceTimer=null),e.abortController&&(e.abortController.abort(),e.abortController=null,e.pending=!1)}getMessage(e,t,s){var n;const i=(n=t==null?void 0:t.messages)==null?void 0:n[e];return i||(this.locale[e]??e)}getAllErrors(){const e=[];for(const[,t]of this.fields)e.push(...t.errors);for(const[,t]of this.groups)e.push(...t.errors);return e}fireHooks(e,t){const s={};for(const[a,o]of this.fields)o.errors.length>0&&(s[a]=[...o.errors]);const i={};for(const[a,o]of this.groups)o.errors.length>0&&(i[a]=[...o.errors]);const n={valid:e,errors:t,fields:s,groups:i};for(const a of this.validateCallbacks)a(n);if(e)for(const a of this.successCallbacks)a();else for(const a of this.failCallbacks)a(t)}handleBlur(e){return()=>{const t=this.fields.get(e);t&&this.debounceValidate(()=>this.validateFieldState(t),t)}}handleChange(e){return()=>{const t=this.fields.get(e);t&&(t.validated=!1,this.debounceValidate(()=>this.validateFieldState(t),t))}}handleGroupEvent(e){return()=>{const t=this.groups.get(e);t&&(t.validated=!1,this.debounceValidate(()=>this.validateGroupState(t),t))}}debounceValidate(e,t){const s="config"in t&&"debounceMs"in t.config?t.config.debounceMs??this.config.debounceMs??0:this.config.debounceMs??0;s&&s>0?(t.debounceTimer!==null&&clearTimeout(t.debounceTimer),t.pending=!0,t instanceof Object&&"field"in t&&f(t.field,t.config,this.config),t.debounceTimer=setTimeout(()=>{t.debounceTimer=null,e()},s)):e()}bindEvents(){this.config.mode==="onSubmit"&&(this.boundSubmit=this.handleSubmit.bind(this),this.form.addEventListener("submit",this.boundSubmit),this.form.setAttribute("novalidate",""))}unbindEvents(){this.boundSubmit&&(this.form.removeEventListener("submit",this.boundSubmit),this.boundSubmit=null),this.boundBlur&&(this.form.removeEventListener("blur",this.boundBlur),this.boundBlur=null),this.boundChange&&(this.form.removeEventListener("input",this.boundChange),this.boundChange=null),this.form.removeAttribute("novalidate")}}const S=/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;function G(r){return{name:"required",validate(e){return typeof e=="boolean"?e===!0||r||!1:typeof e=="string"?e.trim().length>0||r||!1:e instanceof FileList||Array.isArray(e)?e.length>0||r||!1:e!=null&&e!==""||r||!1}}}function w(r){return{name:"email",validate(e){return typeof e!="string"||e.trim()===""?!0:S.test(e.trim())||r||!1}}}function H(r,e){return{name:"minLength",validate(t){const s=typeof t=="string"?t.trim().length:String(t??"").length;return s===0?!0:s>=r||e||!1}}}function x(r,e){return{name:"maxLength",validate(t){return(typeof t=="string"?t.trim().length:String(t??"").length)<=r||e||!1}}}function P(r){return{name:"number",validate(e){return typeof e!="string"||e.trim()===""?!0:!isNaN(Number(e))||r||!1}}}function V(r){return{name:"integer",validate(e){if(typeof e!="string"||e.trim()==="")return!0;const t=Number(e);return!isNaN(t)&&Number.isInteger(t)||r||!1}}}function I(r,e){return{name:"min",validate(t){if(typeof t!="string"||t.trim()==="")return!0;const s=Number(t);return isNaN(s)?!0:s>=r||e||!1}}}function $(r,e){return{name:"max",validate(t){if(typeof t!="string"||t.trim()==="")return!0;const s=Number(t);return isNaN(s)?!0:s<=r||e||!1}}}function q(r,e){return{name:"pattern",validate(t){return typeof t!="string"||t.trim()===""?!0:r.test(t)||e||!1}}}function N(r,e){return{name:"sameAs",validate(t,s){const i=s.querySelector(`[name="${r}"]`);if(!i)return!0;const n=typeof t=="string"?t:"",a=i instanceof HTMLInputElement&&(i.type==="checkbox"||i.type==="radio")?i.checked:i.value;return n===String(a)||e||!1}}}function B(r){return{name:"files",validate(e){return e instanceof FileList?e.length>0||r||!1:!0}}}function _(r,e){return{name:"minFiles",validate(t){return!(t instanceof FileList)||t.length===0?!0:t.length>=r||e||!1}}}function z(r,e){return{name:"maxFiles",validate(t){return t instanceof FileList?t.length<=r||e||!1:!0}}}function Z(r,e){return{name:e??"custom",validate(t,s,i,n){return r(t,s,n)}}}function O(r,e){return{name:(e==null?void 0:e.name)??"async",validate(t,s,i,n){return r(t,s,n)}}}function W(r){const e=/[A-Z]/,t=/[a-z]/,s=/\d/,i=/[!@#$%^&*(),.?":{}|<>_\-+=\[\]\\;'/`~]/;return{name:"strongPassword",validate(n){return typeof n!="string"||n.trim()===""?!0:e.test(n)&&t.test(n)&&s.test(n)&&i.test(n)&&n.length>=8||r||!1}}}exports.FormGuard=T;exports.asyncRule=O;exports.custom=Z;exports.email=w;exports.en=v;exports.files=B;exports.integer=V;exports.max=$;exports.maxFiles=z;exports.maxLength=x;exports.min=I;exports.minFiles=_;exports.minLength=H;exports.number=P;exports.pattern=q;exports.required=G;exports.sameAs=N;exports.strongPassword=W;
|
|
2
|
+
//# sourceMappingURL=form-guard.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-guard.cjs","sources":["../src/locales/en.ts","../src/dom.ts","../src/FormGuard.ts","../src/rules.ts"],"sourcesContent":["import type { Locale } from '../types';\n\nconst en: Locale = {\n required: 'This field is required',\n email: 'Please enter a valid email address',\n minLength: 'Must be at least {0} characters',\n maxLength: 'Must be no more than {0} characters',\n number: 'Must be a number',\n integer: 'Must be an integer',\n min: 'Must be at least {0}',\n max: 'Must be no more than {0}',\n pattern: 'Invalid format',\n sameAs: 'Must match {0}',\n files: 'Please select at least one file',\n minFiles: 'Please select at least {0} files',\n maxFiles: 'Please select at most {0} files',\n strongPassword:\n 'Password must contain uppercase, lowercase, digit, and special character',\n};\n\nexport default en;\n","import type { ValidationError, FieldConfig, FormGuardConfig } from './types';\n\nexport function resolveErrorContainer(\n fieldConfig?: FieldConfig,\n globalConfig?: FormGuardConfig,\n element?: HTMLElement,\n): HTMLElement | null {\n const container =\n fieldConfig?.errorContainer ??\n globalConfig?.errorContainer;\n\n if (!container) return null;\n if (container instanceof HTMLElement) return container;\n\n const selector = container as string;\n if (element?.closest) {\n const found = element.closest(selector) as HTMLElement | null;\n if (found) return found;\n }\n return document.querySelector(selector);\n}\n\nexport function clearFieldDOM(\n element: HTMLElement,\n fieldConfig?: FieldConfig,\n globalConfig?: FormGuardConfig,\n): void {\n const errorClass = fieldConfig?.errorClass ?? globalConfig?.errorClass ?? 'form-guard-error';\n const successClass = fieldConfig?.successClass ?? globalConfig?.successClass ?? 'form-guard-success';\n const pendingClass = globalConfig?.pendingClass ?? 'form-guard-pending';\n\n element.classList.remove(errorClass);\n element.classList.remove(successClass);\n element.classList.remove(pendingClass);\n element.removeAttribute('aria-invalid');\n element.removeAttribute('aria-describedby');\n\n const container = resolveErrorContainer(fieldConfig, globalConfig, element);\n if (container) {\n container.innerHTML = '';\n container.setAttribute('role', 'alert');\n }\n}\n\nexport function renderFieldPending(\n element: HTMLElement,\n fieldConfig?: FieldConfig,\n globalConfig?: FormGuardConfig,\n): void {\n const errorClass = fieldConfig?.errorClass ?? globalConfig?.errorClass ?? 'form-guard-error';\n const successClass = fieldConfig?.successClass ?? globalConfig?.successClass ?? 'form-guard-success';\n const pendingClass = globalConfig?.pendingClass ?? 'form-guard-pending';\n\n element.classList.remove(errorClass);\n element.classList.remove(successClass);\n element.classList.add(pendingClass);\n element.removeAttribute('aria-invalid');\n element.removeAttribute('aria-describedby');\n\n const container = resolveErrorContainer(fieldConfig, globalConfig, element);\n if (container) {\n container.innerHTML = '<span class=\"form-guard-message form-guard-pending-text\">Checking...</span>';\n container.setAttribute('role', 'status');\n container.setAttribute('aria-live', 'polite');\n }\n}\n\nexport function renderFieldError(\n element: HTMLElement,\n error: ValidationError,\n fieldConfig?: FieldConfig,\n globalConfig?: FormGuardConfig,\n): void {\n const errorClass = fieldConfig?.errorClass ?? globalConfig?.errorClass ?? 'form-guard-error';\n const successClass = fieldConfig?.successClass ?? globalConfig?.successClass ?? 'form-guard-success';\n const pendingClass = globalConfig?.pendingClass ?? 'form-guard-pending';\n\n element.classList.add(errorClass);\n element.classList.remove(successClass);\n element.classList.remove(pendingClass);\n element.setAttribute('aria-invalid', 'true');\n\n const container = resolveErrorContainer(fieldConfig, globalConfig, element);\n if (container) {\n const errorId = `form-guard-error-${error.field}`;\n container.innerHTML = `<span id=\"${errorId}\" class=\"form-guard-message\">${escapeHtml(error.message)}</span>`;\n container.setAttribute('role', 'alert');\n element.setAttribute('aria-describedby', errorId);\n }\n}\n\nexport function renderFieldSuccess(\n element: HTMLElement,\n fieldConfig?: FieldConfig,\n globalConfig?: FormGuardConfig,\n): void {\n const errorClass = fieldConfig?.errorClass ?? globalConfig?.errorClass ?? 'form-guard-error';\n const successClass = fieldConfig?.successClass ?? globalConfig?.successClass ?? 'form-guard-success';\n const pendingClass = globalConfig?.pendingClass ?? 'form-guard-pending';\n\n element.classList.remove(errorClass);\n element.classList.add(successClass);\n element.classList.remove(pendingClass);\n element.removeAttribute('aria-invalid');\n element.removeAttribute('aria-describedby');\n\n const container = resolveErrorContainer(fieldConfig, globalConfig, element);\n if (container) {\n container.innerHTML = '';\n }\n}\n\nexport function focusFirstInvalid(\n fields: { element: HTMLElement; errors: ValidationError[] }[],\n): void {\n for (const { element, errors } of fields) {\n if (errors.length > 0) {\n element.focus();\n return;\n }\n }\n}\n\nexport function escapeHtml(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n","import type {\n FormGuardInstance,\n FormGuardConfig,\n Rule,\n FieldConfig,\n GroupConfig,\n FieldState,\n GroupState,\n ValidationError,\n Locale,\n SuccessCallback,\n FailCallback,\n ValidateCallback,\n} from './types';\nimport { en } from './locales';\nimport {\n clearFieldDOM,\n renderFieldError,\n renderFieldSuccess,\n renderFieldPending,\n focusFirstInvalid,\n} from './dom';\n\nfunction resolveElement(\n selector: string,\n form: HTMLFormElement,\n): HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null {\n if (selector.startsWith('#') || selector.startsWith('.') || selector.includes('[')) {\n return form.querySelector(selector);\n }\n return form.querySelector(`[name=\"${selector}\"]`);\n}\n\nfunction resolveElements(\n selector: string,\n form: HTMLFormElement,\n): (HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement)[] {\n if (selector.startsWith('#') || selector.startsWith('.') || selector.includes('[')) {\n return Array.from(\n form.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(selector),\n );\n }\n return Array.from(\n form.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(\n `[name=\"${selector}\"]`,\n ),\n );\n}\n\nfunction getFieldValue(\n el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,\n): unknown {\n if (el instanceof HTMLInputElement && el.type === 'file') {\n return (el as HTMLInputElement).files;\n }\n if (\n el instanceof HTMLInputElement &&\n (el.type === 'checkbox' || el.type === 'radio')\n ) {\n return el.checked;\n }\n if (el instanceof HTMLSelectElement && el.multiple) {\n const selected = Array.from(el.selectedOptions).map((opt) => opt.value);\n return selected;\n }\n return el.value;\n}\n\nfunction getGroupValue(\n elements: (HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement)[],\n): unknown {\n const checkedValues: string[] = [];\n for (const el of elements) {\n if (\n el instanceof HTMLInputElement &&\n (el.type === 'checkbox' || el.type === 'radio')\n ) {\n if (el.checked) {\n checkedValues.push(el.value);\n }\n }\n }\n if (elements.length > 0) {\n const first = elements[0]!;\n if (\n first instanceof HTMLInputElement &&\n first.type === 'radio'\n ) {\n return checkedValues.length > 0 ? checkedValues[0] : '';\n }\n }\n return checkedValues;\n}\n\nfunction getFieldLabel(\n el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,\n fieldConfig?: FieldConfig,\n): string {\n if (fieldConfig?.label) return fieldConfig.label;\n if (el.labels && el.labels.length > 0) return el.labels[0]!.textContent?.trim() ?? el.name;\n return el.name || 'field';\n}\n\nfunction makeError(\n code: string,\n message: string,\n field: string,\n rule: string,\n value: unknown,\n): ValidationError {\n return { code, message, field, rule, value, meta: {} };\n}\n\nexport class FormGuard implements FormGuardInstance {\n private form: HTMLFormElement;\n private config: FormGuardConfig;\n private fields = new Map<string, FieldState>();\n private groups = new Map<string, GroupState>();\n private locale: Locale;\n private successCallbacks: SuccessCallback[] = [];\n private failCallbacks: FailCallback[] = [];\n private validateCallbacks: ValidateCallback[] = [];\n private boundSubmit: ((e: Event) => void) | null = null;\n private boundBlur: ((e: Event) => void) | null = null;\n private boundChange: ((e: Event) => void) | null = null;\n private destroyed = false;\n\n constructor(form: string | HTMLFormElement, config?: FormGuardConfig) {\n this.config = {\n mode: 'onSubmit',\n focusOnError: true,\n lockForm: false,\n collectAll: false,\n debounceMs: 0,\n ...config,\n };\n this.locale = { ...en, ...config?.locale };\n\n if (typeof form === 'string') {\n const el = document.querySelector<HTMLFormElement>(form);\n if (!el) {\n throw new Error(`FormGuard: form not found for selector \"${form}\"`);\n }\n this.form = el;\n } else {\n this.form = form;\n }\n\n this.bindEvents();\n }\n\n addField(\n fieldSelector: string,\n rules: Rule[],\n config?: FieldConfig,\n ): FormGuardInstance {\n if (this.destroyed) return this;\n const key = fieldSelector;\n const el = resolveElement(fieldSelector, this.form);\n if (!el) {\n if (this.config.debug) {\n console.warn(`FormGuard: field \"${fieldSelector}\" not found`);\n }\n return this;\n }\n\n this.fields.set(key, {\n field: el,\n rules: [...rules],\n config: config ?? {},\n errors: [],\n validated: false,\n pending: false,\n abortController: null,\n debounceTimer: null,\n });\n\n if (this.config.mode === 'onBlur') {\n el.addEventListener('blur', this.handleBlur(key));\n }\n if (this.config.mode === 'onChange') {\n const eventType =\n el instanceof HTMLInputElement &&\n (el.type === 'checkbox' || el.type === 'radio')\n ? 'change'\n : 'input';\n el.addEventListener(eventType, this.handleChange(key));\n }\n\n return this;\n }\n\n removeField(fieldSelector: string): FormGuardInstance {\n const state = this.fields.get(fieldSelector);\n if (state) {\n this.cancelFieldValidation(state);\n clearFieldDOM(state.field, state.config, this.config);\n state.field.removeEventListener('blur', this.handleBlur(fieldSelector));\n state.field.removeEventListener('input', this.handleChange(fieldSelector));\n state.field.removeEventListener('change', this.handleChange(fieldSelector));\n }\n this.fields.delete(fieldSelector);\n return this;\n }\n\n addGroup(\n groupSelector: string,\n rules: Rule[],\n config?: GroupConfig,\n ): FormGuardInstance {\n if (this.destroyed) return this;\n const elements = resolveElements(groupSelector, this.form);\n if (elements.length === 0) {\n if (this.config.debug) {\n console.warn(`FormGuard: no elements found for group \"${groupSelector}\"`);\n }\n return this;\n }\n\n this.groups.set(groupSelector, {\n elements,\n rules: [...rules],\n config: config ?? {},\n errors: [],\n validated: false,\n pending: false,\n abortController: null,\n debounceTimer: null,\n });\n\n if (this.config.mode === 'onBlur' || this.config.mode === 'onChange') {\n for (const el of elements) {\n el.addEventListener('blur', this.handleGroupEvent(groupSelector));\n const eventType =\n el instanceof HTMLInputElement &&\n (el.type === 'checkbox' || el.type === 'radio')\n ? 'change'\n : 'input';\n el.addEventListener(eventType, this.handleGroupEvent(groupSelector));\n }\n }\n\n return this;\n }\n\n removeGroup(groupSelector: string): FormGuardInstance {\n const state = this.groups.get(groupSelector);\n if (state) {\n this.cancelFieldValidation(state);\n for (const el of state.elements) {\n el.removeEventListener('blur', this.handleGroupEvent(groupSelector));\n el.removeEventListener('input', this.handleGroupEvent(groupSelector));\n el.removeEventListener('change', this.handleGroupEvent(groupSelector));\n }\n }\n this.groups.delete(groupSelector);\n return this;\n }\n\n async validate(forceRevalidation = false): Promise<boolean> {\n if (this.destroyed) return false;\n\n for (const [, state] of this.fields) {\n if (!forceRevalidation && state.validated) continue;\n await this.validateFieldState(state);\n }\n\n for (const [, state] of this.groups) {\n if (!forceRevalidation && state.validated) continue;\n await this.validateGroupState(state);\n }\n\n const allErrors = this.getAllErrors();\n const valid = allErrors.length === 0;\n\n this.fireHooks(valid, allErrors);\n\n if (!valid && this.config.focusOnError) {\n const fieldEntries: { element: HTMLElement; errors: ValidationError[] }[] = [];\n for (const [, state] of this.fields) {\n if (state.errors.length > 0) {\n fieldEntries.push({ element: state.field, errors: state.errors });\n }\n }\n focusFirstInvalid(fieldEntries);\n }\n\n return valid;\n }\n\n async validateField(fieldSelector: string): Promise<boolean> {\n const state = this.fields.get(fieldSelector);\n if (!state) return true;\n await this.validateFieldState(state);\n return state.errors.length === 0;\n }\n\n async validateGroup(groupSelector: string): Promise<boolean> {\n const state = this.groups.get(groupSelector);\n if (!state) return true;\n await this.validateGroupState(state);\n return state.errors.length === 0;\n }\n\n async revalidate(): Promise<boolean> {\n for (const [, state] of this.fields) {\n state.validated = false;\n }\n for (const [, state] of this.groups) {\n state.validated = false;\n }\n return this.validate(true);\n }\n\n async revalidateField(fieldSelector: string): Promise<boolean> {\n const state = this.fields.get(fieldSelector);\n if (!state) return true;\n state.validated = false;\n await this.validateFieldState(state);\n return state.errors.length === 0;\n }\n\n async revalidateGroup(groupSelector: string): Promise<boolean> {\n const state = this.groups.get(groupSelector);\n if (!state) return true;\n state.validated = false;\n await this.validateGroupState(state);\n return state.errors.length === 0;\n }\n\n clearErrors(): FormGuardInstance {\n for (const [, state] of this.fields) {\n this.cancelFieldValidation(state);\n state.errors = [];\n state.validated = false;\n state.pending = false;\n clearFieldDOM(state.field, state.config, this.config);\n }\n for (const [, state] of this.groups) {\n this.cancelFieldValidation(state);\n state.errors = [];\n state.validated = false;\n state.pending = false;\n }\n return this;\n }\n\n refresh(): FormGuardInstance {\n const fieldEntries = [...this.fields.entries()];\n for (const [key, state] of fieldEntries) {\n const el = resolveElement(key, this.form);\n if (el) {\n state.field = el;\n } else {\n this.fields.delete(key);\n }\n }\n\n const groupEntries = [...this.groups.entries()];\n for (const [key, state] of groupEntries) {\n const elements = resolveElements(key, this.form);\n if (elements.length > 0) {\n state.elements = elements;\n } else {\n this.groups.delete(key);\n }\n }\n\n return this;\n }\n\n destroy(): void {\n this.unbindEvents();\n for (const [key, state] of this.fields) {\n this.cancelFieldValidation(state);\n clearFieldDOM(state.field, state.config, this.config);\n state.field.removeEventListener('blur', this.handleBlur(key));\n state.field.removeEventListener('input', this.handleChange(key));\n state.field.removeEventListener('change', this.handleChange(key));\n }\n for (const [key, state] of this.groups) {\n this.cancelFieldValidation(state);\n for (const el of state.elements) {\n el.removeEventListener('blur', this.handleGroupEvent(key));\n el.removeEventListener('input', this.handleGroupEvent(key));\n el.removeEventListener('change', this.handleGroupEvent(key));\n }\n }\n this.fields.clear();\n this.groups.clear();\n this.successCallbacks.length = 0;\n this.failCallbacks.length = 0;\n this.validateCallbacks.length = 0;\n this.destroyed = true;\n }\n\n setLocale(locale: Locale): FormGuardInstance {\n this.locale = { ...this.locale, ...locale };\n return this;\n }\n\n onSuccess(callback: SuccessCallback): FormGuardInstance {\n this.successCallbacks.push(callback);\n return this;\n }\n\n onFail(callback: FailCallback): FormGuardInstance {\n this.failCallbacks.push(callback);\n return this;\n }\n\n onValidate(callback: ValidateCallback): FormGuardInstance {\n this.validateCallbacks.push(callback);\n return this;\n }\n\n getErrors(): ValidationError[] {\n return this.getAllErrors();\n }\n\n getFieldErrors(fieldSelector: string): ValidationError[] {\n return this.fields.get(fieldSelector)?.errors ?? [];\n }\n\n getGroupErrors(groupSelector: string): ValidationError[] {\n return this.groups.get(groupSelector)?.errors ?? [];\n }\n\n isFieldPending(fieldSelector: string): boolean {\n return this.fields.get(fieldSelector)?.pending ?? false;\n }\n\n isPending(): boolean {\n for (const [, state] of this.fields) {\n if (state.pending) return true;\n }\n for (const [, state] of this.groups) {\n if (state.pending) return true;\n }\n return false;\n }\n\n private async validateFieldState(state: FieldState): Promise<void> {\n // Cancel any previous async validation for this field\n this.cancelFieldValidation(state);\n\n const controller = new AbortController();\n state.abortController = controller;\n state.pending = true;\n state.errors = [];\n renderFieldPending(state.field, state.config, this.config);\n\n const value = getFieldValue(state.field);\n const label = getFieldLabel(state.field, state.config);\n\n try {\n for (const rule of state.rules) {\n if (controller.signal.aborted) break;\n\n try {\n const result = rule.validate(value, this.form, state.config, controller.signal);\n const outcome = result instanceof Promise ? await result : result;\n\n if (controller.signal.aborted) break;\n\n if (outcome === true || outcome === undefined) {\n continue;\n }\n\n let message: string;\n if (typeof outcome === 'string') {\n message = outcome;\n } else if (typeof outcome === 'object' && outcome !== null && 'message' in outcome) {\n message = String((outcome as ValidationError).message);\n } else {\n message = this.getMessage(rule.name, state.config, label);\n }\n\n state.errors.push(\n makeError(rule.name, message, state.field.name || label, rule.name, value),\n );\n\n if (!this.config.collectAll) break;\n } catch (err) {\n if (controller.signal.aborted) break;\n if (this.config.debug) {\n console.error(`FormGuard: rule \"${rule.name}\" threw:`, err);\n }\n state.errors.push(\n makeError(\n rule.name,\n this.getMessage(rule.name, state.config, label),\n state.field.name || label,\n rule.name,\n value,\n ),\n );\n if (!this.config.collectAll) break;\n }\n }\n } finally {\n // Only apply results if this controller hasn't been superseded\n if (state.abortController === controller) {\n state.pending = false;\n state.validated = true;\n state.abortController = null;\n\n if (!controller.signal.aborted) {\n if (state.errors.length > 0) {\n renderFieldError(state.field, state.errors[0]!, state.config, this.config);\n } else {\n renderFieldSuccess(state.field, state.config, this.config);\n }\n }\n }\n }\n }\n\n private async validateGroupState(state: GroupState): Promise<void> {\n this.cancelFieldValidation(state);\n\n const controller = new AbortController();\n state.abortController = controller;\n state.pending = true;\n state.errors = [];\n\n const value = getGroupValue(state.elements);\n const label = state.config.label ?? state.config.name ?? 'group';\n\n try {\n for (const rule of state.rules) {\n if (controller.signal.aborted) break;\n\n try {\n const result = rule.validate(value, this.form, state.config, controller.signal);\n const outcome = result instanceof Promise ? await result : result;\n\n if (controller.signal.aborted) break;\n\n if (outcome === true || outcome === undefined) {\n continue;\n }\n\n let message: string;\n if (typeof outcome === 'string') {\n message = outcome;\n } else {\n message = this.getMessage(rule.name, state.config, label);\n }\n\n state.errors.push(\n makeError(rule.name, message, label, rule.name, value),\n );\n\n if (!this.config.collectAll) break;\n } catch (err) {\n if (controller.signal.aborted) break;\n if (this.config.debug) {\n console.error(`FormGuard: rule \"${rule.name}\" threw:`, err);\n }\n state.errors.push(\n makeError(rule.name, this.getMessage(rule.name, state.config, label), label, rule.name, value),\n );\n if (!this.config.collectAll) break;\n }\n }\n } finally {\n if (state.abortController === controller) {\n state.pending = false;\n state.validated = true;\n state.abortController = null;\n }\n }\n }\n\n private cancelFieldValidation(state: FieldState | GroupState): void {\n if (state.debounceTimer !== null) {\n clearTimeout(state.debounceTimer);\n state.debounceTimer = null;\n }\n if (state.abortController) {\n state.abortController.abort();\n state.abortController = null;\n state.pending = false;\n }\n }\n\n private getMessage(\n ruleName: string,\n config?: FieldConfig | GroupConfig,\n _label?: string,\n ): string {\n const fieldMsg = config?.messages?.[ruleName];\n if (fieldMsg) return fieldMsg;\n return this.locale[ruleName] ?? ruleName;\n }\n\n private getAllErrors(): ValidationError[] {\n const all: ValidationError[] = [];\n for (const [, state] of this.fields) {\n all.push(...state.errors);\n }\n for (const [, state] of this.groups) {\n all.push(...state.errors);\n }\n return all;\n }\n\n private fireHooks(valid: boolean, errors: ValidationError[]): void {\n const fieldsErr: Record<string, ValidationError[]> = {};\n for (const [key, state] of this.fields) {\n if (state.errors.length > 0) {\n fieldsErr[key] = [...state.errors];\n }\n }\n const groupsErr: Record<string, ValidationError[]> = {};\n for (const [key, state] of this.groups) {\n if (state.errors.length > 0) {\n groupsErr[key] = [...state.errors];\n }\n }\n\n const result = { valid, errors, fields: fieldsErr, groups: groupsErr };\n\n for (const cb of this.validateCallbacks) {\n cb(result);\n }\n\n if (valid) {\n for (const cb of this.successCallbacks) {\n cb();\n }\n } else {\n for (const cb of this.failCallbacks) {\n cb(errors);\n }\n }\n }\n\n private handleSubmit = async (e: Event): Promise<void> => {\n if (this.config.mode !== 'onSubmit') return;\n e.preventDefault();\n const valid = await this.validate();\n if (valid) {\n this.form.submit();\n }\n };\n\n private handleBlur(key: string) {\n return () => {\n const state = this.fields.get(key);\n if (state) {\n this.debounceValidate(() => this.validateFieldState(state), state);\n }\n };\n }\n\n private handleChange(key: string) {\n return () => {\n const state = this.fields.get(key);\n if (state) {\n state.validated = false;\n this.debounceValidate(() => this.validateFieldState(state), state);\n }\n };\n }\n\n private handleGroupEvent(key: string) {\n return () => {\n const state = this.groups.get(key);\n if (state) {\n state.validated = false;\n this.debounceValidate(() => this.validateGroupState(state), state);\n }\n };\n }\n\n private debounceValidate(\n fn: () => Promise<void>,\n state: FieldState | GroupState,\n ): void {\n const debounceMs =\n 'config' in state && 'debounceMs' in state.config\n ? (state.config as FieldConfig).debounceMs ?? this.config.debounceMs ?? 0\n : this.config.debounceMs ?? 0;\n\n if (debounceMs && debounceMs > 0) {\n if (state.debounceTimer !== null) {\n clearTimeout(state.debounceTimer);\n }\n state.pending = true;\n if (state instanceof Object && 'field' in state) {\n renderFieldPending((state as FieldState).field, (state as FieldState).config, this.config);\n }\n state.debounceTimer = setTimeout(() => {\n state.debounceTimer = null;\n void fn();\n }, debounceMs);\n } else {\n void fn();\n }\n }\n\n private bindEvents(): void {\n if (this.config.mode === 'onSubmit') {\n this.boundSubmit = this.handleSubmit.bind(this);\n this.form.addEventListener('submit', this.boundSubmit);\n this.form.setAttribute('novalidate', '');\n }\n }\n\n private unbindEvents(): void {\n if (this.boundSubmit) {\n this.form.removeEventListener('submit', this.boundSubmit);\n this.boundSubmit = null;\n }\n if (this.boundBlur) {\n this.form.removeEventListener('blur', this.boundBlur);\n this.boundBlur = null;\n }\n if (this.boundChange) {\n this.form.removeEventListener('input', this.boundChange);\n this.boundChange = null;\n }\n this.form.removeAttribute('novalidate');\n }\n}\n","import type { Rule } from './types';\n\nconst EMAIL_RE =\n /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\\.[a-zA-Z]{2,}$/;\n\nexport function required(message?: string): Rule {\n return {\n name: 'required',\n validate(value: unknown): string | boolean {\n if (typeof value === 'boolean') {\n return value === true || message || false;\n }\n if (typeof value === 'string') {\n return value.trim().length > 0 || message || false;\n }\n if (value instanceof FileList) {\n return value.length > 0 || message || false;\n }\n if (Array.isArray(value)) {\n return value.length > 0 || message || false;\n }\n return (value != null && value !== '') || message || false;\n },\n };\n}\n\nexport function email(message?: string): Rule {\n return {\n name: 'email',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n return EMAIL_RE.test(value.trim()) || message || false;\n },\n };\n}\n\nexport function minLength(min: number, message?: string): Rule {\n return {\n name: 'minLength',\n validate(value: unknown): string | boolean {\n const len =\n typeof value === 'string' ? value.trim().length : String(value ?? '').length;\n if (len === 0) return true;\n return len >= min || message || false;\n },\n };\n}\n\nexport function maxLength(max: number, message?: string): Rule {\n return {\n name: 'maxLength',\n validate(value: unknown): string | boolean {\n const len =\n typeof value === 'string' ? value.trim().length : String(value ?? '').length;\n return len <= max || message || false;\n },\n };\n}\n\nexport function number(message?: string): Rule {\n return {\n name: 'number',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n return !isNaN(Number(value)) || message || false;\n },\n };\n}\n\nexport function integer(message?: string): Rule {\n return {\n name: 'integer',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n const n = Number(value);\n return (!isNaN(n) && Number.isInteger(n)) || message || false;\n },\n };\n}\n\nexport function min(minVal: number, message?: string): Rule {\n return {\n name: 'min',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n const n = Number(value);\n if (isNaN(n)) return true;\n return n >= minVal || message || false;\n },\n };\n}\n\nexport function max(maxVal: number, message?: string): Rule {\n return {\n name: 'max',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n const n = Number(value);\n if (isNaN(n)) return true;\n return n <= maxVal || message || false;\n },\n };\n}\n\nexport function pattern(regexp: RegExp, message?: string): Rule {\n return {\n name: 'pattern',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n return regexp.test(value) || message || false;\n },\n };\n}\n\nexport function sameAs(fieldName: string, message?: string): Rule {\n return {\n name: 'sameAs',\n validate(value: unknown, form: HTMLFormElement): string | boolean {\n const el = form.querySelector<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(\n `[name=\"${fieldName}\"]`,\n );\n if (!el) return true;\n const val = typeof value === 'string' ? value : '';\n const other =\n el instanceof HTMLInputElement &&\n (el.type === 'checkbox' || el.type === 'radio')\n ? el.checked\n : el.value;\n return val === String(other) || message || false;\n },\n };\n}\n\nexport function files(message?: string): Rule {\n return {\n name: 'files',\n validate(value: unknown): string | boolean {\n if (!(value instanceof FileList)) return true;\n return value.length > 0 || message || false;\n },\n };\n}\n\nexport function minFiles(minCount: number, message?: string): Rule {\n return {\n name: 'minFiles',\n validate(value: unknown): string | boolean {\n if (!(value instanceof FileList)) return true;\n if (value.length === 0) return true;\n return value.length >= minCount || message || false;\n },\n };\n}\n\nexport function maxFiles(maxCount: number, message?: string): Rule {\n return {\n name: 'maxFiles',\n validate(value: unknown): string | boolean {\n if (!(value instanceof FileList)) return true;\n return value.length <= maxCount || message || false;\n },\n };\n}\n\nexport function custom(\n fn: (\n value: unknown,\n form: HTMLFormElement,\n signal?: AbortSignal,\n ) => string | boolean | Promise<string | boolean>,\n name?: string,\n): Rule {\n return {\n name: name ?? 'custom',\n validate(value: unknown, form: HTMLFormElement, _fieldConfig?: unknown, signal?: AbortSignal) {\n return fn(value, form, signal);\n },\n };\n}\n\nexport function asyncRule(\n fn: (\n value: unknown,\n form: HTMLFormElement,\n signal?: AbortSignal,\n ) => Promise<string | boolean>,\n options?: { name?: string },\n): Rule {\n return {\n name: options?.name ?? 'async',\n validate(value: unknown, form: HTMLFormElement, _fieldConfig?: unknown, signal?: AbortSignal) {\n return fn(value, form, signal);\n },\n };\n}\n\nexport function strongPassword(message?: string): Rule {\n const HAS_UPPER = /[A-Z]/;\n const HAS_LOWER = /[a-z]/;\n const HAS_DIGIT = /\\d/;\n const HAS_SPECIAL = /[!@#$%^&*(),.?\":{}|<>_\\-+=\\[\\]\\\\;'/`~]/;\n\n return {\n name: 'strongPassword',\n validate(value: unknown): string | boolean {\n if (typeof value !== 'string' || value.trim() === '') return true;\n const ok =\n HAS_UPPER.test(value) &&\n HAS_LOWER.test(value) &&\n HAS_DIGIT.test(value) &&\n HAS_SPECIAL.test(value) &&\n value.length >= 8;\n return ok || message || false;\n },\n };\n}\n"],"names":["en","resolveErrorContainer","fieldConfig","globalConfig","element","container","selector","found","clearFieldDOM","errorClass","successClass","pendingClass","renderFieldPending","renderFieldError","error","errorId","escapeHtml","renderFieldSuccess","focusFirstInvalid","fields","errors","text","div","resolveElement","form","resolveElements","getFieldValue","el","opt","getGroupValue","elements","checkedValues","first","getFieldLabel","_a","makeError","code","message","field","rule","value","FormGuard","config","__publicField","fieldSelector","rules","key","eventType","state","groupSelector","forceRevalidation","allErrors","valid","fieldEntries","groupEntries","locale","callback","controller","label","result","outcome","err","ruleName","_label","fieldMsg","all","fieldsErr","groupsErr","cb","fn","debounceMs","EMAIL_RE","required","email","minLength","min","len","maxLength","max","number","integer","n","minVal","maxVal","pattern","regexp","sameAs","fieldName","val","other","files","minFiles","minCount","maxFiles","maxCount","custom","name","_fieldConfig","signal","asyncRule","options","strongPassword","HAS_UPPER","HAS_LOWER","HAS_DIGIT","HAS_SPECIAL"],"mappings":"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"}
|