jsgui3-server 0.0.143 → 0.0.145

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.
Files changed (67) hide show
  1. package/docs/comprehensive-documentation.md +25 -6
  2. package/docs/configuration-reference.md +46 -11
  3. package/docs/controls-development.md +54 -26
  4. package/docs/jsgui3-html-improvement-ideas.md +162 -0
  5. package/docs/jsgui3-html-improvement-ideas.svg +151 -0
  6. package/docs/troubleshooting.md +9 -8
  7. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +19 -14
  8. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +5 -5
  9. package/examples/jsgui3-html/01) mvvm-counter/client.js +648 -0
  10. package/examples/jsgui3-html/01) mvvm-counter/server.js +21 -0
  11. package/examples/jsgui3-html/02) date-transform/client.js +764 -0
  12. package/examples/jsgui3-html/02) date-transform/server.js +21 -0
  13. package/examples/jsgui3-html/03) form-validation/client.js +1045 -0
  14. package/examples/jsgui3-html/03) form-validation/server.js +21 -0
  15. package/examples/jsgui3-html/04) data-grid/client.js +738 -0
  16. package/examples/jsgui3-html/04) data-grid/server.js +21 -0
  17. package/examples/jsgui3-html/05) master-detail/client.js +649 -0
  18. package/examples/jsgui3-html/05) master-detail/server.js +21 -0
  19. package/examples/jsgui3-html/06) theming/client.js +514 -0
  20. package/examples/jsgui3-html/06) theming/server.js +21 -0
  21. package/examples/jsgui3-html/07) mixins/client.js +465 -0
  22. package/examples/jsgui3-html/07) mixins/server.js +21 -0
  23. package/examples/jsgui3-html/08) router/client.js +372 -0
  24. package/examples/jsgui3-html/08) router/server.js +21 -0
  25. package/examples/jsgui3-html/09) resource-transform/client.js +692 -0
  26. package/examples/jsgui3-html/09) resource-transform/server.js +21 -0
  27. package/examples/jsgui3-html/10) binding-debugger/client.js +810 -0
  28. package/examples/jsgui3-html/10) binding-debugger/server.js +21 -0
  29. package/examples/jsgui3-html/README.md +48 -0
  30. package/http/responders/static/Static_Route_HTTP_Responder.js +25 -20
  31. package/lab/README.md +19 -0
  32. package/lab/experiments/window_examples_dom_audit.js +241 -0
  33. package/lab/results/window_examples_dom_audit.json +131 -0
  34. package/lab/results/window_examples_dom_audit.md +46 -0
  35. package/package.json +8 -3
  36. package/publishers/http-webpageorsite-publisher.js +8 -4
  37. package/resources/processors/bundlers/css-bundler.js +28 -173
  38. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +32 -20
  39. package/resources/processors/bundlers/style-bundler.js +288 -0
  40. package/resources/processors/compilers/SASS_Compiler.js +88 -0
  41. package/resources/processors/extractors/js/css_and_js/AST_Node/CSS_And_JS_From_JS_String_Using_AST_Node_Extractor.js +64 -68
  42. package/resources/website-css-resource.js +24 -20
  43. package/resources/website-javascript-resource-processor.js +17 -57
  44. package/resources/website-javascript-resource.js +17 -57
  45. package/serve-factory.js +38 -24
  46. package/server.js +116 -92
  47. package/tests/README.md +38 -3
  48. package/tests/bundlers.test.js +41 -32
  49. package/tests/content-analysis.test.js +19 -18
  50. package/tests/end-to-end.test.js +336 -365
  51. package/tests/error-handling.test.js +13 -11
  52. package/tests/examples-controls.e2e.test.js +13 -1
  53. package/tests/fixtures/end-to-end-client.js +54 -0
  54. package/tests/fixtures/jsgui3-html/binding_debugger_expectations.json +15 -0
  55. package/tests/fixtures/jsgui3-html/counter_expectations.json +31 -0
  56. package/tests/fixtures/jsgui3-html/data_grid_expectations.json +26 -0
  57. package/tests/fixtures/jsgui3-html/date_transform_expectations.json +26 -0
  58. package/tests/fixtures/jsgui3-html/form_validation_expectations.json +27 -0
  59. package/tests/fixtures/jsgui3-html/master_detail_expectations.json +15 -0
  60. package/tests/fixtures/jsgui3-html/mixins_expectations.json +10 -0
  61. package/tests/fixtures/jsgui3-html/resource_transform_expectations.json +11 -0
  62. package/tests/fixtures/jsgui3-html/router_expectations.json +10 -0
  63. package/tests/fixtures/jsgui3-html/theming_expectations.json +10 -0
  64. package/tests/jsgui3-html-examples.puppeteer.test.js +537 -0
  65. package/tests/sass-controls.e2e.test.js +327 -0
  66. package/tests/test-runner.js +4 -1
  67. package/tests/window-examples.puppeteer.test.js +455 -0
