@relax.js/core 1.0.3 → 1.0.5
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 +45 -27
- package/dist/collections/LinkedList.d.ts +9 -8
- package/dist/collections/index.js +1 -1
- package/dist/collections/index.js.map +3 -3
- package/dist/collections/index.mjs +1 -1
- package/dist/collections/index.mjs.map +3 -3
- 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/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/forms/FormValidator.d.ts +3 -22
- package/dist/forms/ValidationRules.d.ts +4 -6
- 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/forms/setFormData.d.ts +39 -1
- package/dist/html/TableRenderer.d.ts +1 -0
- package/dist/html/index.js +1 -1
- package/dist/html/index.js.map +3 -3
- package/dist/html/index.mjs +1 -1
- package/dist/html/index.mjs.map +3 -3
- package/dist/html/template.d.ts +4 -0
- package/dist/http/ServerSentEvents.d.ts +1 -1
- package/dist/http/SimpleWebSocket.d.ts +1 -1
- package/dist/http/http.d.ts +1 -0
- package/dist/http/index.js +1 -1
- package/dist/http/index.js.map +3 -3
- package/dist/http/index.mjs +1 -1
- package/dist/http/index.mjs.map +3 -3
- package/dist/i18n/icu.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/index.js.map +2 -2
- package/dist/i18n/index.mjs +1 -1
- package/dist/i18n/index.mjs.map +2 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +3 -3
- package/dist/routing/NavigateRouteEvent.d.ts +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/navigation.d.ts +1 -1
- package/dist/routing/routeTargetRegistry.d.ts +1 -0
- package/dist/routing/types.d.ts +2 -1
- package/dist/templates/NodeTemplate.d.ts +3 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +3 -3
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +3 -3
- package/docs/Architecture.md +333 -333
- package/docs/DependencyInjection.md +277 -237
- package/docs/Errors.md +87 -87
- package/docs/GettingStarted.md +238 -231
- package/docs/Pipes.md +5 -5
- package/docs/Translations.md +167 -312
- package/docs/WhyRelaxjs.md +336 -336
- package/docs/api.json +93193 -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 +465 -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/setup/bootstrapping.md +154 -0
- package/docs/setup/build-and-deploy.md +183 -0
- package/docs/setup/project-structure.md +170 -0
- package/docs/setup/vite.md +175 -0
- package/docs/utilities.md +143 -143
- package/package.json +4 -2
package/docs/forms/patterns.md
CHANGED
|
@@ -1,311 +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
|
-
```
|
|
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
|
+
```
|