jsgui3-server 0.0.144 → 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 (47) hide show
  1. package/docs/jsgui3-html-improvement-ideas.md +162 -0
  2. package/docs/jsgui3-html-improvement-ideas.svg +151 -0
  3. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +19 -14
  4. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +5 -5
  5. package/examples/jsgui3-html/01) mvvm-counter/client.js +648 -0
  6. package/examples/jsgui3-html/01) mvvm-counter/server.js +21 -0
  7. package/examples/jsgui3-html/02) date-transform/client.js +764 -0
  8. package/examples/jsgui3-html/02) date-transform/server.js +21 -0
  9. package/examples/jsgui3-html/03) form-validation/client.js +1045 -0
  10. package/examples/jsgui3-html/03) form-validation/server.js +21 -0
  11. package/examples/jsgui3-html/04) data-grid/client.js +738 -0
  12. package/examples/jsgui3-html/04) data-grid/server.js +21 -0
  13. package/examples/jsgui3-html/05) master-detail/client.js +649 -0
  14. package/examples/jsgui3-html/05) master-detail/server.js +21 -0
  15. package/examples/jsgui3-html/06) theming/client.js +514 -0
  16. package/examples/jsgui3-html/06) theming/server.js +21 -0
  17. package/examples/jsgui3-html/07) mixins/client.js +465 -0
  18. package/examples/jsgui3-html/07) mixins/server.js +21 -0
  19. package/examples/jsgui3-html/08) router/client.js +372 -0
  20. package/examples/jsgui3-html/08) router/server.js +21 -0
  21. package/examples/jsgui3-html/09) resource-transform/client.js +692 -0
  22. package/examples/jsgui3-html/09) resource-transform/server.js +21 -0
  23. package/examples/jsgui3-html/10) binding-debugger/client.js +810 -0
  24. package/examples/jsgui3-html/10) binding-debugger/server.js +21 -0
  25. package/examples/jsgui3-html/README.md +48 -0
  26. package/http/responders/static/Static_Route_HTTP_Responder.js +25 -20
  27. package/package.json +3 -3
  28. package/publishers/http-webpageorsite-publisher.js +3 -1
  29. package/serve-factory.js +12 -5
  30. package/server.js +103 -85
  31. package/tests/README.md +7 -0
  32. package/tests/end-to-end.test.js +336 -365
  33. package/tests/examples-controls.e2e.test.js +13 -1
  34. package/tests/fixtures/end-to-end-client.js +54 -0
  35. package/tests/fixtures/jsgui3-html/binding_debugger_expectations.json +15 -0
  36. package/tests/fixtures/jsgui3-html/counter_expectations.json +31 -0
  37. package/tests/fixtures/jsgui3-html/data_grid_expectations.json +26 -0
  38. package/tests/fixtures/jsgui3-html/date_transform_expectations.json +26 -0
  39. package/tests/fixtures/jsgui3-html/form_validation_expectations.json +27 -0
  40. package/tests/fixtures/jsgui3-html/master_detail_expectations.json +15 -0
  41. package/tests/fixtures/jsgui3-html/mixins_expectations.json +10 -0
  42. package/tests/fixtures/jsgui3-html/resource_transform_expectations.json +11 -0
  43. package/tests/fixtures/jsgui3-html/router_expectations.json +10 -0
  44. package/tests/fixtures/jsgui3-html/theming_expectations.json +10 -0
  45. package/tests/jsgui3-html-examples.puppeteer.test.js +537 -0
  46. package/tests/test-runner.js +1 -0
  47. package/tests/window-examples.puppeteer.test.js +217 -1