@@ -0,0 +1,1045 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
3
+ const { Data_Object } = jsgui;
4
+
5
+ const DEFAULT_FORM_VALUES = Object.freeze({
6
+ full_name: '',
7
+ email: '',
8
+ website: '',
9
+ password: '',
10
+ confirm_password: ''
11
+ });
12
+
13
+ class Form_Validation_Control extends jsgui.Control {
14
+ constructor(spec = {}) {
15
+ spec.__type_name = spec.__type_name || 'form_validation_control';
16
+ super(spec);
17
+
18
+ this.data.model = new Data_Object({
19
+ ...DEFAULT_FORM_VALUES,
20
+ ...(spec.form_values || {})
21
+ });
22
+
23
+ this.view.data.model = new Data_Object({
24
+ full_name_error: '',
25
+ email_error: '',
26
+ website_error: '',
27
+ password_error: '',
28
+ confirm_password_error: '',
29
+ display_name: '',
30
+ email_preview: '',
31
+ summary_text: '',
32
+ status_text: '',
33
+ submit_feedback: '',
34
+ submit_enabled: false
35
+ });
36
+
37
+ this.setup_bindings();
38
+ this.setup_computed();
39
+ this.setup_watchers();
40
+
41
+ if (!spec.el) {
42
+ this.compose();
43
+ }
44
+ }
45
+
46
+ setup_bindings() {
47
+ this.bind({
48
+ full_name: {
49
+ to: 'display_name',
50
+ transform: (value) => this.transforms.string.titleCase(
51
+ this.transforms.string.trim(value)
52
+ )
53
+ },
54
+ email: {
55
+ to: 'email_preview',
56
+ transform: (value) => this.transforms.string.toLowerCase(
57
+ this.transforms.string.trim(value)
58
+ )
59
+ }
60
+ });
61
+ }
62
+
63
+ setup_computed() {
64
+ this.computed(
65
+ this.data.model,
66
+ ['full_name'],
67
+ (full_name) => this.validate_full_name(full_name),
68
+ { propertyName: 'full_name_error', target: this.view.data.model }
69
+ );
70
+
71
+ this.computed(
72
+ this.data.model,
73
+ ['email'],
74
+ (email) => this.validate_email(email),
75
+ { propertyName: 'email_error', target: this.view.data.model }
76
+ );
77
+
78
+ this.computed(
79
+ this.data.model,
80
+ ['website'],
81
+ (website) => this.validate_website(website),
82
+ { propertyName: 'website_error', target: this.view.data.model }
83
+ );
84
+
85
+ this.computed(
86
+ this.data.model,
87
+ ['password'],
88
+ (password) => this.validate_password(password),
89
+ { propertyName: 'password_error', target: this.view.data.model }
90
+ );
91
+
92
+ this.computed(
93
+ this.data.model,
94
+ ['password', 'confirm_password'],
95
+ (password, confirm_password) => this.validate_confirm_password(password, confirm_password),
96
+ { propertyName: 'confirm_password_error', target: this.view.data.model }
97
+ );
98
+
99
+ this.computed(
100
+ this.data.model,
101
+ ['full_name', 'email', 'website', 'password', 'confirm_password'],
102
+ () => this.is_form_valid(),
103
+ { propertyName: 'submit_enabled', target: this.view.data.model }
104
+ );
105
+
106
+ this.computed(
107
+ this.view.data.model,
108
+ ['display_name', 'email_preview'],
109
+ (display_name, email_preview) => {
110
+ const name_text = display_name || 'Unnamed';
111
+ const email_text = email_preview || 'no email';
112
+ return `Profile: ${name_text} (${email_text})`;
113
+ },
114
+ { propertyName: 'summary_text', target: this.view.data.model }
115
+ );
116
+
117
+ this.computed(
118
+ this.view.data.model,
119
+ ['submit_enabled'],
120
+ (submit_enabled) => (submit_enabled ? 'Ready to submit.' : 'Fix validation errors.'),
121
+ { propertyName: 'status_text', target: this.view.data.model }
122
+ );
123
+ }
124
+
125
+ setup_watchers() {
126
+ this.watch(
127
+ this.view.data.model,
128
+ ['full_name_error', 'email_error', 'website_error', 'password_error', 'confirm_password_error'],
129
+ () => {
130
+ if (this.view.data.model.submit_feedback) {
131
+ this.view.data.model.submit_feedback = '';
132
+ }
133
+ }
134
+ );
135
+ }
136
+
137
+ validate_full_name(full_name) {
138
+ const trimmed_value = this.transforms.string.trim(full_name);
139
+ if (!this.validators.required(trimmed_value)) {
140
+ return 'Full name is required.';
141
+ }
142
+ if (!this.validators.length(trimmed_value, 3, 40)) {
143
+ return 'Full name must be 3-40 characters.';
144
+ }
145
+ if (!this.validators.pattern(trimmed_value, /^[A-Za-z][A-Za-z\s\-']*$/)) {
146
+ return 'Name can only include letters, spaces, apostrophes, and dashes.';
147
+ }
148
+ return '';
149
+ }
150
+
151
+ validate_email(email) {
152
+ const trimmed_value = this.transforms.string.trim(email);
153
+ if (!this.validators.required(trimmed_value)) {
154
+ return 'Email is required.';
155
+ }
156
+ if (!this.validators.email(trimmed_value)) {
157
+ return 'Email must be valid.';
158
+ }
159
+ return '';
160
+ }
161
+
162
+ validate_website(website) {
163
+ const trimmed_value = this.transforms.string.trim(website);
164
+ if (!trimmed_value) {
165
+ return '';
166
+ }
167
+ if (!this.validators.url(trimmed_value)) {
168
+ return 'Website must be a valid URL.';
169
+ }
170
+ return '';
171
+ }
172
+
173
+ validate_password(password) {
174
+ const raw_value = password || '';
175
+ if (!this.validators.required(raw_value)) {
176
+ return 'Password is required.';
177
+ }
178
+ if (!this.validators.length(raw_value, 8, 64)) {
179
+ return 'Password must be 8-64 characters.';
180
+ }
181
+ if (!this.validators.pattern(raw_value, /^(?=.*[A-Za-z])(?=.*\d).+$/)) {
182
+ return 'Password must include a letter and a number.';
183
+ }
184
+ return '';
185
+ }
186
+
187
+ validate_confirm_password(password, confirm_password) {
188
+ const raw_value = confirm_password || '';
189
+ if (!this.validators.required(raw_value)) {
190
+ return 'Confirm your password.';
191
+ }
192
+ if (password !== confirm_password) {
193
+ return 'Passwords do not match.';
194
+ }
195
+ return '';
196
+ }
197
+
198
+ is_form_valid() {
199
+ const full_name_error = this.validate_full_name(this.data.model.full_name);
200
+ const email_error = this.validate_email(this.data.model.email);
201
+ const website_error = this.validate_website(this.data.model.website);
202
+ const password_error = this.validate_password(this.data.model.password);
203
+ const confirm_password_error = this.validate_confirm_password(
204
+ this.data.model.password,
205
+ this.data.model.confirm_password
206
+ );
207
+
208
+ return [
209
+ full_name_error,
210
+ email_error,
211
+ website_error,
212
+ password_error,
213
+ confirm_password_error
214
+ ].every((error_value) => !error_value);
215
+ }
216
+
217
+ update_form_value(field_name, raw_value) {
218
+ const normalized_value = raw_value === null || raw_value === undefined
219
+ ? ''
220
+ : String(raw_value);
221
+ this.data.model[field_name] = normalized_value;
222
+ }
223
+
224
+ activate() {
225
+ if (this.__active) return;
226
+ super.activate();
227
+
228
+ if (this._dom_bound) return;
229
+ const root_el = this.dom.el;
230
+ if (!root_el) return;
231
+ this._dom_bound = true;
232
+
233
+ const full_name_input_el = root_el.querySelector('[data-test="full-name-input"]');
234
+ const email_input_el = root_el.querySelector('[data-test="email-input"]');
235
+ const website_input_el = root_el.querySelector('[data-test="website-input"]');
236
+ const password_input_el = root_el.querySelector('[data-test="password-input"]');
237
+ const confirm_password_input_el = root_el.querySelector('[data-test="confirm-password-input"]');
238
+ const submit_button_el = root_el.querySelector('[data-test="submit-button"]');
239
+ const summary_text_el = root_el.querySelector('[data-test="summary-text"]');
240
+ const status_text_el = root_el.querySelector('[data-test="status-text"]');
241
+ const feedback_text_el = root_el.querySelector('[data-test="feedback-text"]');
242
+ const full_name_error_el = root_el.querySelector('[data-test="full-name-error"]');
243
+ const email_error_el = root_el.querySelector('[data-test="email-error"]');
244
+ const website_error_el = root_el.querySelector('[data-test="website-error"]');
245
+ const password_error_el = root_el.querySelector('[data-test="password-error"]');
246
+ const confirm_password_error_el = root_el.querySelector('[data-test="confirm-password-error"]');
247
+
248
+ const set_error_state = (input_el, has_error) => {
249
+ if (!input_el) return;
250
+ const row_el = input_el.closest('.form-row');
251
+ if (!row_el) return;
252
+ row_el.classList.toggle('has-error', Boolean(has_error));
253
+ };
254
+
255
+ if (full_name_input_el) {
256
+ full_name_input_el.addEventListener('input', (event) => {
257
+ const raw_value = event && event.target ? event.target.value : '';
258
+ this.update_form_value('full_name', raw_value);
259
+ });
260
+ }
261
+
262
+ if (email_input_el) {
263
+ email_input_el.addEventListener('input', (event) => {
264
+ const raw_value = event && event.target ? event.target.value : '';
265
+ this.update_form_value('email', raw_value);
266
+ });
267
+ }
268
+
269
+ if (website_input_el) {
270
+ website_input_el.addEventListener('input', (event) => {
271
+ const raw_value = event && event.target ? event.target.value : '';
272
+ this.update_form_value('website', raw_value);
273
+ });
274
+ }
275
+
276
+ if (password_input_el) {
277
+ password_input_el.addEventListener('input', (event) => {
278
+ const raw_value = event && event.target ? event.target.value : '';
279
+ this.update_form_value('password', raw_value);
280
+ });
281
+ }
282
+
283
+ if (confirm_password_input_el) {
284
+ confirm_password_input_el.addEventListener('input', (event) => {
285
+ const raw_value = event && event.target ? event.target.value : '';
286
+ this.update_form_value('confirm_password', raw_value);
287
+ });
288
+ }
289
+
290
+ if (submit_button_el) {
291
+ submit_button_el.addEventListener('click', () => {
292
+ if (!this.view.data.model.submit_enabled) {
293
+ this.view.data.model.submit_feedback = 'Please resolve the errors before submitting.';
294
+ return;
295
+ }
296
+ this.view.data.model.submit_feedback = 'Submitted profile details.';
297
+ });
298
+ }
299
+
300
+ this.watch(
301
+ this.data.model,
302
+ 'full_name',
303
+ (value) => {
304
+ if (full_name_input_el) {
305
+ full_name_input_el.value = value || '';
306
+ }
307
+ },
308
+ { immediate: true }
309
+ );
310
+
311
+ this.watch(
312
+ this.data.model,
313
+ 'email',
314
+ (value) => {
315
+ if (email_input_el) {
316
+ email_input_el.value = value || '';
317
+ }
318
+ },
319
+ { immediate: true }
320
+ );
321
+
322
+ this.watch(
323
+ this.data.model,
324
+ 'website',
325
+ (value) => {
326
+ if (website_input_el) {
327
+ website_input_el.value = value || '';
328
+ }
329
+ },
330
+ { immediate: true }
331
+ );
332
+
333
+ this.watch(
334
+ this.data.model,
335
+ 'password',
336
+ (value) => {
337
+ if (password_input_el) {
338
+ password_input_el.value = value || '';
339
+ }
340
+ },
341
+ { immediate: true }
342
+ );
343
+
344
+ this.watch(
345
+ this.data.model,
346
+ 'confirm_password',
347
+ (value) => {
348
+ if (confirm_password_input_el) {
349
+ confirm_password_input_el.value = value || '';
350
+ }
351
+ },
352
+ { immediate: true }
353
+ );
354
+
355
+ this.watch(
356
+ this.view.data.model,
357
+ 'full_name_error',
358
+ (error_text) => {
359
+ if (full_name_error_el) {
360
+ full_name_error_el.textContent = error_text || '';
361
+ }
362
+ set_error_state(full_name_input_el, Boolean(error_text));
363
+ },
364
+ { immediate: true }
365
+ );
366
+
367
+ this.watch(
368
+ this.view.data.model,
369
+ 'email_error',
370
+ (error_text) => {
371
+ if (email_error_el) {
372
+ email_error_el.textContent = error_text || '';
373
+ }
374
+ set_error_state(email_input_el, Boolean(error_text));
375
+ },
376
+ { immediate: true }
377
+ );
378
+
379
+ this.watch(
380
+ this.view.data.model,
381
+ 'website_error',
382
+ (error_text) => {
383
+ if (website_error_el) {
384
+ website_error_el.textContent = error_text || '';
385
+ }
386
+ set_error_state(website_input_el, Boolean(error_text));
387
+ },
388
+ { immediate: true }
389
+ );
390
+
391
+ this.watch(
392
+ this.view.data.model,
393
+ 'password_error',
394
+ (error_text) => {
395
+ if (password_error_el) {
396
+ password_error_el.textContent = error_text || '';
397
+ }
398
+ set_error_state(password_input_el, Boolean(error_text));
399
+ },
400
+ { immediate: true }
401
+ );
402
+
403
+ this.watch(
404
+ this.view.data.model,
405
+ 'confirm_password_error',
406
+ (error_text) => {
407
+ if (confirm_password_error_el) {
408
+ confirm_password_error_el.textContent = error_text || '';
409
+ }
410
+ set_error_state(confirm_password_input_el, Boolean(error_text));
411
+ },
412
+ { immediate: true }
413
+ );
414
+
415
+ this.watch(
416
+ this.view.data.model,
417
+ 'summary_text',
418
+ (value) => {
419
+ if (summary_text_el) {
420
+ summary_text_el.textContent = value || '';
421
+ }
422
+ },
423
+ { immediate: true }
424
+ );
425
+
426
+ this.watch(
427
+ this.view.data.model,
428
+ 'status_text',
429
+ (value) => {
430
+ if (status_text_el) {
431
+ status_text_el.textContent = value || '';
432
+ }
433
+ },
434
+ { immediate: true }
435
+ );
436
+
437
+ this.watch(
438
+ this.view.data.model,
439
+ 'submit_feedback',
440
+ (value) => {
441
+ if (feedback_text_el) {
442
+ feedback_text_el.textContent = value || '';
443
+ }
444
+ },
445
+ { immediate: true }
446
+ );
447
+
448
+ this.watch(
449
+ this.view.data.model,
450
+ 'submit_enabled',
451
+ (submit_enabled) => {
452
+ if (!submit_button_el) return;
453
+ submit_button_el.classList.toggle('is-disabled', !submit_enabled);
454
+ submit_button_el.setAttribute('aria-disabled', submit_enabled ? 'false' : 'true');
455
+ },
456
+ { immediate: true }
457
+ );
458
+ }
459
+
460
+ compose() {
461
+ // Framework expects the method name `compose`.
462
+ const page_context = this.context;
463
+
464
+ this.add_class('form-validation-control');
465
+ this.dom.attributes['data-test'] = 'form-validation-control';
466
+
467
+ const card = new jsgui.Control({
468
+ context: page_context,
469
+ tagName: 'div',
470
+ class: 'form-card'
471
+ });
472
+
473
+ const title = new jsgui.Control({
474
+ context: page_context,
475
+ tagName: 'h1',
476
+ class: 'form-title',
477
+ content: 'Registration Validation'
478
+ });
479
+
480
+ const subtitle = new jsgui.Control({
481
+ context: page_context,
482
+ tagName: 'p',
483
+ class: 'form-subtitle',
484
+ content: 'Each field validates using jsgui3-html validators and transformations.'
485
+ });
486
+
487
+ const form_grid = new jsgui.Control({
488
+ context: page_context,
489
+ tagName: 'div',
490
+ class: 'form-grid'
491
+ });
492
+
493
+ const create_field_row = (label_text, input_control, error_control) => {
494
+ const row = new jsgui.Control({
495
+ context: page_context,
496
+ tagName: 'div',
497
+ class: 'form-row'
498
+ });
499
+
500
+ const label = new jsgui.Control({
501
+ context: page_context,
502
+ tagName: 'label',
503
+ class: 'form-label',
504
+ content: label_text
505
+ });
506
+
507
+ row.add(label);
508
+ row.add(input_control);
509
+ row.add(error_control);
510
+ return { row, label };
511
+ };
512
+
513
+ const full_name_input = new jsgui.Control({
514
+ context: page_context,
515
+ tagName: 'input',
516
+ class: 'form-input'
517
+ });
518
+ full_name_input.dom.attributes.type = 'text';
519
+ full_name_input.dom.attributes.placeholder = 'Ada Lovelace';
520
+ full_name_input.dom.attributes['data-test'] = 'full-name-input';
521
+
522
+ const full_name_error = new jsgui.Control({
523
+ context: page_context,
524
+ tagName: 'div',
525
+ class: 'form-error'
526
+ });
527
+ full_name_error.dom.attributes['data-test'] = 'full-name-error';
528
+
529
+ const full_name_row = create_field_row('Full name', full_name_input, full_name_error);
530
+
531
+ const email_input = new jsgui.Control({
532
+ context: page_context,
533
+ tagName: 'input',
534
+ class: 'form-input'
535
+ });
536
+ email_input.dom.attributes.type = 'email';
537
+ email_input.dom.attributes.placeholder = 'name@company.com';
538
+ email_input.dom.attributes['data-test'] = 'email-input';
539
+
540
+ const email_error = new jsgui.Control({
541
+ context: page_context,
542
+ tagName: 'div',
543
+ class: 'form-error'
544
+ });
545
+ email_error.dom.attributes['data-test'] = 'email-error';
546
+
547
+ const email_row = create_field_row('Email', email_input, email_error);
548
+
549
+ const website_input = new jsgui.Control({
550
+ context: page_context,
551
+ tagName: 'input',
552
+ class: 'form-input'
553
+ });
554
+ website_input.dom.attributes.type = 'url';
555
+ website_input.dom.attributes.placeholder = 'https://example.com';
556
+ website_input.dom.attributes['data-test'] = 'website-input';
557
+
558
+ const website_error = new jsgui.Control({
559
+ context: page_context,
560
+ tagName: 'div',
561
+ class: 'form-error'
562
+ });
563
+ website_error.dom.attributes['data-test'] = 'website-error';
564
+
565
+ const website_row = create_field_row('Website', website_input, website_error);
566
+
567
+ const password_input = new jsgui.Control({
568
+ context: page_context,
569
+ tagName: 'input',
570
+ class: 'form-input'
571
+ });
572
+ password_input.dom.attributes.type = 'password';
573
+ password_input.dom.attributes.placeholder = 'Minimum 8 characters';
574
+ password_input.dom.attributes['data-test'] = 'password-input';
575
+
576
+ const password_error = new jsgui.Control({
577
+ context: page_context,
578
+ tagName: 'div',
579
+ class: 'form-error'
580
+ });
581
+ password_error.dom.attributes['data-test'] = 'password-error';
582
+
583
+ const password_row = create_field_row('Password', password_input, password_error);
584
+
585
+ const confirm_password_input = new jsgui.Control({
586
+ context: page_context,
587
+ tagName: 'input',
588
+ class: 'form-input'
589
+ });
590
+ confirm_password_input.dom.attributes.type = 'password';
591
+ confirm_password_input.dom.attributes.placeholder = 'Re-enter password';
592
+ confirm_password_input.dom.attributes['data-test'] = 'confirm-password-input';
593
+
594
+ const confirm_password_error = new jsgui.Control({
595
+ context: page_context,
596
+ tagName: 'div',
597
+ class: 'form-error'
598
+ });
599
+ confirm_password_error.dom.attributes['data-test'] = 'confirm-password-error';
600
+
601
+ const confirm_password_row = create_field_row('Confirm password', confirm_password_input, confirm_password_error);
602
+
603
+ form_grid.add(full_name_row.row);
604
+ form_grid.add(email_row.row);
605
+ form_grid.add(website_row.row);
606
+ form_grid.add(password_row.row);
607
+ form_grid.add(confirm_password_row.row);
608
+
609
+ const summary_panel = new jsgui.Control({
610
+ context: page_context,
611
+ tagName: 'div',
612
+ class: 'form-summary'
613
+ });
614
+
615
+ const summary_title = new jsgui.Control({
616
+ context: page_context,
617
+ tagName: 'div',
618
+ class: 'form-summary-title',
619
+ content: 'Preview'
620
+ });
621
+
622
+ const summary_text = new jsgui.Control({
623
+ context: page_context,
624
+ tagName: 'div',
625
+ class: 'form-summary-text'
626
+ });
627
+ summary_text.dom.attributes['data-test'] = 'summary-text';
628
+
629
+ const status_text = new jsgui.Control({
630
+ context: page_context,
631
+ tagName: 'div',
632
+ class: 'form-status'
633
+ });
634
+ status_text.dom.attributes['data-test'] = 'status-text';
635
+
636
+ const submit_feedback = new jsgui.Control({
637
+ context: page_context,
638
+ tagName: 'div',
639
+ class: 'form-feedback'
640
+ });
641
+ submit_feedback.dom.attributes['data-test'] = 'feedback-text';
642
+
643
+ summary_panel.add(summary_title);
644
+ summary_panel.add(summary_text);
645
+ summary_panel.add(status_text);
646
+ summary_panel.add(submit_feedback);
647
+
648
+ const submit_button = new jsgui.Control({
649
+ context: page_context,
650
+ tagName: 'button',
651
+ class: 'form-button',
652
+ content: 'Submit'
653
+ });
654
+ submit_button.dom.attributes['data-test'] = 'submit-button';
655
+
656
+ card.add(title);
657
+ card.add(subtitle);
658
+ card.add(form_grid);
659
+ card.add(summary_panel);
660
+ card.add(submit_button);
661
+
662
+ this.add(card);
663
+
664
+ full_name_input.on('input', (event) => {
665
+ const raw_value = event && event.target ? event.target.value : '';
666
+ this.update_form_value('full_name', raw_value);
667
+ });
668
+
669
+ email_input.on('input', (event) => {
670
+ const raw_value = event && event.target ? event.target.value : '';
671
+ this.update_form_value('email', raw_value);
672
+ });
673
+
674
+ website_input.on('input', (event) => {
675
+ const raw_value = event && event.target ? event.target.value : '';
676
+ this.update_form_value('website', raw_value);
677
+ });
678
+
679
+ password_input.on('input', (event) => {
680
+ const raw_value = event && event.target ? event.target.value : '';
681
+ this.update_form_value('password', raw_value);
682
+ });
683
+
684
+ confirm_password_input.on('input', (event) => {
685
+ const raw_value = event && event.target ? event.target.value : '';
686
+ this.update_form_value('confirm_password', raw_value);
687
+ });
688
+
689
+ submit_button.on('click', () => {
690
+ if (!this.view.data.model.submit_enabled) {
691
+ this.view.data.model.submit_feedback = 'Please resolve the errors before submitting.';
692
+ return;
693
+ }
694
+ this.view.data.model.submit_feedback = 'Submitted profile details.';
695
+ });
696
+
697
+ const set_error_state = (row_control, has_error) => {
698
+ if (has_error) {
699
+ row_control.add_class('has-error');
700
+ } else {
701
+ row_control.remove_class('has-error');
702
+ }
703
+ };
704
+
705
+ const set_button_state = (enabled) => {
706
+ if (enabled) {
707
+ submit_button.remove_class('is-disabled');
708
+ } else {
709
+ submit_button.add_class('is-disabled');
710
+ }
711
+ submit_button.dom.attributes['aria-disabled'] = enabled ? 'false' : 'true';
712
+ };
713
+
714
+ this.watch(
715
+ this.data.model,
716
+ 'full_name',
717
+ (value) => {
718
+ full_name_input.dom.attributes.value = value || '';
719
+ },
720
+ { immediate: true }
721
+ );
722
+
723
+ this.watch(
724
+ this.data.model,
725
+ 'email',
726
+ (value) => {
727
+ email_input.dom.attributes.value = value || '';
728
+ },
729
+ { immediate: true }
730
+ );
731
+
732
+ this.watch(
733
+ this.data.model,
734
+ 'website',
735
+ (value) => {
736
+ website_input.dom.attributes.value = value || '';
737
+ },
738
+ { immediate: true }
739
+ );
740
+
741
+ this.watch(
742
+ this.data.model,
743
+ 'password',
744
+ (value) => {
745
+ password_input.dom.attributes.value = value || '';
746
+ },
747
+ { immediate: true }
748
+ );
749
+
750
+ this.watch(
751
+ this.data.model,
752
+ 'confirm_password',
753
+ (value) => {
754
+ confirm_password_input.dom.attributes.value = value || '';
755
+ },
756
+ { immediate: true }
757
+ );
758
+
759
+ this.watch(
760
+ this.view.data.model,
761
+ 'full_name_error',
762
+ (error_text) => {
763
+ full_name_error.clear();
764
+ if (error_text) {
765
+ full_name_error.add(error_text);
766
+ }
767
+ set_error_state(full_name_row.row, Boolean(error_text));
768
+ },
769
+ { immediate: true }
770
+ );
771
+
772
+ this.watch(
773
+ this.view.data.model,
774
+ 'email_error',
775
+ (error_text) => {
776
+ email_error.clear();
777
+ if (error_text) {
778
+ email_error.add(error_text);
779
+ }
780
+ set_error_state(email_row.row, Boolean(error_text));
781
+ },
782
+ { immediate: true }
783
+ );
784
+
785
+ this.watch(
786
+ this.view.data.model,
787
+ 'website_error',
788
+ (error_text) => {
789
+ website_error.clear();
790
+ if (error_text) {
791
+ website_error.add(error_text);
792
+ }
793
+ set_error_state(website_row.row, Boolean(error_text));
794
+ },
795
+ { immediate: true }
796
+ );
797
+
798
+ this.watch(
799
+ this.view.data.model,
800
+ 'password_error',
801
+ (error_text) => {
802
+ password_error.clear();
803
+ if (error_text) {
804
+ password_error.add(error_text);
805
+ }
806
+ set_error_state(password_row.row, Boolean(error_text));
807
+ },
808
+ { immediate: true }
809
+ );
810
+
811
+ this.watch(
812
+ this.view.data.model,
813
+ 'confirm_password_error',
814
+ (error_text) => {
815
+ confirm_password_error.clear();
816
+ if (error_text) {
817
+ confirm_password_error.add(error_text);
818
+ }
819
+ set_error_state(confirm_password_row.row, Boolean(error_text));
820
+ },
821
+ { immediate: true }
822
+ );
823
+
824
+ this.watch(
825
+ this.view.data.model,
826
+ 'summary_text',
827
+ (value) => {
828
+ summary_text.clear();
829
+ summary_text.add(value || '');
830
+ },
831
+ { immediate: true }
832
+ );
833
+
834
+ this.watch(
835
+ this.view.data.model,
836
+ 'status_text',
837
+ (value) => {
838
+ status_text.clear();
839
+ status_text.add(value || '');
840
+ },
841
+ { immediate: true }
842
+ );
843
+
844
+ this.watch(
845
+ this.view.data.model,
846
+ 'submit_feedback',
847
+ (value) => {
848
+ submit_feedback.clear();
849
+ if (value) {
850
+ submit_feedback.add(value);
851
+ }
852
+ },
853
+ { immediate: true }
854
+ );
855
+
856
+ this.watch(
857
+ this.view.data.model,
858
+ 'submit_enabled',
859
+ (submit_enabled) => {
860
+ set_button_state(Boolean(submit_enabled));
861
+ },
862
+ { immediate: true }
863
+ );
864
+ }
865
+ }
866
+
867
+ class Demo_UI extends Active_HTML_Document {
868
+ constructor(spec = {}) {
869
+ spec.__type_name = spec.__type_name || 'form_validation_demo_ui';
870
+ super(spec);
871
+
872
+ if (!spec.el) {
873
+ this.compose();
874
+ }
875
+ }
876
+
877
+ compose() {
878
+ // Framework expects the method name `compose`.
879
+ const page_context = this.context;
880
+ this.body.add_class('form-validation-demo');
881
+
882
+ const form_control = new Form_Validation_Control({
883
+ context: page_context
884
+ });
885
+
886
+ this.body.add(form_control);
887
+ }
888
+ }
889
+
890
+ Demo_UI.css = `
891
+ * {
892
+ box-sizing: border-box;
893
+ }
894
+
895
+ body {
896
+ margin: 0;
897
+ padding: 0;
898
+ font-family: "DM Serif Text", "Georgia", serif;
899
+ background: radial-gradient(circle at top, #fdf6ec 0%, #edf2f8 60%, #e2e8f1 100%);
900
+ color: #1b2330;
901
+ }
902
+
903
+ .form-validation-demo {
904
+ min-height: 100vh;
905
+ display: flex;
906
+ align-items: center;
907
+ justify-content: center;
908
+ padding: 32px;
909
+ }
910
+
911
+ .form-validation-control {
912
+ width: 100%;
913
+ max-width: 720px;
914
+ }
915
+
916
+ .form-card {
917
+ background: #ffffff;
918
+ border-radius: 22px;
919
+ padding: 36px;
920
+ border: 1px solid #e3d6c5;
921
+ box-shadow: 0 28px 55px rgba(22, 28, 41, 0.14);
922
+ display: grid;
923
+ gap: 18px;
924
+ }
925
+
926
+ .form-title {
927
+ margin: 0;
928
+ font-size: 26px;
929
+ }
930
+
931
+ .form-subtitle {
932
+ margin: 0;
933
+ color: #4e5869;
934
+ font-size: 14px;
935
+ }
936
+
937
+ .form-grid {
938
+ display: grid;
939
+ gap: 14px;
940
+ }
941
+
942
+ .form-row {
943
+ display: grid;
944
+ gap: 8px;
945
+ }
946
+
947
+ .form-row.has-error .form-input {
948
+ border-color: #c4372c;
949
+ background: #fff1f0;
950
+ }
951
+
952
+ .form-label {
953
+ font-size: 12px;
954
+ text-transform: uppercase;
955
+ letter-spacing: 0.12em;
956
+ color: #6a5f53;
957
+ }
958
+
959
+ .form-input {
960
+ padding: 12px 14px;
961
+ border-radius: 12px;
962
+ border: 1px solid #c5cad3;
963
+ font-size: 15px;
964
+ background: #fbfcfe;
965
+ }
966
+
967
+ .form-error {
968
+ min-height: 16px;
969
+ font-size: 12px;
970
+ color: #b13b30;
971
+ }
972
+
973
+ .form-summary {
974
+ border-radius: 16px;
975
+ padding: 16px 18px;
976
+ background: #f4f6fb;
977
+ border: 1px solid #dee4ee;
978
+ display: grid;
979
+ gap: 6px;
980
+ }
981
+
982
+ .form-summary-title {
983
+ font-size: 12px;
984
+ text-transform: uppercase;
985
+ letter-spacing: 0.12em;
986
+ color: #5e6777;
987
+ }
988
+
989
+ .form-summary-text {
990
+ font-size: 16px;
991
+ color: #1f2a38;
992
+ }
993
+
994
+ .form-status {
995
+ font-size: 13px;
996
+ color: #3f4a5a;
997
+ }
998
+
999
+ .form-feedback {
1000
+ font-size: 13px;
1001
+ color: #1c6f5a;
1002
+ min-height: 16px;
1003
+ }
1004
+
1005
+ .form-button {
1006
+ justify-self: start;
1007
+ padding: 12px 18px;
1008
+ border-radius: 999px;
1009
+ border: none;
1010
+ background: #1f2a38;
1011
+ color: #fdfbf8;
1012
+ font-size: 14px;
1013
+ cursor: pointer;
1014
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
1015
+ }
1016
+
1017
+ .form-button:hover {
1018
+ transform: translateY(-1px);
1019
+ box-shadow: 0 10px 20px rgba(31, 42, 56, 0.2);
1020
+ }
1021
+
1022
+ .form-button.is-disabled {
1023
+ background: #8b95a5;
1024
+ cursor: not-allowed;
1025
+ transform: none;
1026
+ box-shadow: none;
1027
+ }
1028
+
1029
+ @media (max-width: 640px) {
1030
+ .form-card {
1031
+ padding: 28px;
1032
+ }
1033
+
1034
+ .form-button {
1035
+ width: 100%;
1036
+ text-align: center;
1037
+ }
1038
+ }
1039
+ `;
1040
+
1041
+ jsgui.controls.Form_Validation_Control = Form_Validation_Control;
1042
+ jsgui.controls.Demo_UI = Demo_UI;
1043
+ jsgui.controls.form_validation_demo_ui = Demo_UI;
1044
+
1045
+ module.exports = jsgui;