@relax.js/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/DataLoader.d.ts +51 -0
- package/dist/DependencyInjection.d.ts +271 -0
- package/dist/DependencyInjectionOld.d.ts +35 -0
- package/dist/Metadata.d.ts +8 -0
- package/dist/SequentialId.d.ts +47 -0
- package/dist/_alt/src/MustardEngine.d.ts +30 -0
- package/dist/_alt/src/MustardParser.d.ts +63 -0
- package/dist/_alt/src/MustardParser2.d.ts +35 -0
- package/dist/_alt/src/pipes.d.ts +93 -0
- package/dist/_alt/src/template.d.ts +166 -0
- package/dist/_alt/src/tools.d.ts +4 -0
- package/dist/_alt/tests/pipes.tests.d.ts +1 -0
- package/dist/_alt/tests/template.tests.d.ts +1 -0
- package/dist/_alt/vitest.config.d.ts +2 -0
- package/dist/collections/Index.d.ts +1 -0
- package/dist/collections/LinkedList.d.ts +75 -0
- package/dist/collections/Pager.d.ts +15 -0
- package/dist/collections/index.js +2 -0
- package/dist/collections/index.js.map +7 -0
- package/dist/collections/index.mjs +2 -0
- package/dist/collections/index.mjs.map +7 -0
- package/dist/components/Table.d.ts +13 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +128 -0
- package/dist/components/index.js.map +7 -0
- package/dist/components/index.mjs +128 -0
- package/dist/components/index.mjs.map +7 -0
- package/dist/components/lists/Table.d.ts +59 -0
- package/dist/components/lists/TreeView.d.ts +67 -0
- package/dist/components/lists/index.d.ts +2 -0
- package/dist/components/loader.d.ts +60 -0
- package/dist/components/menus/MenuItem.d.ts +30 -0
- package/dist/components/menus/TopMenu.d.ts +16 -0
- package/dist/components/menus/index.d.ts +2 -0
- package/dist/components/panels/tabs.d.ts +15 -0
- package/dist/di/index.d.ts +1 -0
- package/dist/di/index.js +2 -0
- package/dist/di/index.js.map +7 -0
- package/dist/di/index.mjs +2 -0
- package/dist/di/index.mjs.map +7 -0
- package/dist/elements/CopyAttributes.d.ts +2 -0
- package/dist/elements/dom.d.ts +18 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/index.js.map +7 -0
- package/dist/elements/index.mjs +2 -0
- package/dist/elements/index.mjs.map +7 -0
- package/dist/errors.d.ts +71 -0
- package/dist/forms/FormReader.d.ts +182 -0
- package/dist/forms/FormValidator.d.ts +114 -0
- package/dist/forms/ValidationRules.d.ts +103 -0
- package/dist/forms/index.d.ts +4 -0
- package/dist/forms/index.js +2 -0
- package/dist/forms/index.js.map +7 -0
- package/dist/forms/index.mjs +2 -0
- package/dist/forms/index.mjs.map +7 -0
- package/dist/forms/setFormData.d.ts +49 -0
- package/dist/getParentComponent.d.ts +43 -0
- package/dist/html/TableRenderer.d.ts +44 -0
- package/dist/html/TreeBinder.d.ts +9 -0
- package/dist/html/html.d.ts +55 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.js +2 -0
- package/dist/html/index.js.map +7 -0
- package/dist/html/index.mjs +2 -0
- package/dist/html/index.mjs.map +7 -0
- package/dist/html/template.d.ts +167 -0
- package/dist/http/ServerSentEvents.d.ts +116 -0
- package/dist/http/SimpleWebSocket.d.ts +153 -0
- package/dist/http/http.d.ts +177 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +2 -0
- package/dist/http/index.js.map +7 -0
- package/dist/http/index.mjs +2 -0
- package/dist/http/index.mjs.map +7 -0
- package/dist/i18n/i18n.d.ts +105 -0
- package/dist/i18n/icu.d.ts +64 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/index.js.map +7 -0
- package/dist/i18n/index.mjs +2 -0
- package/dist/i18n/index.mjs.map +7 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/index.mjs +5 -0
- package/dist/index.mjs.map +7 -0
- package/dist/lib/DataLoader.d.ts +51 -0
- package/dist/lib/DependencyInjection.d.ts +271 -0
- package/dist/lib/InvokeParent.d.ts +10 -0
- package/dist/lib/Pipes.d.ts +236 -0
- package/dist/lib/SequentialId.d.ts +47 -0
- package/dist/lib/collections/Index.d.ts +1 -0
- package/dist/lib/collections/LinkedList.d.ts +75 -0
- package/dist/lib/collections/Pager.d.ts +15 -0
- package/dist/lib/collections/TableRenderer.d.ts +44 -0
- package/dist/lib/di/index.d.ts +1 -0
- package/dist/lib/elements/CopyAttributes.d.ts +2 -0
- package/dist/lib/elements/dom.d.ts +18 -0
- package/dist/lib/elements/index.d.ts +2 -0
- package/dist/lib/errors.d.ts +71 -0
- package/dist/lib/forms/FormReader.d.ts +182 -0
- package/dist/lib/forms/FormValidator.d.ts +114 -0
- package/dist/lib/forms/ValidationRules.d.ts +103 -0
- package/dist/lib/forms/index.d.ts +4 -0
- package/dist/lib/forms/setFormData.d.ts +49 -0
- package/dist/lib/getParentComponent.d.ts +43 -0
- package/dist/lib/html/TableRenderer.d.ts +44 -0
- package/dist/lib/html/TreeBinder.d.ts +9 -0
- package/dist/lib/html/html.d.ts +55 -0
- package/dist/lib/html/html2.d.ts +55 -0
- package/dist/lib/html/index.d.ts +5 -0
- package/dist/lib/html/m.d.ts +167 -0
- package/dist/lib/html/m2.d.ts +8 -0
- package/dist/lib/html/m3.d.ts +0 -0
- package/dist/lib/html/template.d.ts +167 -0
- package/dist/lib/http/HttpClient.d.ts +153 -0
- package/dist/lib/http/ServerSentEvents.d.ts +116 -0
- package/dist/lib/http/SimpleWebSocket.d.ts +153 -0
- package/dist/lib/http/http.d.ts +177 -0
- package/dist/lib/http/index.d.ts +3 -0
- package/dist/lib/i18n/i18n.d.ts +105 -0
- package/dist/lib/i18n/icu.d.ts +64 -0
- package/dist/lib/i18n/index.d.ts +2 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/routing/NavigateRouteEvent.d.ts +52 -0
- package/dist/lib/routing/RouteLink.d.ts +7 -0
- package/dist/lib/routing/Routing.d.ts +270 -0
- package/dist/lib/routing/RoutingTarget.d.ts +22 -0
- package/dist/lib/routing/index.d.ts +7 -0
- package/dist/lib/routing/navigation.d.ts +70 -0
- package/dist/lib/routing/routeMatching.d.ts +21 -0
- package/dist/lib/routing/routeTargetRegistry.d.ts +23 -0
- package/dist/lib/routing/types.d.ts +130 -0
- package/dist/lib/templates/NodeTemplate.d.ts +38 -0
- package/dist/lib/templates/accessorParser.d.ts +87 -0
- package/dist/lib/templates/parseTemplate.d.ts +6 -0
- package/dist/lib/templates/tokenizer.d.ts +76 -0
- package/dist/lib/tools.d.ts +30 -0
- package/dist/lib/utils/index.d.ts +4 -0
- package/dist/pipes.d.ts +236 -0
- package/dist/routing/NavigateRouteEvent.d.ts +52 -0
- package/dist/routing/RouteLink.d.ts +7 -0
- package/dist/routing/RoutingTarget.d.ts +22 -0
- package/dist/routing/index.d.ts +7 -0
- package/dist/routing/index.js +5 -0
- package/dist/routing/index.js.map +7 -0
- package/dist/routing/index.mjs +5 -0
- package/dist/routing/index.mjs.map +7 -0
- package/dist/routing/navigation.d.ts +70 -0
- package/dist/routing/routeMatching.d.ts +21 -0
- package/dist/routing/routeTargetRegistry.d.ts +23 -0
- package/dist/routing/types.d.ts +130 -0
- package/dist/templates/NodeTemplate.d.ts +38 -0
- package/dist/templates/accessorParser.d.ts +87 -0
- package/dist/templates/parseTemplate.d.ts +6 -0
- package/dist/templates/tokenizer.d.ts +76 -0
- package/dist/tools.d.ts +30 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +7 -0
- package/dist/utils/index.mjs +2 -0
- package/dist/utils/index.mjs.map +7 -0
- package/docs/Architecture.md +333 -0
- package/docs/DependencyInjection.md +237 -0
- package/docs/Errors.md +87 -0
- package/docs/GettingStarted.md +231 -0
- package/docs/Pipes.md +211 -0
- package/docs/Translations.md +312 -0
- package/docs/WhyRelaxjs.md +336 -0
- package/docs/elements/dom.md +102 -0
- package/docs/forms/creating-form-components.md +924 -0
- package/docs/forms/form-api.md +94 -0
- package/docs/forms/forms.md +99 -0
- package/docs/forms/patterns.md +311 -0
- package/docs/forms/reading-writing.md +365 -0
- package/docs/forms/validation.md +351 -0
- package/docs/html/TableRenderer.md +292 -0
- package/docs/html/html.md +175 -0
- package/docs/html/index.md +54 -0
- package/docs/html/template.md +422 -0
- package/docs/http/HttpClient.md +459 -0
- package/docs/http/ServerSentEvents.md +184 -0
- package/docs/http/index.md +109 -0
- package/docs/i18n/i18n.md +309 -0
- package/docs/i18n/intl-standard.md +178 -0
- package/docs/routing/RouteLink.md +98 -0
- package/docs/routing/Routing.md +332 -0
- package/docs/routing/RoutingTarget.md +136 -0
- package/docs/routing/layouts.md +207 -0
- package/docs/utilities.md +143 -0
- package/package.json +93 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Form-Associated Custom Elements API
|
|
2
|
+
|
|
3
|
+
The [Form-Associated Custom Elements](https://html.spec.whatwg.org/multipage/custom-elements.html#form-associated-custom-elements) standard lets web components participate in HTML forms just like native `<input>`, `<select>`, and `<textarea>` elements. RelaxJS form utilities fully support this API. Custom elements work transparently with `readData()`, `setFormData()`, `mapFormToClass()`, and `FormValidator`.
|
|
4
|
+
|
|
5
|
+
## How the Standard Works
|
|
6
|
+
|
|
7
|
+
A custom element becomes form-associated by declaring `static formAssociated = true` and using `ElementInternals`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
class MyInput extends HTMLElement {
|
|
11
|
+
static formAssociated = true;
|
|
12
|
+
private internals: ElementInternals;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.internals = this.attachInternals();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This gives the element:
|
|
22
|
+
|
|
23
|
+
- Inclusion in `form.elements`: the browser tracks it like a native input
|
|
24
|
+
- Participation in `FormData`: submitted values via `internals.setFormValue()`
|
|
25
|
+
- Native validation: constraint checking via `internals.setValidity()`
|
|
26
|
+
- Form lifecycle callbacks: `formResetCallback()`, `formDisabledCallback()`, etc.
|
|
27
|
+
|
|
28
|
+
## How RelaxJS Supports It
|
|
29
|
+
|
|
30
|
+
All form utilities use duck-typing rather than `instanceof` checks. This means they work with any element that exposes the right properties, whether it's a native `<input>` or a custom `<r-input>`.
|
|
31
|
+
|
|
32
|
+
### Property Detection
|
|
33
|
+
|
|
34
|
+
RelaxJS reads element properties via attribute and property checks:
|
|
35
|
+
|
|
36
|
+
| Property | How it's detected |
|
|
37
|
+
|----------|-------------------|
|
|
38
|
+
| `type` | Property or `getAttribute('type')` |
|
|
39
|
+
| `value` | `'value' in element` |
|
|
40
|
+
| `checked` | Property (boolean) or attribute (`""`, `"true"`, `"checked"`) |
|
|
41
|
+
| `disabled` | Property (boolean) or attribute (`""`, `"true"`, `"disabled"`) |
|
|
42
|
+
| `multiple` | Property (boolean) or attribute (`""`, `"true"`, `"multiple"`) |
|
|
43
|
+
| `selectedOptions` | `'selectedOptions' in element` |
|
|
44
|
+
|
|
45
|
+
### readData()
|
|
46
|
+
|
|
47
|
+
Uses `FormData` to read values, which automatically includes form-associated custom elements. Unchecked checkboxes (including custom ones with `type="checkbox"` and a `checked` property) are included as `false`.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const data = readData(form);
|
|
51
|
+
// Custom elements with name/value are included just like native inputs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### setFormData()
|
|
55
|
+
|
|
56
|
+
Sets values by querying `[name]` attributes and using duck-typed property access:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
setFormData(form, { email: 'user@example.com', terms: true });
|
|
60
|
+
// Works on both <input name="email"> and <r-input name="email">
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For custom checkbox-like elements, `setFormData` checks `type === 'checkbox'` and sets the `checked` property. For custom select-like elements, it checks for `selectedOptions` and `multiple`.
|
|
64
|
+
|
|
65
|
+
### mapFormToClass()
|
|
66
|
+
|
|
67
|
+
Queries `input, select, textarea` and uses duck-typed value reading. Disabled elements (detected via attribute, not `instanceof`) are skipped, matching `FormData` behavior.
|
|
68
|
+
|
|
69
|
+
### FormValidator
|
|
70
|
+
|
|
71
|
+
Validates any element returned by `form.querySelectorAll('input,textarea,select')` that implements `checkValidity()`. Custom elements using `ElementInternals.setValidity()` integrate automatically.
|
|
72
|
+
|
|
73
|
+
## Required Properties for Custom Elements
|
|
74
|
+
|
|
75
|
+
For full compatibility with RelaxJS form utilities, custom elements should expose:
|
|
76
|
+
|
|
77
|
+
| Property | Purpose |
|
|
78
|
+
|----------|---------|
|
|
79
|
+
| `name` | Field name for data mapping |
|
|
80
|
+
| `value` | Current value (string) |
|
|
81
|
+
| `disabled` | Whether the field is excluded from form data |
|
|
82
|
+
| `type` | Element type (e.g., `'checkbox'`, `'text'`) for converter selection |
|
|
83
|
+
| `checked` | Boolean state for checkbox/radio-like elements |
|
|
84
|
+
|
|
85
|
+
See [Creating Form Components](creating-form-components.md) for complete implementation examples.
|
|
86
|
+
|
|
87
|
+
## Browser Support
|
|
88
|
+
|
|
89
|
+
Form-associated custom elements are supported in all modern browsers:
|
|
90
|
+
|
|
91
|
+
- Chrome 77+
|
|
92
|
+
- Edge 79+
|
|
93
|
+
- Firefox 93+
|
|
94
|
+
- Safari 16.4+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Form Utilities
|
|
2
|
+
|
|
3
|
+
A TypeScript library for form validation, data mapping, and form manipulation with strong typing support.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The form utilities provide:
|
|
8
|
+
|
|
9
|
+
- **[Validation](validation.md)** - Form validation with HTML5 integration, error summaries, and custom validators
|
|
10
|
+
- **[Reading & Writing](reading-writing.md)** - Read and write form data with automatic type conversion
|
|
11
|
+
- **[Form API](form-api.md)** - How RelaxJS supports the Form-Associated Custom Elements standard
|
|
12
|
+
- **[Patterns](patterns.md)** - Common patterns for multi-step forms, file uploads, and state management
|
|
13
|
+
- **[Creating Form Components](creating-form-components.md)** - Guide for building custom form-associated components
|
|
14
|
+
|
|
15
|
+
## Form Components Support
|
|
16
|
+
|
|
17
|
+
All form utilities fully support **form-associated custom elements** (the HTML Form API). Custom web components created with `ElementInternals` and `formAssociated = true` integrate seamlessly with:
|
|
18
|
+
|
|
19
|
+
- `setFormData()` - Populates custom form components like standard HTML inputs
|
|
20
|
+
- `readData()` - Extracts values from custom form components
|
|
21
|
+
- `mapFormToClass()` - Maps custom form component values to class properties
|
|
22
|
+
- `FormValidator` - Validates custom form components with native and custom validation
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Basic Form Validation
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const form = document.querySelector('form');
|
|
30
|
+
const validator = new FormValidator(form, {
|
|
31
|
+
useSummary: true,
|
|
32
|
+
autoValidate: true
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Reading Form Data
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const form = document.querySelector('form');
|
|
40
|
+
const data = readData(form);
|
|
41
|
+
// Returns typed object with automatic conversion
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Writing Form Data
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const form = document.querySelector('form');
|
|
48
|
+
const userData = {
|
|
49
|
+
name: 'John',
|
|
50
|
+
email: 'john@example.com',
|
|
51
|
+
preferences: {
|
|
52
|
+
notifications: true
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
setFormData(form, userData);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Mapping to Class Instance
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
class UserDTO {
|
|
62
|
+
name: string = '';
|
|
63
|
+
email: string = '';
|
|
64
|
+
age: number = 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const form = document.querySelector('form');
|
|
68
|
+
const user = mapFormToClass(form, new UserDTO());
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Complete Example
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
class UserRegistration {
|
|
75
|
+
name: string = '';
|
|
76
|
+
email: string = '';
|
|
77
|
+
age: number = 0;
|
|
78
|
+
isSubscribed: boolean = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const form = document.querySelector('#registration-form');
|
|
82
|
+
const user = new UserRegistration();
|
|
83
|
+
|
|
84
|
+
const validator = new FormValidator(form, {
|
|
85
|
+
useSummary: true,
|
|
86
|
+
customChecks: (form) => {
|
|
87
|
+
const password = form.querySelector('[name="password"]') as HTMLInputElement;
|
|
88
|
+
const confirm = form.querySelector('[name="confirmPassword"]') as HTMLInputElement;
|
|
89
|
+
|
|
90
|
+
if (password.value !== confirm.value) {
|
|
91
|
+
validator.addErrorToSummary('Password Confirmation', 'Passwords do not match');
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
submitCallback: () => {
|
|
95
|
+
mapFormToClass(form, user);
|
|
96
|
+
console.log('User data:', user);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
```
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Form Patterns
|
|
2
|
+
|
|
3
|
+
Common patterns for building forms with RelaxJS utilities.
|
|
4
|
+
|
|
5
|
+
## Multi-Step Forms
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
class MultiStepForm {
|
|
9
|
+
private currentStep = 1;
|
|
10
|
+
private validators: FormValidator[] = [];
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.setupStepValidation();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setupStepValidation() {
|
|
17
|
+
document.querySelectorAll('.form-step').forEach((step, index) => {
|
|
18
|
+
const validator = new FormValidator(step as HTMLFormElement, {
|
|
19
|
+
useSummary: true,
|
|
20
|
+
preventDefault: true,
|
|
21
|
+
submitCallback: () => this.nextStep()
|
|
22
|
+
});
|
|
23
|
+
this.validators.push(validator);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
nextStep() {
|
|
28
|
+
if (this.validators[this.currentStep - 1].validateForm()) {
|
|
29
|
+
this.currentStep++;
|
|
30
|
+
this.showStep(this.currentStep);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
showStep(step: number) {
|
|
35
|
+
document.querySelectorAll('.form-step').forEach((el, index) => {
|
|
36
|
+
(el as HTMLElement).style.display = index + 1 === step ? 'block' : 'none';
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Form State Management
|
|
43
|
+
|
|
44
|
+
Track changes and auto-save drafts:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
class FormStateManager {
|
|
48
|
+
private formData: Record<string, unknown> = {};
|
|
49
|
+
private isDirty = false;
|
|
50
|
+
|
|
51
|
+
constructor(private form: HTMLFormElement) {
|
|
52
|
+
this.captureInitialState();
|
|
53
|
+
this.setupChangeTracking();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
captureInitialState() {
|
|
57
|
+
this.formData = readData(this.form);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setupChangeTracking() {
|
|
61
|
+
this.form.addEventListener('input', () => {
|
|
62
|
+
this.isDirty = true;
|
|
63
|
+
this.updateSaveButton();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Auto-save every 30 seconds if dirty
|
|
67
|
+
setInterval(() => {
|
|
68
|
+
if (this.isDirty) {
|
|
69
|
+
this.autoSave();
|
|
70
|
+
}
|
|
71
|
+
}, 30000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
autoSave() {
|
|
75
|
+
const currentData = readData(this.form);
|
|
76
|
+
localStorage.setItem('form-draft', JSON.stringify(currentData));
|
|
77
|
+
this.isDirty = false;
|
|
78
|
+
this.showSaveIndicator('Auto-saved');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
restoreDraft() {
|
|
82
|
+
const draft = localStorage.getItem('form-draft');
|
|
83
|
+
if (draft) {
|
|
84
|
+
const data = JSON.parse(draft);
|
|
85
|
+
setFormData(this.form, data);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
hasChanges(): boolean {
|
|
90
|
+
const current = readData(this.form);
|
|
91
|
+
return JSON.stringify(current) !== JSON.stringify(this.formData);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Web Component Integration
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
class UserProfileEditor extends HTMLElement {
|
|
100
|
+
private form: HTMLFormElement;
|
|
101
|
+
private validator: FormValidator;
|
|
102
|
+
private user: UserProfile;
|
|
103
|
+
|
|
104
|
+
connectedCallback() {
|
|
105
|
+
this.innerHTML = `
|
|
106
|
+
<form>
|
|
107
|
+
<input name="displayName" placeholder="Display Name" required>
|
|
108
|
+
<input name="email" type="email" placeholder="Email" required>
|
|
109
|
+
<input name="preferences.notifications" type="checkbox"> Notifications
|
|
110
|
+
<input name="preferences.theme" type="radio" value="light"> Light
|
|
111
|
+
<input name="preferences.theme" type="radio" value="dark"> Dark
|
|
112
|
+
</form>
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
this.form = FormValidator.FindForm(this);
|
|
116
|
+
this.user = new UserProfile();
|
|
117
|
+
|
|
118
|
+
this.validator = new FormValidator(this.form, {
|
|
119
|
+
useSummary: true,
|
|
120
|
+
autoValidate: true,
|
|
121
|
+
submitCallback: () => this.saveProfile()
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.loadUserData();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async loadUserData() {
|
|
128
|
+
const userData = await fetchUserProfile();
|
|
129
|
+
setFormData(this.form, userData);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
saveProfile() {
|
|
133
|
+
mapFormToClass(this.form, this.user);
|
|
134
|
+
|
|
135
|
+
if (this.user.displayName.length < 2) {
|
|
136
|
+
this.validator.addErrorToSummary('Display Name', 'Must be at least 2 characters');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
updateUserProfile(this.user);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
customElements.define('user-profile-editor', UserProfileEditor);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Dynamic Survey Form
|
|
148
|
+
|
|
149
|
+
Handle forms with dynamic question types:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
class SurveyResponse {
|
|
153
|
+
userId: number = 0;
|
|
154
|
+
responses: Record<string, string | number | boolean> = {};
|
|
155
|
+
completedAt: Date = new Date();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const surveyForm = document.querySelector('#survey-form') as HTMLFormElement;
|
|
159
|
+
const response = new SurveyResponse();
|
|
160
|
+
|
|
161
|
+
// Custom data extraction for complex question types
|
|
162
|
+
document.querySelectorAll('[data-question-type="rating"]').forEach(element => {
|
|
163
|
+
(element as any).getData = function() {
|
|
164
|
+
const stars = this.querySelectorAll('.star.selected');
|
|
165
|
+
return stars.length;
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const validator = new FormValidator(surveyForm, {
|
|
170
|
+
autoValidate: true,
|
|
171
|
+
customChecks: (form) => {
|
|
172
|
+
const required = form.querySelectorAll('[data-required]');
|
|
173
|
+
required.forEach(field => {
|
|
174
|
+
const input = field as HTMLInputElement;
|
|
175
|
+
if (!input.value && input.type !== 'checkbox') {
|
|
176
|
+
const questionText = field.getAttribute('data-question');
|
|
177
|
+
validator.addErrorToSummary(questionText, 'This question is required');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
submitCallback: () => {
|
|
182
|
+
response.responses = readData(surveyForm);
|
|
183
|
+
response.completedAt = new Date();
|
|
184
|
+
submitSurvey(response);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Unsaved Changes Warning
|
|
190
|
+
|
|
191
|
+
Warn users before leaving with unsaved changes:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
class UnsavedChangesGuard {
|
|
195
|
+
private initialData: string;
|
|
196
|
+
|
|
197
|
+
constructor(private form: HTMLFormElement) {
|
|
198
|
+
this.initialData = JSON.stringify(readData(form));
|
|
199
|
+
this.setupWarning();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setupWarning() {
|
|
203
|
+
window.addEventListener('beforeunload', (e) => {
|
|
204
|
+
if (this.hasUnsavedChanges()) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
e.returnValue = '';
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
hasUnsavedChanges(): boolean {
|
|
212
|
+
return JSON.stringify(readData(this.form)) !== this.initialData;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
markAsSaved() {
|
|
216
|
+
this.initialData = JSON.stringify(readData(this.form));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Form Reset with Confirmation
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
class ResettableForm {
|
|
225
|
+
private initialData: Record<string, unknown>;
|
|
226
|
+
|
|
227
|
+
constructor(private form: HTMLFormElement) {
|
|
228
|
+
this.initialData = readData(form);
|
|
229
|
+
this.setupResetButton();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setupResetButton() {
|
|
233
|
+
const resetBtn = this.form.querySelector('[type="reset"]');
|
|
234
|
+
resetBtn?.addEventListener('click', (e) => {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
if (confirm('Are you sure you want to reset the form?')) {
|
|
237
|
+
setFormData(this.form, this.initialData);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Conditional Field Visibility
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
class ConditionalForm {
|
|
248
|
+
constructor(private form: HTMLFormElement) {
|
|
249
|
+
this.setupConditionalFields();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
setupConditionalFields() {
|
|
253
|
+
this.form.querySelectorAll('[data-show-when]').forEach(field => {
|
|
254
|
+
const condition = field.getAttribute('data-show-when');
|
|
255
|
+
const [fieldName, expectedValue] = condition.split('=');
|
|
256
|
+
|
|
257
|
+
const triggerField = this.form.querySelector(`[name="${fieldName}"]`);
|
|
258
|
+
triggerField?.addEventListener('change', () => {
|
|
259
|
+
const currentValue = (triggerField as HTMLInputElement).value;
|
|
260
|
+
(field as HTMLElement).style.display =
|
|
261
|
+
currentValue === expectedValue ? 'block' : 'none';
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Usage:
|
|
269
|
+
|
|
270
|
+
```html
|
|
271
|
+
<form>
|
|
272
|
+
<select name="contactMethod">
|
|
273
|
+
<option value="email">Email</option>
|
|
274
|
+
<option value="phone">Phone</option>
|
|
275
|
+
</select>
|
|
276
|
+
|
|
277
|
+
<input name="email" data-show-when="contactMethod=email" placeholder="Email">
|
|
278
|
+
<input name="phone" data-show-when="contactMethod=phone" placeholder="Phone">
|
|
279
|
+
</form>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Form Data Comparison
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
class FormComparison {
|
|
286
|
+
static getDiff(
|
|
287
|
+
original: Record<string, unknown>,
|
|
288
|
+
current: Record<string, unknown>
|
|
289
|
+
): Record<string, { old: unknown; new: unknown }> {
|
|
290
|
+
const diff: Record<string, { old: unknown; new: unknown }> = {};
|
|
291
|
+
|
|
292
|
+
for (const key in current) {
|
|
293
|
+
if (JSON.stringify(original[key]) !== JSON.stringify(current[key])) {
|
|
294
|
+
diff[key] = {
|
|
295
|
+
old: original[key],
|
|
296
|
+
new: current[key]
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return diff;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Usage
|
|
306
|
+
const original = readData(form);
|
|
307
|
+
// ... user makes changes ...
|
|
308
|
+
const current = readData(form);
|
|
309
|
+
const changes = FormComparison.getDiff(original, current);
|
|
310
|
+
console.log('Changed fields:', changes);
|
|
311
|
+
```
|