@@ -0,0 +1,648 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
3
+ const { Data_Object } = jsgui;
4
+
5
+ class Counter_Control extends jsgui.Control {
6
+ constructor(spec = {}) {
7
+ spec.__type_name = spec.__type_name || 'counter_control';
8
+ super(spec);
9
+
10
+ const initial_count = Number.isFinite(spec.initial_count) ? spec.initial_count : 0;
11
+ const initial_step = Number.isFinite(spec.step) ? spec.step : 1;
12
+
13
+ this.data.model = new Data_Object({
14
+ count: initial_count,
15
+ step: initial_step
16
+ });
17
+
18
+ this.view.data.model = new Data_Object({
19
+ display_text: '',
20
+ status_text: '',
21
+ step_error: '',
22
+ is_even: false,
23
+ is_positive: false,
24
+ step_valid: true
25
+ });
26
+
27
+ this.setup_bindings();
28
+ this.setup_computed();
29
+ this.setup_watchers();
30
+
31
+ if (!spec.el) {
32
+ this.compose();
33
+ }
34
+ }
35
+
36
+ setup_bindings() {
37
+ this.bind({
38
+ count: {
39
+ to: 'display_text',
40
+ transform: (count) => `Count: ${this.transforms.number.withCommas(count)}`
41
+ }
42
+ });
43
+ }
44
+
45
+ setup_computed() {
46
+ this.computed(
47
+ this.data.model,
48
+ ['count'],
49
+ (count) => count > 0,
50
+ { propertyName: 'is_positive', target: this.view.data.model }
51
+ );
52
+
53
+ this.computed(
54
+ this.data.model,
55
+ ['count'],
56
+ (count) => count % 2 === 0,
57
+ { propertyName: 'is_even', target: this.view.data.model }
58
+ );
59
+
60
+ this.computed(
61
+ this.data.model,
62
+ ['step'],
63
+ (step) => this.validators.range(step, 1, 10),
64
+ { propertyName: 'step_valid', target: this.view.data.model }
65
+ );
66
+
67
+ this.computed(
68
+ this.view.data.model,
69
+ ['is_positive', 'is_even'],
70
+ (is_positive, is_even) => {
71
+ const sign_label = is_positive ? 'positive' : 'negative or zero';
72
+ const parity_label = is_even ? 'even' : 'odd';
73
+ return `${sign_label} and ${parity_label}`;
74
+ },
75
+ { propertyName: 'status_text', target: this.view.data.model }
76
+ );
77
+ }
78
+
79
+ setup_watchers() {
80
+ this.watch(
81
+ this.view.data.model,
82
+ 'step_valid',
83
+ (step_valid) => {
84
+ this.view.data.model.step_error = step_valid ? '' : 'Step must be between 1 and 10.';
85
+ },
86
+ { immediate: true }
87
+ );
88
+ }
89
+
90
+ increment_count() {
91
+ const step_value = Number.isFinite(this.data.model.step) ? this.data.model.step : 0;
92
+ this.data.model.count += step_value;
93
+ }
94
+
95
+ decrement_count() {
96
+ const step_value = Number.isFinite(this.data.model.step) ? this.data.model.step : 0;
97
+ this.data.model.count -= step_value;
98
+ }
99
+
100
+ reset_count() {
101
+ this.data.model.count = 0;
102
+ }
103
+
104
+ update_step(raw_value) {
105
+ const parsed_value = this.transforms.number.parseInt(raw_value);
106
+ this.data.model.step = parsed_value === null ? null : parsed_value;
107
+ }
108
+
109
+ activate() {
110
+ if (this.__active) return;
111
+ super.activate();
112
+
113
+ if (this._dom_bound) return;
114
+ const root_el = this.dom.el;
115
+ if (!root_el) return;
116
+
117
+ this._dom_bound = true;
118
+
119
+ const counter_display_el = root_el.querySelector('[data-test="counter-display"]');
120
+ const counter_status_el = root_el.querySelector('[data-test="counter-status"]');
121
+ const step_input_el = root_el.querySelector('[data-test="step-input"]');
122
+ const step_error_el = root_el.querySelector('[data-test="step-error"]');
123
+ const increment_button_el = root_el.querySelector('[data-test="increment-button"]');
124
+ const decrement_button_el = root_el.querySelector('[data-test="decrement-button"]');
125
+ const reset_button_el = root_el.querySelector('[data-test="reset-button"]');
126
+ const step_row_el = step_input_el ? step_input_el.closest('.counter-step') : null;
127
+
128
+ const set_button_state = (button_el, is_enabled) => {
129
+ if (!button_el) return;
130
+ button_el.classList.toggle('is-disabled', !is_enabled);
131
+ button_el.setAttribute('aria-disabled', is_enabled ? 'false' : 'true');
132
+ };
133
+
134
+ if (increment_button_el) {
135
+ increment_button_el.addEventListener('click', () => {
136
+ if (!this.view.data.model.step_valid) return;
137
+ this.increment_count();
138
+ });
139
+ }
140
+
141
+ if (decrement_button_el) {
142
+ decrement_button_el.addEventListener('click', () => {
143
+ if (!this.view.data.model.step_valid) return;
144
+ this.decrement_count();
145
+ });
146
+ }
147
+
148
+ if (reset_button_el) {
149
+ reset_button_el.addEventListener('click', () => {
150
+ this.reset_count();
151
+ });
152
+ }
153
+
154
+ if (step_input_el) {
155
+ step_input_el.addEventListener('input', (event) => {
156
+ const raw_value = event && event.target ? event.target.value : '';
157
+ this.update_step(raw_value);
158
+ });
159
+ }
160
+
161
+ this.watch(
162
+ this.view.data.model,
163
+ 'display_text',
164
+ (display_text) => {
165
+ if (!counter_display_el) return;
166
+ counter_display_el.textContent = display_text || '';
167
+ },
168
+ { immediate: true }
169
+ );
170
+
171
+ this.watch(
172
+ this.view.data.model,
173
+ 'status_text',
174
+ (status_text) => {
175
+ if (!counter_status_el) return;
176
+ counter_status_el.textContent = `Status: ${status_text || ''}`;
177
+ },
178
+ { immediate: true }
179
+ );
180
+
181
+ const update_display_classes = () => {
182
+ if (!counter_display_el) return;
183
+ const is_even = this.view.data.model.is_even;
184
+ const is_positive = this.view.data.model.is_positive;
185
+ counter_display_el.classList.toggle('even', Boolean(is_even));
186
+ counter_display_el.classList.toggle('odd', !is_even);
187
+ counter_display_el.classList.toggle('positive', Boolean(is_positive));
188
+ counter_display_el.classList.toggle('negative', !is_positive);
189
+ };
190
+
191
+ this.watch(
192
+ this.view.data.model,
193
+ 'is_even',
194
+ () => update_display_classes(),
195
+ { immediate: true }
196
+ );
197
+
198
+ this.watch(
199
+ this.view.data.model,
200
+ 'is_positive',
201
+ () => update_display_classes(),
202
+ { immediate: true }
203
+ );
204
+
205
+ this.watch(
206
+ this.data.model,
207
+ 'step',
208
+ (step_value) => {
209
+ if (!step_input_el) return;
210
+ step_input_el.value = Number.isFinite(step_value) ? String(step_value) : '';
211
+ },
212
+ { immediate: true }
213
+ );
214
+
215
+ this.watch(
216
+ this.view.data.model,
217
+ 'step_error',
218
+ (step_error_text) => {
219
+ if (!step_error_el) return;
220
+ step_error_el.textContent = step_error_text || '';
221
+ },
222
+ { immediate: true }
223
+ );
224
+
225
+ this.watch(
226
+ this.view.data.model,
227
+ 'step_valid',
228
+ (step_valid) => {
229
+ if (step_row_el) {
230
+ step_row_el.classList.toggle('has-error', !step_valid);
231
+ }
232
+ set_button_state(decrement_button_el, step_valid);
233
+ set_button_state(increment_button_el, step_valid);
234
+ },
235
+ { immediate: true }
236
+ );
237
+ }
238
+
239
+ compose() {
240
+ // Framework expects the method name `compose`.
241
+ const page_context = this.context;
242
+
243
+ this.add_class('counter-control');
244
+ this.dom.attributes['data-test'] = 'counter-control';
245
+
246
+ const card = new jsgui.Control({
247
+ context: page_context,
248
+ tagName: 'div',
249
+ class: 'counter-card'
250
+ });
251
+
252
+ const title = new jsgui.Control({
253
+ context: page_context,
254
+ tagName: 'h1',
255
+ class: 'counter-title',
256
+ content: 'MVVM Counter'
257
+ });
258
+
259
+ const counter_display = new jsgui.Control({
260
+ context: page_context,
261
+ tagName: 'div',
262
+ class: 'counter-display'
263
+ });
264
+ counter_display.dom.attributes['data-test'] = 'counter-display';
265
+
266
+ const counter_status = new jsgui.Control({
267
+ context: page_context,
268
+ tagName: 'div',
269
+ class: 'counter-status'
270
+ });
271
+ counter_status.dom.attributes['data-test'] = 'counter-status';
272
+
273
+ const step_row = new jsgui.Control({
274
+ context: page_context,
275
+ tagName: 'div',
276
+ class: 'counter-step'
277
+ });
278
+
279
+ const step_label = new jsgui.Control({
280
+ context: page_context,
281
+ tagName: 'label',
282
+ class: 'counter-step-label',
283
+ content: 'Step'
284
+ });
285
+
286
+ const step_input = new jsgui.Control({
287
+ context: page_context,
288
+ tagName: 'input',
289
+ class: 'counter-step-input'
290
+ });
291
+ step_input.dom.attributes.type = 'number';
292
+ step_input.dom.attributes.min = '1';
293
+ step_input.dom.attributes.max = '10';
294
+ step_input.dom.attributes.step = '1';
295
+ step_input.dom.attributes.id = 'counter-step-input';
296
+ step_input.dom.attributes['data-test'] = 'step-input';
297
+ step_label.dom.attributes['for'] = 'counter-step-input';
298
+
299
+ const step_error = new jsgui.Control({
300
+ context: page_context,
301
+ tagName: 'div',
302
+ class: 'counter-step-error'
303
+ });
304
+ step_error.dom.attributes['data-test'] = 'step-error';
305
+
306
+ step_row.add(step_label);
307
+ step_row.add(step_input);
308
+ step_row.add(step_error);
309
+
310
+ const button_row = new jsgui.Control({
311
+ context: page_context,
312
+ tagName: 'div',
313
+ class: 'counter-buttons'
314
+ });
315
+
316
+ const decrement_button = new jsgui.Control({
317
+ context: page_context,
318
+ tagName: 'button',
319
+ class: 'counter-button',
320
+ content: '-'
321
+ });
322
+ decrement_button.dom.attributes['data-test'] = 'decrement-button';
323
+
324
+ const reset_button = new jsgui.Control({
325
+ context: page_context,
326
+ tagName: 'button',
327
+ class: 'counter-button',
328
+ content: 'Reset'
329
+ });
330
+ reset_button.dom.attributes['data-test'] = 'reset-button';
331
+
332
+ const increment_button = new jsgui.Control({
333
+ context: page_context,
334
+ tagName: 'button',
335
+ class: 'counter-button',
336
+ content: '+'
337
+ });
338
+ increment_button.dom.attributes['data-test'] = 'increment-button';
339
+
340
+ decrement_button.on('click', () => {
341
+ if (!this.view.data.model.step_valid) return;
342
+ this.decrement_count();
343
+ });
344
+
345
+ reset_button.on('click', () => {
346
+ this.reset_count();
347
+ });
348
+
349
+ increment_button.on('click', () => {
350
+ if (!this.view.data.model.step_valid) return;
351
+ this.increment_count();
352
+ });
353
+
354
+ step_input.on('input', (event) => {
355
+ const raw_value = event && event.target ? event.target.value : '';
356
+ this.update_step(raw_value);
357
+ });
358
+
359
+ button_row.add(decrement_button);
360
+ button_row.add(reset_button);
361
+ button_row.add(increment_button);
362
+
363
+ card.add(title);
364
+ card.add(counter_display);
365
+ card.add(counter_status);
366
+ card.add(step_row);
367
+ card.add(button_row);
368
+
369
+ this.add(card);
370
+
371
+ const set_button_state = (button, is_enabled) => {
372
+ if (is_enabled) {
373
+ button.remove_class('is-disabled');
374
+ button.dom.attributes['aria-disabled'] = 'false';
375
+ } else {
376
+ button.add_class('is-disabled');
377
+ button.dom.attributes['aria-disabled'] = 'true';
378
+ }
379
+ };
380
+
381
+ this.watch(
382
+ this.view.data.model,
383
+ 'display_text',
384
+ (display_text) => {
385
+ counter_display.clear();
386
+ counter_display.add(display_text || '');
387
+ },
388
+ { immediate: true }
389
+ );
390
+
391
+ this.watch(
392
+ this.view.data.model,
393
+ 'status_text',
394
+ (status_text) => {
395
+ counter_status.clear();
396
+ counter_status.add(`Status: ${status_text || ''}`);
397
+ },
398
+ { immediate: true }
399
+ );
400
+
401
+ const update_display_classes = () => {
402
+ const is_even = this.view.data.model.is_even;
403
+ const is_positive = this.view.data.model.is_positive;
404
+
405
+ counter_display.remove_class('even');
406
+ counter_display.remove_class('odd');
407
+ counter_display.remove_class('positive');
408
+ counter_display.remove_class('negative');
409
+
410
+ if (is_even) {
411
+ counter_display.add_class('even');
412
+ } else {
413
+ counter_display.add_class('odd');
414
+ }
415
+
416
+ if (is_positive) {
417
+ counter_display.add_class('positive');
418
+ } else {
419
+ counter_display.add_class('negative');
420
+ }
421
+ };
422
+
423
+ this.watch(
424
+ this.view.data.model,
425
+ 'is_even',
426
+ () => update_display_classes(),
427
+ { immediate: true }
428
+ );
429
+
430
+ this.watch(
431
+ this.view.data.model,
432
+ 'is_positive',
433
+ () => update_display_classes(),
434
+ { immediate: true }
435
+ );
436
+
437
+ this.watch(
438
+ this.data.model,
439
+ 'step',
440
+ (step_value) => {
441
+ const step_text = Number.isFinite(step_value) ? String(step_value) : '';
442
+ step_input.dom.attributes.value = step_text;
443
+ },
444
+ { immediate: true }
445
+ );
446
+
447
+ this.watch(
448
+ this.view.data.model,
449
+ 'step_error',
450
+ (step_error_text) => {
451
+ step_error.clear();
452
+ if (step_error_text) {
453
+ step_error.add(step_error_text);
454
+ }
455
+ },
456
+ { immediate: true }
457
+ );
458
+
459
+ this.watch(
460
+ this.view.data.model,
461
+ 'step_valid',
462
+ (step_valid) => {
463
+ if (step_valid) {
464
+ step_row.remove_class('has-error');
465
+ } else {
466
+ step_row.add_class('has-error');
467
+ }
468
+
469
+ set_button_state(decrement_button, step_valid);
470
+ set_button_state(increment_button, step_valid);
471
+ },
472
+ { immediate: true }
473
+ );
474
+ }
475
+ }
476
+
477
+ class Demo_UI extends Active_HTML_Document {
478
+ constructor(spec = {}) {
479
+ spec.__type_name = spec.__type_name || 'mvvm_counter_demo_ui';
480
+ super(spec);
481
+
482
+ if (!spec.el) {
483
+ this.compose();
484
+ }
485
+ }
486
+
487
+ compose() {
488
+ // Framework expects the method name `compose`.
489
+ const page_context = this.context;
490
+ this.body.add_class('mvvm-counter-demo');
491
+
492
+ const counter_control = new Counter_Control({
493
+ context: page_context,
494
+ initial_count: 0,
495
+ step: 1
496
+ });
497
+
498
+ this.body.add(counter_control);
499
+ }
500
+ }
501
+
502
+ Demo_UI.css = `
503
+ * {
504
+ box-sizing: border-box;
505
+ }
506
+
507
+ body {
508
+ margin: 0;
509
+ padding: 0;
510
+ font-family: "Trebuchet MS", "Verdana", sans-serif;
511
+ background: #f2f4f8;
512
+ color: #1c232e;
513
+ }
514
+
515
+ .mvvm-counter-demo {
516
+ min-height: 100vh;
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ padding: 24px;
521
+ }
522
+
523
+ .counter-control {
524
+ width: 100%;
525
+ max-width: 420px;
526
+ }
527
+
528
+ .counter-card {
529
+ background: #ffffff;
530
+ border-radius: 16px;
531
+ padding: 28px 30px;
532
+ border: 1px solid #d7dde6;
533
+ box-shadow: 0 20px 45px rgba(20, 32, 52, 0.1);
534
+ }
535
+
536
+ .counter-title {
537
+ margin: 0 0 12px;
538
+ font-size: 22px;
539
+ font-weight: 700;
540
+ }
541
+
542
+ .counter-display {
543
+ font-size: 32px;
544
+ font-weight: 700;
545
+ padding: 12px 0;
546
+ border-bottom: 1px solid #e3e7ee;
547
+ }
548
+
549
+ .counter-display.positive {
550
+ color: #1c7c54;
551
+ }
552
+
553
+ .counter-display.negative {
554
+ color: #b3382c;
555
+ }
556
+
557
+ .counter-display.even {
558
+ letter-spacing: 1px;
559
+ }
560
+
561
+ .counter-display.odd {
562
+ letter-spacing: 0;
563
+ }
564
+
565
+ .counter-status {
566
+ margin-top: 8px;
567
+ color: #3c4b5d;
568
+ font-size: 14px;
569
+ }
570
+
571
+ .counter-step {
572
+ margin-top: 18px;
573
+ display: grid;
574
+ grid-template-columns: auto 1fr;
575
+ grid-template-rows: auto auto;
576
+ gap: 6px 12px;
577
+ align-items: center;
578
+ }
579
+
580
+ .counter-step-label {
581
+ font-size: 14px;
582
+ font-weight: 600;
583
+ }
584
+
585
+ .counter-step-input {
586
+ padding: 8px 10px;
587
+ border-radius: 8px;
588
+ border: 1px solid #c7ced8;
589
+ font-size: 14px;
590
+ }
591
+
592
+ .counter-step.has-error .counter-step-input {
593
+ border-color: #d64545;
594
+ background: #fff3f3;
595
+ }
596
+
597
+ .counter-step-error {
598
+ grid-column: 1 / -1;
599
+ font-size: 12px;
600
+ color: #b3382c;
601
+ min-height: 16px;
602
+ }
603
+
604
+ .counter-buttons {
605
+ display: grid;
606
+ grid-template-columns: repeat(3, 1fr);
607
+ gap: 10px;
608
+ margin-top: 18px;
609
+ }
610
+
611
+ .counter-button {
612
+ padding: 10px 12px;
613
+ border-radius: 10px;
614
+ border: 1px solid #c7ced8;
615
+ background: #f7f9fc;
616
+ font-size: 16px;
617
+ cursor: pointer;
618
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
619
+ }
620
+
621
+ .counter-button:hover {
622
+ transform: translateY(-1px);
623
+ box-shadow: 0 6px 12px rgba(17, 24, 39, 0.08);
624
+ }
625
+
626
+ .counter-button.is-disabled {
627
+ opacity: 0.5;
628
+ cursor: not-allowed;
629
+ box-shadow: none;
630
+ transform: none;
631
+ }
632
+
633
+ @media (max-width: 520px) {
634
+ .counter-card {
635
+ padding: 22px;
636
+ }
637
+
638
+ .counter-display {
639
+ font-size: 28px;
640
+ }
641
+ }
642
+ `;
643
+
644
+ jsgui.controls.Counter_Control = Counter_Control;
645
+ jsgui.controls.Demo_UI = Demo_UI;
646
+ jsgui.controls.mvvm_counter_demo_ui = Demo_UI;
647
+
648
+ module.exports = jsgui;
@@ -0,0 +1,21 @@
1
+ const jsgui = require('./client');
2
+ const Server = require('../../../server');
3
+ const { Demo_UI } = jsgui.controls;
4
+
5
+ if (require.main === module) {
6
+ const server = new Server({
7
+ Ctrl: Demo_UI,
8
+ src_path_client_js: require.resolve('./client.js')
9
+ });
10
+
11
+ server.allowed_addresses = ['127.0.0.1'];
12
+
13
+ server.on('ready', () => {
14
+ server.start(52000, (err) => {
15
+ if (err) {
16
+ throw err;
17
+ }
18
+ console.log('server started on port 52000');
19
+ });
20
+ });
21
+ }