@relax.js/core 1.0.2 → 1.0.4
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 +194 -188
- package/dist/DependencyInjection.d.ts +42 -24
- package/dist/collections/Index.d.ts +2 -0
- package/dist/collections/index.js +1 -1
- package/dist/collections/index.js.map +4 -4
- package/dist/collections/index.mjs +1 -1
- package/dist/collections/index.mjs.map +4 -4
- package/dist/di/index.js +1 -1
- package/dist/di/index.js.map +3 -3
- package/dist/di/index.mjs +1 -1
- package/dist/di/index.mjs.map +3 -3
- package/dist/errors.d.ts +20 -0
- package/dist/forms/FormValidator.d.ts +1 -20
- package/dist/forms/ValidationRules.d.ts +2 -0
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +4 -4
- package/dist/forms/index.mjs +1 -1
- package/dist/forms/index.mjs.map +4 -4
- package/dist/html/TableRenderer.d.ts +1 -0
- package/dist/html/index.js.map +2 -2
- package/dist/html/index.mjs.map +2 -2
- package/dist/html/template.d.ts +4 -0
- package/dist/http/http.d.ts +1 -0
- package/dist/http/index.js.map +2 -2
- package/dist/http/index.mjs.map +2 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +4 -4
- package/dist/routing/index.js +3 -3
- package/dist/routing/index.js.map +3 -3
- package/dist/routing/index.mjs +3 -3
- package/dist/routing/index.mjs.map +3 -3
- package/dist/routing/routeTargetRegistry.d.ts +1 -0
- package/dist/routing/types.d.ts +2 -1
- package/dist/templates/NodeTemplate.d.ts +2 -0
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +2 -2
- package/docs/Architecture.md +333 -333
- package/docs/DependencyInjection.md +277 -237
- package/docs/Errors.md +87 -87
- package/docs/GettingStarted.md +231 -231
- package/docs/Pipes.md +5 -5
- package/docs/Translations.md +167 -312
- package/docs/WhyRelaxjs.md +336 -336
- package/docs/api/.nojekyll +1 -0
- package/docs/api/assets/hierarchy.js +1 -0
- package/docs/api/assets/highlight.css +120 -0
- package/docs/api/assets/icons.js +18 -0
- package/docs/api/assets/icons.svg +1 -0
- package/docs/api/assets/main.js +60 -0
- package/docs/api/assets/navigation.js +1 -0
- package/docs/api/assets/search.js +1 -0
- package/docs/api/assets/style.css +1633 -0
- package/docs/api/classes/http.WebSocketClient.html +26 -0
- package/docs/api/classes/i18n.LocaleChangeEvent.html +66 -0
- package/docs/api/classes/index.Blueprint.html +3 -0
- package/docs/api/classes/index.BoundNode.html +3 -0
- package/docs/api/classes/index.DigitsValidation.html +10 -0
- package/docs/api/classes/index.FormValidator.html +32 -0
- package/docs/api/classes/index.HttpError.html +13 -0
- package/docs/api/classes/index.LinkedList.html +26 -0
- package/docs/api/classes/index.NavigateRouteEvent.html +76 -0
- package/docs/api/classes/index.Node.html +15 -0
- package/docs/api/classes/index.PageSelectedEvent.html +61 -0
- package/docs/api/classes/index.Pager.html +4 -0
- package/docs/api/classes/index.RangeValidation.html +15 -0
- package/docs/api/classes/index.RelaxError.html +17 -0
- package/docs/api/classes/index.RequiredValidation.html +10 -0
- package/docs/api/classes/index.RouteError.html +11 -0
- package/docs/api/classes/index.RouteGuardError.html +12 -0
- package/docs/api/classes/index.RouteLink.html +779 -0
- package/docs/api/classes/index.RouteTarget.html +788 -0
- package/docs/api/classes/index.SSEClient.html +13 -0
- package/docs/api/classes/index.SSEDataEvent.html +63 -0
- package/docs/api/classes/index.ServiceCollection.html +28 -0
- package/docs/api/classes/index.ServiceContainer.html +24 -0
- package/docs/api/classes/index.SortChangeEvent.html +61 -0
- package/docs/api/classes/index.TableRenderer.html +5 -0
- package/docs/api/classes/index.TableSorter.html +4 -0
- package/docs/api/enums/index.GuardResult.html +9 -0
- package/docs/api/functions/elements.formError.html +6 -0
- package/docs/api/functions/elements.selectOne.html +6 -0
- package/docs/api/functions/i18n.getCurrentLocale.html +3 -0
- package/docs/api/functions/i18n.loadNamespace.html +7 -0
- package/docs/api/functions/i18n.loadNamespaces.html +6 -0
- package/docs/api/functions/i18n.onMissingTranslation.html +7 -0
- package/docs/api/functions/i18n.setLocale.html +7 -0
- package/docs/api/functions/i18n.setMessageFormatter.html +7 -0
- package/docs/api/functions/i18n.t.html +9 -0
- package/docs/api/functions/index.BooleanConverter.html +6 -0
- package/docs/api/functions/index.ContainerService.html +13 -0
- package/docs/api/functions/index.DateConverter.html +11 -0
- package/docs/api/functions/index.Inject.html +16 -0
- package/docs/api/functions/index.NumberConverter.html +5 -0
- package/docs/api/functions/index.RegisterValidator.html +7 -0
- package/docs/api/functions/index.applyPipes.html +17 -0
- package/docs/api/functions/index.asyncHandler.html +11 -0
- package/docs/api/functions/index.capitalizePipe.html +4 -0
- package/docs/api/functions/index.clearPendingNavigations.html +1 -0
- package/docs/api/functions/index.compileTemplate.html +26 -0
- package/docs/api/functions/index.configure.html +5 -0
- package/docs/api/functions/index.createBluePrint.html +1 -0
- package/docs/api/functions/index.createConverterFromDataType.html +4 -0
- package/docs/api/functions/index.createConverterFromInputType.html +5 -0
- package/docs/api/functions/index.createPipeRegistry.html +12 -0
- package/docs/api/functions/index.currencyPipe.html +9 -0
- package/docs/api/functions/index.datePipe.html +9 -0
- package/docs/api/functions/index.daysAgoPipe.html +8 -0
- package/docs/api/functions/index.defaultPipe.html +5 -0
- package/docs/api/functions/index.defineRoutes.html +8 -0
- package/docs/api/functions/index.del.html +8 -0
- package/docs/api/functions/index.findRouteByName.html +5 -0
- package/docs/api/functions/index.findRouteByUrl.html +4 -0
- package/docs/api/functions/index.firstPipe.html +4 -0
- package/docs/api/functions/index.generateSequentialId.html +21 -0
- package/docs/api/functions/index.get.html +9 -0
- package/docs/api/functions/index.getDataConverter.html +11 -0
- package/docs/api/functions/index.getParentComponent.html +18 -0
- package/docs/api/functions/index.getValidator.html +4 -0
- package/docs/api/functions/index.html.html +19 -0
- package/docs/api/functions/index.joinPipe.html +5 -0
- package/docs/api/functions/index.keysPipe.html +4 -0
- package/docs/api/functions/index.lastPipe.html +4 -0
- package/docs/api/functions/index.lowercasePipe.html +4 -0
- package/docs/api/functions/index.mapFormToClass.html +17 -0
- package/docs/api/functions/index.matchRoute.html +5 -0
- package/docs/api/functions/index.navigate.html +8 -0
- package/docs/api/functions/index.onError.html +8 -0
- package/docs/api/functions/index.piecesPipe.html +8 -0
- package/docs/api/functions/index.post.html +9 -0
- package/docs/api/functions/index.printRoutes.html +2 -0
- package/docs/api/functions/index.put.html +9 -0
- package/docs/api/functions/index.readData.html +17 -0
- package/docs/api/functions/index.registerRouteTarget.html +9 -0
- package/docs/api/functions/index.reportError.html +10 -0
- package/docs/api/functions/index.request.html +8 -0
- package/docs/api/functions/index.resolveValue.html +18 -0
- package/docs/api/functions/index.setFetch.html +6 -0
- package/docs/api/functions/index.setFormData.html +17 -0
- package/docs/api/functions/index.shortenPipe.html +5 -0
- package/docs/api/functions/index.startRouting.html +6 -0
- package/docs/api/functions/index.ternaryPipe.html +6 -0
- package/docs/api/functions/index.trimPipe.html +4 -0
- package/docs/api/functions/index.unregisterRouteTarget.html +3 -0
- package/docs/api/functions/index.uppercasePipe.html +4 -0
- package/docs/api/hierarchy.html +1 -0
- package/docs/api/index.html +323 -0
- package/docs/api/interfaces/http.SimpleDataEvent.html +3 -0
- package/docs/api/interfaces/http.WebSocketAbstraction.html +9 -0
- package/docs/api/interfaces/http.WebSocketCodec.html +4 -0
- package/docs/api/interfaces/http.WebSocketOptions.html +20 -0
- package/docs/api/interfaces/index.CompiledTemplate.html +10 -0
- package/docs/api/interfaces/index.DataLoader.html +19 -0
- package/docs/api/interfaces/index.EngineConfig.html +11 -0
- package/docs/api/interfaces/index.ErrorContext.html +4 -0
- package/docs/api/interfaces/index.FormReaderOptions.html +8 -0
- package/docs/api/interfaces/index.HttpOptions.html +16 -0
- package/docs/api/interfaces/index.HttpResponse.html +17 -0
- package/docs/api/interfaces/index.LoadRoute.html +7 -0
- package/docs/api/interfaces/index.NavigateOptions.html +7 -0
- package/docs/api/interfaces/index.PipeRegistry.html +12 -0
- package/docs/api/interfaces/index.RegistrationOptions.html +22 -0
- package/docs/api/interfaces/index.RenderTemplate.html +7 -0
- package/docs/api/interfaces/index.RequestOptions.html +11 -0
- package/docs/api/interfaces/index.Routable.html +10 -0
- package/docs/api/interfaces/index.Route.html +13 -0
- package/docs/api/interfaces/index.RouteGuard.html +2 -0
- package/docs/api/interfaces/index.RouteValue.html +6 -0
- package/docs/api/interfaces/index.SSEOptions.html +24 -0
- package/docs/api/interfaces/index.ValidationContext.html +8 -0
- package/docs/api/interfaces/index.ValidatorOptions.html +14 -0
- package/docs/api/media/Architecture.md +333 -0
- package/docs/api/media/DependencyInjection.md +277 -0
- package/docs/api/media/GettingStarted.md +231 -0
- package/docs/api/media/HttpClient.md +459 -0
- package/docs/api/media/Pipes.md +211 -0
- package/docs/api/media/Routing.md +332 -0
- package/docs/api/media/WhyRelaxjs.md +336 -0
- package/docs/api/media/forms.md +99 -0
- package/docs/api/media/html.md +175 -0
- package/docs/api/media/i18n.md +354 -0
- package/docs/api/media/utilities.md +143 -0
- package/docs/api/media/validation.md +351 -0
- package/docs/api/modules/collections_Index.html +1 -0
- package/docs/api/modules/di.html +1 -0
- package/docs/api/modules/elements.html +1 -0
- package/docs/api/modules/forms.html +1 -0
- package/docs/api/modules/html.html +1 -0
- package/docs/api/modules/http.html +1 -0
- package/docs/api/modules/i18n.html +1 -0
- package/docs/api/modules/index.html +1 -0
- package/docs/api/modules/routing.html +1 -0
- package/docs/api/modules/utils.html +1 -0
- package/docs/api/modules.html +1 -0
- package/docs/api/types/http.WebSocketFactory.html +2 -0
- package/docs/api/types/i18n.MessageFormatter.html +3 -0
- package/docs/api/types/i18n.MissingTranslationHandler.html +1 -0
- package/docs/api/types/index.Constructor.html +7 -0
- package/docs/api/types/index.ConverterFunc.html +2 -0
- package/docs/api/types/index.DataType.html +2 -0
- package/docs/api/types/index.InputType.html +2 -0
- package/docs/api/types/index.PipeFunction.html +6 -0
- package/docs/api/types/index.RouteData.html +1 -0
- package/docs/api/types/index.RouteMatchResult.html +9 -0
- package/docs/api/types/index.RouteParamType.html +1 -0
- package/docs/api/types/index.RouteSegmentType.html +2 -0
- package/docs/api/types/index.SSEEventFactory.html +5 -0
- package/docs/api/types/index.ServiceScope.html +10 -0
- package/docs/api/types/index.SortColumn.html +3 -0
- package/docs/api/variables/i18n.formatICU.html +3 -0
- package/docs/api/variables/index.container.html +6 -0
- package/docs/api/variables/index.defaultPipes.html +6 -0
- package/docs/api/variables/index.internalRoutes.html +1 -0
- package/docs/api/variables/index.serviceCollection.html +6 -0
- package/docs/api.json +93171 -0
- package/docs/elements/dom.md +102 -102
- package/docs/forms/creating-form-components.md +924 -924
- package/docs/forms/form-api.md +94 -94
- package/docs/forms/forms.md +99 -99
- package/docs/forms/patterns.md +311 -311
- package/docs/forms/reading-writing.md +365 -365
- package/docs/forms/validation.md +351 -351
- package/docs/html/TableRenderer.md +291 -291
- package/docs/html/html.md +175 -175
- package/docs/html/index.md +54 -54
- package/docs/html/template.md +422 -422
- package/docs/http/HttpClient.md +459 -459
- package/docs/http/ServerSentEvents.md +184 -184
- package/docs/http/index.md +109 -109
- package/docs/i18n/i18n.md +49 -4
- package/docs/i18n/intl-standard.md +178 -178
- package/docs/routing/RouteLink.md +98 -98
- package/docs/routing/Routing.md +332 -332
- package/docs/routing/layouts.md +207 -207
- package/docs/utilities.md +143 -143
- package/package.json +4 -3
package/docs/forms/validation.md
CHANGED
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
# Form Validation
|
|
2
|
-
|
|
3
|
-
The `FormValidator` class provides form validation with HTML5 integration, error summaries, and custom validation support.
|
|
4
|
-
|
|
5
|
-
## Basic Usage
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { FormValidator } from '
|
|
9
|
-
|
|
10
|
-
const form = document.querySelector('form');
|
|
11
|
-
const validator = new FormValidator(form, {
|
|
12
|
-
submitCallback: () => saveData()
|
|
13
|
-
});
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Configuration Options
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
interface ValidatorOptions {
|
|
20
|
-
autoValidate?: boolean; // Validate on every input event
|
|
21
|
-
useSummary?: boolean; // Show errors in summary element
|
|
22
|
-
customChecks?: (form: HTMLFormElement) => void; // Custom validation
|
|
23
|
-
preventDefault?: boolean; // Always prevent form submission
|
|
24
|
-
preventDefaultOnFailed?: boolean; // Prevent submission on failure (default: true)
|
|
25
|
-
submitCallback?: () => void; // Called when form is valid
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Auto-Validation
|
|
30
|
-
|
|
31
|
-
Enable real-time validation as users type:
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
const validator = new FormValidator(form, {
|
|
35
|
-
autoValidate: true,
|
|
36
|
-
useSummary: true
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Error Summary Display
|
|
41
|
-
|
|
42
|
-
Instead of browser tooltips, show errors in a summary element:
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
const validator = new FormValidator(form, {
|
|
46
|
-
useSummary: true,
|
|
47
|
-
submitCallback: () => handleSubmit()
|
|
48
|
-
});
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The error summary is automatically created and prepended to the form with:
|
|
52
|
-
- `role="alert"` for accessibility
|
|
53
|
-
- `aria-live="assertive"` for screen readers
|
|
54
|
-
- `class="error-summary"` for styling
|
|
55
|
-
|
|
56
|
-
## Custom Validation
|
|
57
|
-
|
|
58
|
-
Add business logic validation beyond HTML5 constraints:
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
const validator = new FormValidator(form, {
|
|
62
|
-
useSummary: true,
|
|
63
|
-
customChecks: (form) => {
|
|
64
|
-
const password = form.querySelector('[name="password"]') as HTMLInputElement;
|
|
65
|
-
const confirm = form.querySelector('[name="confirmPassword"]') as HTMLInputElement;
|
|
66
|
-
|
|
67
|
-
if (password.value !== confirm.value) {
|
|
68
|
-
validator.addErrorToSummary('Password Confirmation', 'Passwords do not match');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const startDate = new Date(form.querySelector('[name="startDate"]').value);
|
|
72
|
-
const endDate = new Date(form.querySelector('[name="endDate"]').value);
|
|
73
|
-
|
|
74
|
-
if (endDate <= startDate) {
|
|
75
|
-
validator.addErrorToSummary('End Date', 'Must be after start date');
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
submitCallback: () => saveData()
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Manual Validation
|
|
83
|
-
|
|
84
|
-
Trigger validation programmatically:
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
if (validator.validateForm()) {
|
|
88
|
-
// Form is valid
|
|
89
|
-
proceedToNextStep();
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Error Summary Methods
|
|
94
|
-
|
|
95
|
-
### addErrorToSummary
|
|
96
|
-
|
|
97
|
-
Add a single error to the summary:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
validator.addErrorToSummary('Email', 'This email is already registered');
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### displayErrorSummary
|
|
104
|
-
|
|
105
|
-
Display multiple errors at once:
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
validator.displayErrorSummary([
|
|
109
|
-
'Name: This field is required',
|
|
110
|
-
'Email: Invalid email format',
|
|
111
|
-
'Age: Must be 18 or older'
|
|
112
|
-
]);
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### clearErrorSummary
|
|
116
|
-
|
|
117
|
-
Remove all errors from the summary:
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
validator.clearErrorSummary();
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Finding Forms
|
|
124
|
-
|
|
125
|
-
Use `FindForm` to locate a form relative to a custom element:
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
class MyComponent extends HTMLElement {
|
|
129
|
-
connectedCallback() {
|
|
130
|
-
const form = FormValidator.FindForm(this);
|
|
131
|
-
new FormValidator(form);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
`FindForm` searches:
|
|
137
|
-
1. Parent element (if it's a form)
|
|
138
|
-
2. Direct children
|
|
139
|
-
|
|
140
|
-
## Built-in Validators
|
|
141
|
-
|
|
142
|
-
Declarative validation rules applied via the `data-validate` attribute. Multiple rules are space-separated.
|
|
143
|
-
|
|
144
|
-
```html
|
|
145
|
-
<input name="age" type="number" data-validate="required range(0-120)" />
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Validation messages use the i18n system. Load the `r-validation` namespace for localized messages:
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
await loadNamespace('r-validation');
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Translation keys in the `r-validation` namespace:
|
|
155
|
-
|
|
156
|
-
| Key | Message (EN) | Message (SV) |
|
|
157
|
-
|-----|-------------|-------------|
|
|
158
|
-
| `required` | `This field is required.` | `Detta fält är obligatoriskt.` |
|
|
159
|
-
| `range` | `Number must be between {min} and {max}, was {actual}.` | `Talet måste vara mellan {min} och {max}, var {actual}.` |
|
|
160
|
-
| `digits` | `Please enter only digits.` | `Ange endast siffror.` |
|
|
161
|
-
|
|
162
|
-
### required
|
|
163
|
-
|
|
164
|
-
Validates that the field has a non-empty value (whitespace-only fails).
|
|
165
|
-
|
|
166
|
-
```html
|
|
167
|
-
<input name="username" data-validate="required" />
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### range(min-max)
|
|
171
|
-
|
|
172
|
-
Validates that a numeric value falls within a range (inclusive). Supports negative numbers and decimals. Empty values are skipped (use with `required` if the field must be filled).
|
|
173
|
-
|
|
174
|
-
```html
|
|
175
|
-
<input name="age" type="number" data-validate="range(0-120)" />
|
|
176
|
-
<input name="temperature" type="number" data-validate="range(-40-50)" />
|
|
177
|
-
<input name="ratio" type="number" data-validate="range(0.0-1.0)" />
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### digits
|
|
181
|
-
|
|
182
|
-
Validates that the value contains only digits (0-9).
|
|
183
|
-
|
|
184
|
-
```html
|
|
185
|
-
<input name="zipCode" data-validate="digits" />
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Custom Validators
|
|
189
|
-
|
|
190
|
-
Register your own validators using the `@RegisterValidator` decorator. The class must implement a `validate(value, context)` method.
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
import { RegisterValidator, ValidationContext } from '
|
|
194
|
-
|
|
195
|
-
@RegisterValidator('email')
|
|
196
|
-
class EmailValidation {
|
|
197
|
-
validate(value: string, context: ValidationContext) {
|
|
198
|
-
if (value && !value.includes('@')) {
|
|
199
|
-
context.addError('Invalid email address');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Restrict a validator to specific input types with the second parameter:
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
@RegisterValidator('positive', ['number'])
|
|
209
|
-
class PositiveValidation {
|
|
210
|
-
validate(value: string, context: ValidationContext) {
|
|
211
|
-
if (value && parseFloat(value) < 0) {
|
|
212
|
-
context.addError('Value must be positive');
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### ValidationContext
|
|
219
|
-
|
|
220
|
-
The context object passed to validators:
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
interface ValidationContext {
|
|
224
|
-
inputType: string; // The HTML input type
|
|
225
|
-
dataType?: string; // The data-type attribute value
|
|
226
|
-
addError(message: string): void; // Report a validation error
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Validator Registry
|
|
231
|
-
|
|
232
|
-
Look up registered validators programmatically:
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
import { getValidator } from '
|
|
236
|
-
|
|
237
|
-
const entry = getValidator('required');
|
|
238
|
-
// entry.validator is the class constructor
|
|
239
|
-
// entry.validInputTypes lists the input types this validator applies to
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## File Upload Validation
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
class FileUploadForm {
|
|
246
|
-
private maxFileSize = 5 * 1024 * 1024; // 5MB
|
|
247
|
-
private allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
|
|
248
|
-
|
|
249
|
-
constructor(form: HTMLFormElement) {
|
|
250
|
-
const validator = new FormValidator(form, {
|
|
251
|
-
useSummary: true,
|
|
252
|
-
customChecks: (form) => this.validateFiles(form, validator),
|
|
253
|
-
submitCallback: () => this.handleSubmit()
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
validateFiles(form: HTMLFormElement, validator: FormValidator) {
|
|
258
|
-
const fileInputs = form.querySelectorAll('input[type="file"]');
|
|
259
|
-
|
|
260
|
-
fileInputs.forEach(input => {
|
|
261
|
-
const files = (input as HTMLInputElement).files;
|
|
262
|
-
if (!files) return;
|
|
263
|
-
|
|
264
|
-
Array.from(files).forEach(file => {
|
|
265
|
-
if (file.size > this.maxFileSize) {
|
|
266
|
-
validator.addErrorToSummary(
|
|
267
|
-
input.name,
|
|
268
|
-
`File "${file.name}" is too large (max 5MB)`
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (!this.allowedTypes.includes(file.type)) {
|
|
273
|
-
validator.addErrorToSummary(
|
|
274
|
-
input.name,
|
|
275
|
-
`File "${file.name}" has invalid type`
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
handleSubmit() {
|
|
283
|
-
const formData = new FormData(form);
|
|
284
|
-
// Upload files...
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
## Robust Error Handling
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
class RobustFormHandler {
|
|
293
|
-
private validator: FormValidator;
|
|
294
|
-
|
|
295
|
-
constructor(private form: HTMLFormElement) {
|
|
296
|
-
this.validator = new FormValidator(form, {
|
|
297
|
-
useSummary: true,
|
|
298
|
-
customChecks: (form) => this.comprehensiveValidation(form),
|
|
299
|
-
submitCallback: () => this.handleSubmissionWithRetry()
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
comprehensiveValidation(form: HTMLFormElement) {
|
|
304
|
-
try {
|
|
305
|
-
this.validateRequiredFields(form);
|
|
306
|
-
this.validateDataFormats(form);
|
|
307
|
-
this.validateBusinessRules(form);
|
|
308
|
-
} catch (error) {
|
|
309
|
-
console.error('Validation error:', error);
|
|
310
|
-
this.validator.addErrorToSummary('System', 'Validation failed. Please try again.');
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
validateDataFormats(form: HTMLFormElement) {
|
|
315
|
-
const emails = form.querySelectorAll('input[type="email"]');
|
|
316
|
-
emails.forEach(input => {
|
|
317
|
-
if (input.value && !this.isValidEmail(input.value)) {
|
|
318
|
-
this.validator.addErrorToSummary(input.name, 'Invalid email format');
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const phones = form.querySelectorAll('input[data-type="phone"]');
|
|
323
|
-
phones.forEach(input => {
|
|
324
|
-
if (input.value && !this.isValidPhone(input.value)) {
|
|
325
|
-
this.validator.addErrorToSummary(input.name, 'Invalid phone number');
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async handleSubmissionWithRetry() {
|
|
331
|
-
const maxRetries = 3;
|
|
332
|
-
let attempt = 0;
|
|
333
|
-
|
|
334
|
-
while (attempt < maxRetries) {
|
|
335
|
-
try {
|
|
336
|
-
const data = readData(this.form);
|
|
337
|
-
await this.submitToAPI(data);
|
|
338
|
-
this.showSuccess();
|
|
339
|
-
return;
|
|
340
|
-
} catch (error) {
|
|
341
|
-
attempt++;
|
|
342
|
-
if (attempt >= maxRetries) {
|
|
343
|
-
this.validator.addErrorToSummary('Submission', 'Failed to submit after multiple attempts');
|
|
344
|
-
} else {
|
|
345
|
-
await this.delay(1000 * attempt);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
```
|
|
1
|
+
# Form Validation
|
|
2
|
+
|
|
3
|
+
The `FormValidator` class provides form validation with HTML5 integration, error summaries, and custom validation support.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { FormValidator } from '@relax.js/core/forms';
|
|
9
|
+
|
|
10
|
+
const form = document.querySelector('form');
|
|
11
|
+
const validator = new FormValidator(form, {
|
|
12
|
+
submitCallback: () => saveData()
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration Options
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
interface ValidatorOptions {
|
|
20
|
+
autoValidate?: boolean; // Validate on every input event
|
|
21
|
+
useSummary?: boolean; // Show errors in summary element
|
|
22
|
+
customChecks?: (form: HTMLFormElement) => void; // Custom validation
|
|
23
|
+
preventDefault?: boolean; // Always prevent form submission
|
|
24
|
+
preventDefaultOnFailed?: boolean; // Prevent submission on failure (default: true)
|
|
25
|
+
submitCallback?: () => void; // Called when form is valid
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Auto-Validation
|
|
30
|
+
|
|
31
|
+
Enable real-time validation as users type:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const validator = new FormValidator(form, {
|
|
35
|
+
autoValidate: true,
|
|
36
|
+
useSummary: true
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Error Summary Display
|
|
41
|
+
|
|
42
|
+
Instead of browser tooltips, show errors in a summary element:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const validator = new FormValidator(form, {
|
|
46
|
+
useSummary: true,
|
|
47
|
+
submitCallback: () => handleSubmit()
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The error summary is automatically created and prepended to the form with:
|
|
52
|
+
- `role="alert"` for accessibility
|
|
53
|
+
- `aria-live="assertive"` for screen readers
|
|
54
|
+
- `class="error-summary"` for styling
|
|
55
|
+
|
|
56
|
+
## Custom Validation
|
|
57
|
+
|
|
58
|
+
Add business logic validation beyond HTML5 constraints:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const validator = new FormValidator(form, {
|
|
62
|
+
useSummary: true,
|
|
63
|
+
customChecks: (form) => {
|
|
64
|
+
const password = form.querySelector('[name="password"]') as HTMLInputElement;
|
|
65
|
+
const confirm = form.querySelector('[name="confirmPassword"]') as HTMLInputElement;
|
|
66
|
+
|
|
67
|
+
if (password.value !== confirm.value) {
|
|
68
|
+
validator.addErrorToSummary('Password Confirmation', 'Passwords do not match');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const startDate = new Date(form.querySelector('[name="startDate"]').value);
|
|
72
|
+
const endDate = new Date(form.querySelector('[name="endDate"]').value);
|
|
73
|
+
|
|
74
|
+
if (endDate <= startDate) {
|
|
75
|
+
validator.addErrorToSummary('End Date', 'Must be after start date');
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
submitCallback: () => saveData()
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Manual Validation
|
|
83
|
+
|
|
84
|
+
Trigger validation programmatically:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
if (validator.validateForm()) {
|
|
88
|
+
// Form is valid
|
|
89
|
+
proceedToNextStep();
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Error Summary Methods
|
|
94
|
+
|
|
95
|
+
### addErrorToSummary
|
|
96
|
+
|
|
97
|
+
Add a single error to the summary:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
validator.addErrorToSummary('Email', 'This email is already registered');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### displayErrorSummary
|
|
104
|
+
|
|
105
|
+
Display multiple errors at once:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
validator.displayErrorSummary([
|
|
109
|
+
'Name: This field is required',
|
|
110
|
+
'Email: Invalid email format',
|
|
111
|
+
'Age: Must be 18 or older'
|
|
112
|
+
]);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### clearErrorSummary
|
|
116
|
+
|
|
117
|
+
Remove all errors from the summary:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
validator.clearErrorSummary();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Finding Forms
|
|
124
|
+
|
|
125
|
+
Use `FindForm` to locate a form relative to a custom element:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
class MyComponent extends HTMLElement {
|
|
129
|
+
connectedCallback() {
|
|
130
|
+
const form = FormValidator.FindForm(this);
|
|
131
|
+
new FormValidator(form);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`FindForm` searches:
|
|
137
|
+
1. Parent element (if it's a form)
|
|
138
|
+
2. Direct children
|
|
139
|
+
|
|
140
|
+
## Built-in Validators
|
|
141
|
+
|
|
142
|
+
Declarative validation rules applied via the `data-validate` attribute. Multiple rules are space-separated.
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<input name="age" type="number" data-validate="required range(0-120)" />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Validation messages use the i18n system. Load the `r-validation` namespace for localized messages:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
await loadNamespace('r-validation');
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Translation keys in the `r-validation` namespace:
|
|
155
|
+
|
|
156
|
+
| Key | Message (EN) | Message (SV) |
|
|
157
|
+
|-----|-------------|-------------|
|
|
158
|
+
| `required` | `This field is required.` | `Detta fält är obligatoriskt.` |
|
|
159
|
+
| `range` | `Number must be between {min} and {max}, was {actual}.` | `Talet måste vara mellan {min} och {max}, var {actual}.` |
|
|
160
|
+
| `digits` | `Please enter only digits.` | `Ange endast siffror.` |
|
|
161
|
+
|
|
162
|
+
### required
|
|
163
|
+
|
|
164
|
+
Validates that the field has a non-empty value (whitespace-only fails).
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<input name="username" data-validate="required" />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### range(min-max)
|
|
171
|
+
|
|
172
|
+
Validates that a numeric value falls within a range (inclusive). Supports negative numbers and decimals. Empty values are skipped (use with `required` if the field must be filled).
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<input name="age" type="number" data-validate="range(0-120)" />
|
|
176
|
+
<input name="temperature" type="number" data-validate="range(-40-50)" />
|
|
177
|
+
<input name="ratio" type="number" data-validate="range(0.0-1.0)" />
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### digits
|
|
181
|
+
|
|
182
|
+
Validates that the value contains only digits (0-9).
|
|
183
|
+
|
|
184
|
+
```html
|
|
185
|
+
<input name="zipCode" data-validate="digits" />
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Custom Validators
|
|
189
|
+
|
|
190
|
+
Register your own validators using the `@RegisterValidator` decorator. The class must implement a `validate(value, context)` method.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { RegisterValidator, ValidationContext } from '@relax.js/core/forms';
|
|
194
|
+
|
|
195
|
+
@RegisterValidator('email')
|
|
196
|
+
class EmailValidation {
|
|
197
|
+
validate(value: string, context: ValidationContext) {
|
|
198
|
+
if (value && !value.includes('@')) {
|
|
199
|
+
context.addError('Invalid email address');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Restrict a validator to specific input types with the second parameter:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
@RegisterValidator('positive', ['number'])
|
|
209
|
+
class PositiveValidation {
|
|
210
|
+
validate(value: string, context: ValidationContext) {
|
|
211
|
+
if (value && parseFloat(value) < 0) {
|
|
212
|
+
context.addError('Value must be positive');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### ValidationContext
|
|
219
|
+
|
|
220
|
+
The context object passed to validators:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
interface ValidationContext {
|
|
224
|
+
inputType: string; // The HTML input type
|
|
225
|
+
dataType?: string; // The data-type attribute value
|
|
226
|
+
addError(message: string): void; // Report a validation error
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Validator Registry
|
|
231
|
+
|
|
232
|
+
Look up registered validators programmatically:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { getValidator } from '@relax.js/core/forms';
|
|
236
|
+
|
|
237
|
+
const entry = getValidator('required');
|
|
238
|
+
// entry.validator is the class constructor
|
|
239
|
+
// entry.validInputTypes lists the input types this validator applies to
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## File Upload Validation
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
class FileUploadForm {
|
|
246
|
+
private maxFileSize = 5 * 1024 * 1024; // 5MB
|
|
247
|
+
private allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
|
|
248
|
+
|
|
249
|
+
constructor(form: HTMLFormElement) {
|
|
250
|
+
const validator = new FormValidator(form, {
|
|
251
|
+
useSummary: true,
|
|
252
|
+
customChecks: (form) => this.validateFiles(form, validator),
|
|
253
|
+
submitCallback: () => this.handleSubmit()
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
validateFiles(form: HTMLFormElement, validator: FormValidator) {
|
|
258
|
+
const fileInputs = form.querySelectorAll('input[type="file"]');
|
|
259
|
+
|
|
260
|
+
fileInputs.forEach(input => {
|
|
261
|
+
const files = (input as HTMLInputElement).files;
|
|
262
|
+
if (!files) return;
|
|
263
|
+
|
|
264
|
+
Array.from(files).forEach(file => {
|
|
265
|
+
if (file.size > this.maxFileSize) {
|
|
266
|
+
validator.addErrorToSummary(
|
|
267
|
+
input.name,
|
|
268
|
+
`File "${file.name}" is too large (max 5MB)`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!this.allowedTypes.includes(file.type)) {
|
|
273
|
+
validator.addErrorToSummary(
|
|
274
|
+
input.name,
|
|
275
|
+
`File "${file.name}" has invalid type`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
handleSubmit() {
|
|
283
|
+
const formData = new FormData(form);
|
|
284
|
+
// Upload files...
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Robust Error Handling
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
class RobustFormHandler {
|
|
293
|
+
private validator: FormValidator;
|
|
294
|
+
|
|
295
|
+
constructor(private form: HTMLFormElement) {
|
|
296
|
+
this.validator = new FormValidator(form, {
|
|
297
|
+
useSummary: true,
|
|
298
|
+
customChecks: (form) => this.comprehensiveValidation(form),
|
|
299
|
+
submitCallback: () => this.handleSubmissionWithRetry()
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
comprehensiveValidation(form: HTMLFormElement) {
|
|
304
|
+
try {
|
|
305
|
+
this.validateRequiredFields(form);
|
|
306
|
+
this.validateDataFormats(form);
|
|
307
|
+
this.validateBusinessRules(form);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error('Validation error:', error);
|
|
310
|
+
this.validator.addErrorToSummary('System', 'Validation failed. Please try again.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
validateDataFormats(form: HTMLFormElement) {
|
|
315
|
+
const emails = form.querySelectorAll('input[type="email"]');
|
|
316
|
+
emails.forEach(input => {
|
|
317
|
+
if (input.value && !this.isValidEmail(input.value)) {
|
|
318
|
+
this.validator.addErrorToSummary(input.name, 'Invalid email format');
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const phones = form.querySelectorAll('input[data-type="phone"]');
|
|
323
|
+
phones.forEach(input => {
|
|
324
|
+
if (input.value && !this.isValidPhone(input.value)) {
|
|
325
|
+
this.validator.addErrorToSummary(input.name, 'Invalid phone number');
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async handleSubmissionWithRetry() {
|
|
331
|
+
const maxRetries = 3;
|
|
332
|
+
let attempt = 0;
|
|
333
|
+
|
|
334
|
+
while (attempt < maxRetries) {
|
|
335
|
+
try {
|
|
336
|
+
const data = readData(this.form);
|
|
337
|
+
await this.submitToAPI(data);
|
|
338
|
+
this.showSuccess();
|
|
339
|
+
return;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
attempt++;
|
|
342
|
+
if (attempt >= maxRetries) {
|
|
343
|
+
this.validator.addErrorToSummary('Submission', 'Failed to submit after multiple attempts');
|
|
344
|
+
} else {
|
|
345
|
+
await this.delay(1000 * attempt);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|