@webqit/oohtml 3.0.1-8 → 3.1.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/README.md +530 -228
- package/dist/bindings-api.js +1 -1
- package/dist/bindings-api.js.map +3 -3
- package/dist/context-api.js +1 -1
- package/dist/context-api.js.map +3 -3
- package/dist/data-binding.js +16 -36
- package/dist/data-binding.js.map +3 -3
- package/dist/html-imports.js +1 -1
- package/dist/html-imports.js.map +3 -3
- package/dist/main.js +31 -37
- package/dist/main.js.map +3 -3
- package/dist/main.lite.js +56 -0
- package/dist/main.lite.js.map +7 -0
- package/dist/namespaced-html.js +1 -1
- package/dist/namespaced-html.js.map +3 -3
- package/dist/scoped-css.js +3 -3
- package/dist/scoped-css.js.map +2 -2
- package/dist/scoped-js.js +1 -21
- package/dist/scoped-js.js.map +3 -3
- package/package.json +3 -5
- package/src/api.global.js +11 -0
- package/src/api.global.lite.js +11 -0
- package/src/bindings-api/DOMBindingsContext.js +51 -0
- package/src/bindings-api/index.js +30 -42
- package/src/context-api/DOMContext.js +128 -0
- package/src/context-api/DOMContextResponse.js +15 -0
- package/src/context-api/DOMContexts.js +89 -0
- package/src/context-api/DuplicateContextError.js +2 -0
- package/src/context-api/_DOMContextRequestEvent.js +49 -0
- package/src/context-api/index.js +20 -22
- package/src/data-binding/index.js +20 -26
- package/src/html-imports/{_HTMLImportsProvider.js → HTMLImportsContext.js} +42 -33
- package/src/html-imports/{_HTMLExportsManager.js → HTMLModule.js} +40 -38
- package/src/html-imports/_HTMLImportElement.js +17 -17
- package/src/html-imports/index.js +46 -49
- package/src/index.js +2 -7
- package/src/namespaced-html/index.js +3 -7
- package/src/scoped-css/index.js +1 -1
- package/src/scoped-js/index.js +2 -13
- package/src/util.js +8 -5
- package/test/imports.test.js +1 -3
- package/test/index.js +1 -1
- package/test/modules.test.js +35 -38
- package/src/bindings-api/_HTMLBindingsProvider.js +0 -51
- package/src/context-api/ContextReturnValue.js +0 -22
- package/src/context-api/HTMLContext.js +0 -85
- package/src/context-api/HTMLContextProvider.js +0 -175
- package/src/context-api/_ContextRequestEvent.js +0 -26
- package/src/html-imports/_HTMLImportElement copy.js +0 -217
- package/src/targets.browser.js +0 -10
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@ Vanilla HTML is unsurprisingly becoming a compelling option for an increasing nu
|
|
|
25
25
|
|
|
26
26
|
This project pursues an object-oriented approach to HTML and implicitly revisits much of what inhibits the idea of a *component* architecture for HTML!
|
|
27
27
|
|
|
28
|
+
<!--
|
|
28
29
|
└ [See more in the introductory blog post](https://dev.to/oxharris/the-web-native-equations-1m1p-temp-slug-6661657?preview=ba70ad2c17f05b5761bc74516dbde8c9eff8b581a0420d87334fd9ef6bab9d6e6d3ab6aaf3fe02542bb9e7250d0a88a6df91dae40919aabcc9a07320)<sup>draft</sup>
|
|
30
|
+
-->
|
|
29
31
|
|
|
30
32
|
</details>
|
|
31
33
|
|
|
@@ -66,7 +68,7 @@ user
|
|
|
66
68
|
└── email
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
**-->** *with a
|
|
71
|
+
**-->** *with a complementary API that exposes said structure to JavaScript applications*:
|
|
70
72
|
|
|
71
73
|
```js
|
|
72
74
|
// The document.namespace API
|
|
@@ -142,7 +144,7 @@ Here, we get the `scoped` attribute for *scoping* said element-specific styleshe
|
|
|
142
144
|
</div>
|
|
143
145
|
```
|
|
144
146
|
|
|
145
|
-
**-->** *with a
|
|
147
|
+
**-->** *with a complementary API that exposes said assets to JavaScript applications*:
|
|
146
148
|
|
|
147
149
|
```js
|
|
148
150
|
let { styleSheets, scripts } = user; // APIs that are analogous to the document.styleSheets, document.scripts properties
|
|
@@ -218,10 +220,9 @@ Here, we get the `<template src>` element for that:
|
|
|
218
220
|
```html
|
|
219
221
|
-- file: /nested.html --
|
|
220
222
|
<div def="fragment2"></div>
|
|
221
|
-
--
|
|
222
223
|
```
|
|
223
224
|
|
|
224
|
-
**-->** *which
|
|
225
|
+
**-->** *which borrows from how elements like images work; terminating with either a `load` or an `error` event*:
|
|
225
226
|
|
|
226
227
|
```js
|
|
227
228
|
foo.addEventListener('load', loadCallback);
|
|
@@ -250,19 +251,20 @@ Here, we get an `<import>` element that lets us do that declaratively:
|
|
|
250
251
|
|
|
251
252
|
<details><summary>All in realtime</summary>
|
|
252
253
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
+ the
|
|
254
|
+
Import *refs* are live references and are sensitive to:
|
|
255
|
+
|
|
256
|
+
+ changes in the *ref* itself (in being defined/undefined/redefined)
|
|
257
|
+
+ changes in the referenced *defs* themselves (in being added/removed/loaded)
|
|
258
|
+
|
|
259
|
+
And an `<import>` element that has been resolved will self-restore in the event that:
|
|
256
260
|
|
|
257
|
-
|
|
258
|
-
+ the `<import>` element no longer points to a module; or the module has been emptied or removed.
|
|
259
|
-
+ the previously slotted contents have been programmatically removed and slot is empty.
|
|
261
|
+
+ the previously slotted contents have *all* been programmatically removed and slot is empty.
|
|
260
262
|
|
|
261
263
|
</details>
|
|
262
264
|
|
|
263
|
-
<details><summary>With SSR
|
|
265
|
+
<details><summary>With SSR support</summary>
|
|
264
266
|
|
|
265
|
-
On the server, these `<import>` elements
|
|
267
|
+
On the server, these `<import>` elements will retain their place in the DOM, but this time, serialized into comment nodes, while having their output rendered just above them as siblings.
|
|
266
268
|
|
|
267
269
|
The above resolved imports would thus give us something like:
|
|
268
270
|
|
|
@@ -275,23 +277,23 @@ The above resolved imports would thus give us something like:
|
|
|
275
277
|
</body>
|
|
276
278
|
```
|
|
277
279
|
|
|
278
|
-
But they also
|
|
280
|
+
But they also will need to remember the exact imported nodes that they manage so as to be able to re-establish that relationship on getting to the client. This information is automatically encoded as part of the serialised element itself, in something like:
|
|
279
281
|
|
|
280
282
|
```html
|
|
281
283
|
<!--<import ref="/foo/nested#fragment2" nodecount="1"></import>-->
|
|
282
284
|
```
|
|
283
285
|
|
|
284
|
-
Now that extra bit of information gets decoded and original relationships are formed again
|
|
286
|
+
Now, on getting to the client and "hydrating" the `<import>` element, that extra bit of information gets decoded, and original relationships are formed again. But, the `<import>` element itself stays invisible in the DOM while still continuing to kick as above!
|
|
285
287
|
|
|
286
288
|
> Note: We know we're on the server when `window.webqit.env === 'server'`. This flag is automatically set by OOHTML's current SSR engine: [OOHTML-SSR](https://github.com/webqit/oohtml-ssr)
|
|
287
289
|
|
|
288
290
|
</details>
|
|
289
291
|
|
|
290
|
-
###
|
|
292
|
+
### Imperative Module Imports
|
|
291
293
|
|
|
292
294
|
JavaScript applications will need more than a declarative import mechanism.
|
|
293
295
|
|
|
294
|
-
Here, we get an *HTMLImports* API for
|
|
296
|
+
Here, we get an *HTMLImports* API for imperative module import:
|
|
295
297
|
|
|
296
298
|
```js
|
|
297
299
|
const moduleObject1 = document.import('/foo#fragment1');
|
|
@@ -303,7 +305,7 @@ const moduleObject2 = document.import('/foo/nested#fragment2');
|
|
|
303
305
|
console.log(moduleObject2.value); // divElement
|
|
304
306
|
```
|
|
305
307
|
|
|
306
|
-
**-->** *with
|
|
308
|
+
**-->** *with the `moduleObject.value` property being a live property for when results are asynchronous; e.g. asynchronously loaded modules*:
|
|
307
309
|
|
|
308
310
|
```js
|
|
309
311
|
Observer.observe(moduleObject2, 'value', e => {
|
|
@@ -319,7 +321,7 @@ document.import('/foo#fragment1', divElement => {
|
|
|
319
321
|
});
|
|
320
322
|
```
|
|
321
323
|
|
|
322
|
-
**-->** *with an optional `live` parameter for staying subscribed to
|
|
324
|
+
**-->** *with an optional `live` parameter for staying subscribed to updated results*:
|
|
323
325
|
|
|
324
326
|
```js
|
|
325
327
|
const moduleObject2 = document.import('/foo/nested#fragment2', true/*live*/);
|
|
@@ -335,38 +337,25 @@ document.import('/foo#fragment1', true/*live*/, divElement => {
|
|
|
335
337
|
});
|
|
336
338
|
```
|
|
337
339
|
|
|
338
|
-
*...both of which
|
|
340
|
+
*...both of which gets notified on doing the below*:
|
|
339
341
|
|
|
340
342
|
```js
|
|
341
343
|
document.querySelector('template[def="foo"]').content.firstElementChild.remove();
|
|
342
344
|
```
|
|
343
345
|
|
|
344
|
-
**-->** *with
|
|
346
|
+
**-->** *with a `moduleObject.abort()` method for unsubscribing from live updates*:
|
|
345
347
|
|
|
346
|
-
|
|
347
|
-
const abortController = new AbortController;
|
|
348
|
-
```
|
|
348
|
+
**-->** *with an optional `signal` parameter for passing in a custom `AbortSignal` instance*:
|
|
349
349
|
|
|
350
350
|
```js
|
|
351
|
+
const abortController = new AbortController;
|
|
351
352
|
const moduleObject2 = document.import('/foo/nested#fragment2', { live: true, signal: abortController.signal });
|
|
352
353
|
```
|
|
353
354
|
|
|
354
|
-
*...in the absence of which an `AbortSignal` instance is automatically created internally for the same purpose, such that we're always able to do*:
|
|
355
|
-
|
|
356
|
-
```js
|
|
357
|
-
moduleObject2.abort();
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
*...whereas in the `callback` approach, no automatic `AbortSignals` are created*:
|
|
361
|
-
|
|
362
355
|
```js
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
});
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
```js
|
|
369
|
-
setTimeout(() => abortController.abort(), 1000);
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
abortController.abort(); // which would also call moduleObject2.abort()
|
|
358
|
+
}, 1000);
|
|
370
359
|
```
|
|
371
360
|
|
|
372
361
|
<details><summary>Extended Imports concepts</summary>
|
|
@@ -375,7 +364,7 @@ setTimeout(() => abortController.abort(), 1000);
|
|
|
375
364
|
|
|
376
365
|
We can defer module loading until we really need them.
|
|
377
366
|
|
|
378
|
-
Here, we get the `loading="lazy"` directive for that; and loading is only then triggered on the first attempt to import their contents:
|
|
367
|
+
Here, we get the `loading="lazy"` directive for that; and loading is only then triggered on the first attempt to import them or their contents:
|
|
379
368
|
|
|
380
369
|
```html
|
|
381
370
|
<!-- Loading doesn't happen until the first time this is being accessed -->
|
|
@@ -391,43 +380,6 @@ Here, we get the `loading="lazy"` directive for that; and loading is only then t
|
|
|
391
380
|
const moduleObject2 = document.import('/foo#fragment1'); // Triggers module loading and resolves at moduleObject2.value on load success
|
|
392
381
|
```
|
|
393
382
|
|
|
394
|
-
### Scoped Modules
|
|
395
|
-
|
|
396
|
-
Some modules will only be relevant within a specific context in the page, and these shouldn't map to the global document scope.
|
|
397
|
-
|
|
398
|
-
Here, we get the `scoped` attribute for scoping those to their respective contexts, and thus, implicitly have an *object-scoped* module system:
|
|
399
|
-
|
|
400
|
-
```html
|
|
401
|
-
<section> <!-- object with own modules -->
|
|
402
|
-
|
|
403
|
-
<template def="foo" scoped> <!-- Scoped to host object and not available globally -->
|
|
404
|
-
<div def="fragment1"></div>
|
|
405
|
-
</template>
|
|
406
|
-
|
|
407
|
-
<div>
|
|
408
|
-
<import ref="foo#fragment1"></import> <!-- Relative path (beginning without a slash), resolves to the local module: foo#fragment1 -->
|
|
409
|
-
<import ref="/foo#fragment1"></import> <!-- Absolute path, resolves to the global module: /foo#fragment1 -->
|
|
410
|
-
</div>
|
|
411
|
-
|
|
412
|
-
</section>
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
**-->** *with an equivalent `Element.prototype.import()` API for accessing said scoped modules*:
|
|
416
|
-
|
|
417
|
-
```js
|
|
418
|
-
// Using the HTMLImports API
|
|
419
|
-
const moduleHost = document.querySelector('div');
|
|
420
|
-
const localImport1 = moduleHost.import('foo#fragment1'); // the local module: foo#fragment1
|
|
421
|
-
console.log(localImport1); // { value: div }
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
```js
|
|
425
|
-
// Using the HTMLImports API
|
|
426
|
-
const moduleHost = document.querySelector('div');
|
|
427
|
-
const globalImport1 = moduleHost.import('/foo#fragment1'); // the global module: foo#fragment1
|
|
428
|
-
console.log(globalImport1); // { value: div }
|
|
429
|
-
```
|
|
430
|
-
|
|
431
383
|
### Module Inheritance
|
|
432
384
|
|
|
433
385
|
We'll often have repeating markup structures.
|
|
@@ -478,80 +430,142 @@ Here, we get module nesting with inheritance to simplify that:
|
|
|
478
430
|
|
|
479
431
|
### Imports Contexts
|
|
480
432
|
|
|
481
|
-
We should be able to
|
|
433
|
+
We should be able to have *relative* import refs that resolve against local contexts in the document tree.
|
|
434
|
+
|
|
435
|
+
Here, we call those arbitrary contexts "Imports Contexts", and these could be:
|
|
482
436
|
|
|
483
|
-
|
|
437
|
+
+ Simple Base Path Contexts ([below](#base-path-contexts))
|
|
438
|
+
+ Scoped Module Contexts ([below](#scoped-module-contexts))
|
|
439
|
+
+ Named Contexts ([below](#named-contexts))
|
|
440
|
+
+ Extended Scoped Module Contexts ([below](#extended-scoped-module-contexts))
|
|
441
|
+
|
|
442
|
+
Alongside which we have a complementary `Element.prototype.import()` API for thinking in contexts.
|
|
443
|
+
|
|
444
|
+
#### "Base Path" Contexts
|
|
445
|
+
|
|
446
|
+
Base paths may be defined at arbitrary levels in the page using the `importscontext` attribute:
|
|
484
447
|
|
|
485
448
|
```html
|
|
486
449
|
<body importscontext="/foo">
|
|
487
450
|
<section>
|
|
488
|
-
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash),
|
|
451
|
+
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash), resolving to: /foo#fragment1 -->
|
|
489
452
|
</section>
|
|
490
453
|
</body>
|
|
491
454
|
```
|
|
492
455
|
|
|
493
456
|
```html
|
|
494
457
|
<body importscontext="/foo/nested">
|
|
458
|
+
<main>
|
|
459
|
+
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolving to: /foo/nested#fragment2 -->
|
|
460
|
+
</main>
|
|
461
|
+
</body>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**-->** *with said base paths being able to "nest" nicely*:
|
|
465
|
+
|
|
466
|
+
```html
|
|
467
|
+
<body importscontext="/foo">
|
|
468
|
+
|
|
495
469
|
<section>
|
|
496
|
-
<import ref="#
|
|
470
|
+
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash), resolves to: /foo#fragment1 -->
|
|
497
471
|
</section>
|
|
472
|
+
|
|
473
|
+
<div importscontext="nested"> <!-- Relative path (beginning without a slash), resolves to: /foo/nested -->
|
|
474
|
+
|
|
475
|
+
<main>
|
|
476
|
+
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolves to: /foo/nested#fragment2 -->
|
|
477
|
+
</main>
|
|
478
|
+
|
|
479
|
+
</div>
|
|
480
|
+
|
|
498
481
|
</body>
|
|
499
482
|
```
|
|
500
483
|
|
|
484
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
485
|
+
|
|
501
486
|
```js
|
|
502
|
-
// Using the HTMLImports API
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
487
|
+
// Using the HTMLImports API to import from context
|
|
488
|
+
const contextElement = document.querySelector('section');
|
|
489
|
+
const response = contextElement.import('#fragment1'); // Relative path (beginning without a slash), resolving to: /foo#fragment1
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
// Using the HTMLImports API to import from context
|
|
494
|
+
const contextElement = document.querySelector('main');
|
|
495
|
+
const response = contextElement.import('#fragment1'); // Relative path (beginning without a slash), resolving to: /foo/nested#fragment1
|
|
506
496
|
```
|
|
507
497
|
|
|
508
|
-
|
|
498
|
+
#### "Scoped Module" Contexts
|
|
499
|
+
|
|
500
|
+
Some modules will only be relevant within a specific context in the page, and those wouldn't need to have a business with the global scope.
|
|
501
|
+
|
|
502
|
+
Here, we get the `scoped` attribute for scoping those to their respective contexts, to give us an *object-scoped* module system (like what Scoped Registries are to Custom Elements):
|
|
509
503
|
|
|
510
504
|
```html
|
|
511
|
-
<
|
|
505
|
+
<section> <!-- Host object -->
|
|
512
506
|
|
|
513
|
-
<
|
|
507
|
+
<template def="foo" scoped> <!-- Scoped to host object and not available globally -->
|
|
508
|
+
<div def="fragment1"></div>
|
|
509
|
+
</template>
|
|
514
510
|
|
|
515
|
-
<
|
|
516
|
-
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash),
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
</div>
|
|
520
|
-
</section>
|
|
511
|
+
<div>
|
|
512
|
+
<import ref="foo#fragment1"></import> <!-- Relative path (beginning without a slash), resolving to the local module: foo#fragment1 -->
|
|
513
|
+
<import ref="/foo#fragment1"></import> <!-- Absolute path, resolving to the global module: /foo#fragment1 -->
|
|
514
|
+
</div>
|
|
521
515
|
|
|
522
|
-
</
|
|
516
|
+
</section>
|
|
523
517
|
```
|
|
524
518
|
|
|
519
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
520
|
+
|
|
525
521
|
```js
|
|
526
|
-
// Using the HTMLImports API
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
console.log(globalImport2); // { value: div }
|
|
522
|
+
// Using the HTMLImports API for local import
|
|
523
|
+
const contextElement = document.querySelector('div');
|
|
524
|
+
const localModule = moduleHost.import('#fragment1'); // Relative path (beginning without a slash), resolving to the local module: foo#fragment1
|
|
530
525
|
```
|
|
531
526
|
|
|
532
|
-
|
|
527
|
+
```js
|
|
528
|
+
// Using the HTMLImports API for global import
|
|
529
|
+
const contextElement = document.querySelector('div');
|
|
530
|
+
const globalModule = contextElement.import('/foo#fragment1'); // Absolute path, resolving to the global module: /foo#fragment1
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### Named Contexts
|
|
534
|
+
|
|
535
|
+
Imports Contexts may be named:
|
|
533
536
|
|
|
534
537
|
```html
|
|
535
|
-
<body importscontext="/foo">
|
|
538
|
+
<body contextname="context1" importscontext="/foo/nested">
|
|
536
539
|
|
|
537
|
-
<import ref="#
|
|
540
|
+
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolves to: /foo/nested#fragment2 -->
|
|
541
|
+
|
|
542
|
+
<section importscontext="/foo">
|
|
543
|
+
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash), resolves to: /foo#fragment1 -->
|
|
544
|
+
|
|
545
|
+
<div>
|
|
546
|
+
<import ref="@context1#fragment2"></import> <!-- Context-relative path (beginning with a context name), resolves to: /foo/nested#fragment2 -->
|
|
547
|
+
</div>
|
|
538
548
|
|
|
539
|
-
<section importscontext="nested"> <!-- Relative path (beginning without a slash), resolves to: /foo/nested -->
|
|
540
|
-
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolves to: /foo/nested#fragment2 -->
|
|
541
549
|
</section>
|
|
542
550
|
|
|
543
551
|
</body>
|
|
544
552
|
```
|
|
545
553
|
|
|
546
|
-
|
|
554
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
547
555
|
|
|
548
|
-
|
|
556
|
+
```js
|
|
557
|
+
// Using the HTMLImports API to import from a named Imports Context
|
|
558
|
+
const contextElement = document.querySelector('div');
|
|
559
|
+
const result = contextElement.import('@context1#fragment2'); // Resolving to the module:/foo/nested#fragment2
|
|
560
|
+
```
|
|
549
561
|
|
|
550
|
-
|
|
562
|
+
#### Extended Scoped Module Contexts
|
|
563
|
+
|
|
564
|
+
Scoped Module Contexts may also have a Base Path Context that they inherit from:
|
|
551
565
|
|
|
552
566
|
```html
|
|
553
567
|
<body contextname="context1" importscontext="/bar">
|
|
554
|
-
<section importscontext="nested"> <!-- object with
|
|
568
|
+
<section importscontext="nested"> <!-- object with Scoped Modules, plus inherited context: /bar/nested -->
|
|
555
569
|
|
|
556
570
|
<template def="foo" scoped> <!-- Scoped to host object and not available globally -->
|
|
557
571
|
<div def="fragment1"></div>
|
|
@@ -559,33 +573,36 @@ Here, we're able to have *one* element implement *both* at the same time - with
|
|
|
559
573
|
</template>
|
|
560
574
|
|
|
561
575
|
<div>
|
|
562
|
-
<import ref="foo#fragment2"></import> <!-- Relative path (beginning without a slash),
|
|
563
|
-
<import ref="/foo#fragment1"></import> <!-- Absolute path,
|
|
564
|
-
<import ref="@context1#fragment1"></import> <!--
|
|
576
|
+
<import ref="foo#fragment2"></import> <!-- Relative path (beginning without a slash), resolving to the local module: foo#fragment2, and if not found, the inherited module: /bar/nested/foo#2 -->
|
|
577
|
+
<import ref="/foo#fragment1"></import> <!-- Absolute path, resolving to the global module: /foo#fragment1 -->
|
|
578
|
+
<import ref="@context1#fragment1"></import> <!-- Relative path with a named context, resolving to the global module: /bar#fragment1 -->
|
|
565
579
|
</div>
|
|
566
580
|
|
|
567
581
|
</section>
|
|
568
582
|
</body>
|
|
569
583
|
```
|
|
570
584
|
|
|
585
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
586
|
+
|
|
571
587
|
```js
|
|
572
588
|
// Using the HTMLImports API
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
console.log(localOrGlobalImport2); // { value: div }
|
|
589
|
+
const contextElement = document.querySelector('div');
|
|
590
|
+
const result = contextElement.import('#fragment2'); // the local module: foo#fragment2, and if not found, the inherited module: /bar/nested#fragment2
|
|
576
591
|
```
|
|
577
592
|
|
|
578
593
|
</details>
|
|
579
594
|
|
|
580
595
|
## Data Binding
|
|
581
596
|
|
|
582
|
-
Data binding is about declaratively binding the UI to application data,
|
|
597
|
+
Data binding is about declaratively binding the UI to application data, wherein the relevant parts of the UI *automatically* update as application state changes.
|
|
583
598
|
|
|
584
|
-
OOHTML makes this possible in just simple conventions - via a new comment-based data-binding syntax `<?{ }?>` and a complementary new `
|
|
599
|
+
OOHTML makes this possible in just simple conventions - via a new comment-based data-binding syntax `<?{ }?>` and a complementary new `expr` attribute!
|
|
600
|
+
|
|
601
|
+
And there's one more: Quantum Scripts which brings the most advanced form of reactivity to HTML!
|
|
585
602
|
|
|
586
603
|
### Discrete Data-Binding
|
|
587
604
|
|
|
588
|
-
Here, we get a comment-based data-binding tag `<?{ }?>` which
|
|
605
|
+
Here, we get a comment-based data-binding tag `<?{ }?>` which gives us a regular HTML comment but also an insertion point for application data:
|
|
589
606
|
|
|
590
607
|
```js
|
|
591
608
|
<html>
|
|
@@ -599,9 +616,9 @@ Here, we get a comment-based data-binding tag `<?{ }?>` which works like regular
|
|
|
599
616
|
</html>
|
|
600
617
|
```
|
|
601
618
|
|
|
602
|
-
<details><summary>
|
|
619
|
+
<details><summary>Resolution details</summary>
|
|
603
620
|
|
|
604
|
-
Here, JavaScript references are resolved from the closest node up the document
|
|
621
|
+
Here, JavaScript references are resolved from the closest node up the document tree that exposes a corresponding *binding* on its Bindings API ([discussed below](#bindings-api)). Thus, for the above markup, our underlying data structure could be something like the below:
|
|
605
622
|
|
|
606
623
|
```js
|
|
607
624
|
document.bind({ name: 'James Boye', cool: '100%', app: { title: 'Demo App' } });
|
|
@@ -615,7 +632,7 @@ document: { name: 'James Boye', cool: '100%', app: { title: 'Demo App' } }
|
|
|
615
632
|
└── body: { name: 'John Doe' }
|
|
616
633
|
```
|
|
617
634
|
|
|
618
|
-
|
|
635
|
+
Now, the `name` reference remains bound to the `name` *binding* on the `<body>` element until the meaning of "closest node" changes again:
|
|
619
636
|
|
|
620
637
|
```js
|
|
621
638
|
delete document.body.bindings.name;
|
|
@@ -627,21 +644,21 @@ While the `cool` reference remains bound to the `cool` *binding* on the `documen
|
|
|
627
644
|
document.body.bindings.cool = '200%';
|
|
628
645
|
```
|
|
629
646
|
|
|
630
|
-
|
|
647
|
+
</details>
|
|
631
648
|
|
|
632
|
-
<details><summary>With SSR
|
|
649
|
+
<details><summary>With SSR support</summary>
|
|
633
650
|
|
|
634
|
-
On the server, these data-binding tags
|
|
651
|
+
On the server, these data-binding tags will retain their place in the DOM while having their output rendered to their right in a text node.
|
|
635
652
|
|
|
636
|
-
The following: `<?{ 'Hello World' }?>` would thus give us: `<?{ 'Hello World' }?>Hello World`.
|
|
653
|
+
The following expression: `<?{ 'Hello World' }?>` would thus give us: `<?{ 'Hello World' }?>Hello World`.
|
|
637
654
|
|
|
638
|
-
But they also
|
|
655
|
+
But they also will need to remember the exact text node that they manage, so as to be able to re-establish relevant relationships on getting to the client. That information is automatically encoded as part of the declaration itself, and that brings us to having a typical server-rendered binding in the following form:
|
|
639
656
|
|
|
640
657
|
```html
|
|
641
658
|
<?{ 'Hello World'; [=11] }?>Hello World
|
|
642
659
|
```
|
|
643
660
|
|
|
644
|
-
Now that extra bit of information gets decoded and original relationships are forned again
|
|
661
|
+
Now, on getting to the client, that extra bit of information gets decoded, and original relationships are forned again. But the binding tag itself graciously disappears from the DOM, while the now "hydrated" text node continues to kick!
|
|
645
662
|
|
|
646
663
|
> Note: We know we're on the server when `window.webqit.env === 'server'`. This flag is automatically set by OOHTML's current SSR engine: [OOHTML-SSR](https://github.com/webqit/oohtml-ssr)
|
|
647
664
|
|
|
@@ -649,51 +666,51 @@ Now that extra bit of information gets decoded and original relationships are fo
|
|
|
649
666
|
|
|
650
667
|
### Inline Data-Binding
|
|
651
668
|
|
|
652
|
-
Here, we get the `
|
|
669
|
+
Here, we get the `expr` attribute for a declarative, neat, key/value data-binding syntax:
|
|
653
670
|
|
|
654
671
|
```html
|
|
655
|
-
<div
|
|
672
|
+
<div expr="<directive> <param>: <arg>;"></div>
|
|
656
673
|
```
|
|
657
674
|
|
|
658
675
|
**-->** *where*:
|
|
659
676
|
|
|
660
|
-
+ *`<directive>` is
|
|
677
|
+
+ *`<directive>` is a directive, which is always a symbol*
|
|
661
678
|
+ *`<param>` is the parameter being bound, which could be a CSS property, class name, attribute name, Structural Directive - depending on the givin directive*
|
|
662
679
|
+ *`<arg>` is the bound value or expression*
|
|
663
680
|
|
|
664
681
|
**-->** *which would give us the following for a CSS property*:
|
|
665
682
|
|
|
666
683
|
```html
|
|
667
|
-
<div
|
|
684
|
+
<div expr="& color:someColor; & backgroundColor:'red'"></div>
|
|
668
685
|
```
|
|
669
686
|
|
|
670
687
|
**-->** *without being space-sensitive*:
|
|
671
688
|
|
|
672
689
|
```html
|
|
673
|
-
<div
|
|
690
|
+
<div expr="& color:someColor; &backgroundColor: 'red'"></div>
|
|
674
691
|
```
|
|
675
692
|
|
|
676
693
|
**-->** *the rest of which can be seen below*:
|
|
677
694
|
|
|
678
695
|
| Directive | Type | Usage |
|
|
679
696
|
| :---- | :---- | :---- |
|
|
680
|
-
| `&` | CSS Property | `<div
|
|
681
|
-
| `%` | Class Name | `<div
|
|
682
|
-
| `~` | Attribute Name | `<a
|
|
683
|
-
| | Boolean Attribute | `<a
|
|
697
|
+
| `&` | CSS Property | `<div expr="& color:someColor; & backgroundColor:someBgColor;"></div>` |
|
|
698
|
+
| `%` | Class Name | `<div expr="% active:app.isActive; % expanded:app.isExpanded;"></div>` |
|
|
699
|
+
| `~` | Attribute Name | `<a expr="~ href:person.profileUrl+'#bio'; ~ title:'Click me';"></a>` |
|
|
700
|
+
| | Boolean Attribute | `<a expr="~ ?required:formField.required; ~ ?aria-checked: formField.checked"></a>` |
|
|
684
701
|
| `@` | Structural Directive: | *See below* |
|
|
685
|
-
| `@text` | Plain text content | `<span
|
|
686
|
-
| `@html` | Markup content | `<span
|
|
687
|
-
| `@items` | A list,
|
|
702
|
+
| `@text` | Plain text content | `<span expr="@text:firstName+' '+lastName;"></span>` |
|
|
703
|
+
| `@html` | Markup content | `<span expr="@html: '<i>'+firstName+'</i>';"></span>` |
|
|
704
|
+
| `@items` | A list, of the following format | `<declaration> <of\|in> <iterable> / <importRef>`<br>*See next two tables* |
|
|
688
705
|
|
|
689
706
|
<details><summary><code>For ... Of</code> Loops</summary>
|
|
690
707
|
|
|
691
708
|
| Idea | Usage |
|
|
692
709
|
| :---- | :---- |
|
|
693
|
-
| A `for...of` loop over an array/iterable | `<ul
|
|
694
|
-
| Same as above but with a `key` declaration | `<ul
|
|
695
|
-
| Same as above but with different variable names | `<ul
|
|
696
|
-
| Same as above but with a dynamic `importRef` | `<ul
|
|
710
|
+
| A `for...of` loop over an array/iterable | `<ul expr="@items: value of [1,2,3] / 'foo#fragment';"></ul>` |
|
|
711
|
+
| Same as above but with a `key` declaration | `<ul expr="@items: (value,key) of [1,2,3] / 'foo#fragment';"></ul>` |
|
|
712
|
+
| Same as above but with different variable names | `<ul expr="@items: (product,id) of store.products / 'foo#fragment';"></ul>` |
|
|
713
|
+
| Same as above but with a dynamic `importRef` | `<ul expr="@items: (product,id) of store.products / store.importRef;"></ul>` |
|
|
697
714
|
|
|
698
715
|
</details>
|
|
699
716
|
|
|
@@ -701,14 +718,14 @@ Here, we get the `binding` attribute for a declarative and neat, key/value data-
|
|
|
701
718
|
|
|
702
719
|
| Idea | Usage |
|
|
703
720
|
| :---- | :---- |
|
|
704
|
-
| A `for...in` loop over an object | `<ul
|
|
705
|
-
| Same as above but with a `value` and `index` declaration | `<ul
|
|
721
|
+
| A `for...in` loop over an object | `<ul expr="@items: key in {a:1,b:2} / 'foo#fragment';"></ul>` |
|
|
722
|
+
| Same as above but with a `value` and `index` declaration | `<ul expr="@items: (key,value,index) in {a:1, b:2} / 'foo#fragment';"></ul>` |
|
|
706
723
|
|
|
707
724
|
</details>
|
|
708
725
|
|
|
709
|
-
<details><summary>
|
|
726
|
+
<details><summary>Resolution details</summary>
|
|
710
727
|
|
|
711
|
-
Here, JavaScript references are resolved from the closest node up the document
|
|
728
|
+
Here, JavaScript references are resolved from the closest node up the document tree that exposes a corresponding *binding* on its Bindings API ([discussed below](#bindings-api)). Thus, for the above CSS bindings, our underlying data structure could be something like the below:
|
|
712
729
|
|
|
713
730
|
```js
|
|
714
731
|
document.bind({ someColor: 'green', someBgColor: 'yellow' });
|
|
@@ -722,7 +739,7 @@ document: { someColor: 'green', someBgColor: 'yellow' }
|
|
|
722
739
|
└── body: { someBgColor: 'silver' }
|
|
723
740
|
```
|
|
724
741
|
|
|
725
|
-
|
|
742
|
+
Now, the `someBgColor` reference remains bound to the `someBgColor` *binding* on the `<body>` element until the meaning of "closest node" changes again:
|
|
726
743
|
|
|
727
744
|
```js
|
|
728
745
|
delete document.body.bindings.someBgColor;
|
|
@@ -734,7 +751,7 @@ While the `someColor` reference remains bound to the `someColor` *binding* on th
|
|
|
734
751
|
document.body.bindings.someColor = 'brown';
|
|
735
752
|
```
|
|
736
753
|
|
|
737
|
-
|
|
754
|
+
</details>
|
|
738
755
|
|
|
739
756
|
<details><summary>All in realtime</summary>
|
|
740
757
|
|
|
@@ -742,9 +759,9 @@ Bindings are resolved in realtime! And in fact, for lists, in-place mutations -
|
|
|
742
759
|
|
|
743
760
|
</details>
|
|
744
761
|
|
|
745
|
-
<details><summary>With SSR
|
|
762
|
+
<details><summary>With SSR support</summary>
|
|
746
763
|
|
|
747
|
-
For lists, generated item elements are automatically assigned a corresponding
|
|
764
|
+
For lists, generated item elements are automatically assigned a corresponding key with a `data-key` attribute! This helps in remapping generated item nodes to their respective entry in *iteratee* during a rerendering or during hydration.
|
|
748
765
|
|
|
749
766
|
</details>
|
|
750
767
|
|
|
@@ -752,7 +769,7 @@ For lists, generated item elements are automatically assigned a corresponding in
|
|
|
752
769
|
|
|
753
770
|
We often still need to write more serious reactive logic on the UI than a declarative data-binding language can provide for. But we shouldn't need to reach for special tooling or some "serious" programming paradigm on top of JavaScript.
|
|
754
771
|
|
|
755
|
-
Here, from the same `<script>` element we already write, we get a direct upgrade path to reactive programming in just an attribute: `quantum`:
|
|
772
|
+
Here, from the same `<script>` element we already write, we get a direct upgrade path to reactive programming in just the addition of an attribute: `quantum`:
|
|
756
773
|
|
|
757
774
|
```html
|
|
758
775
|
<script quantum>
|
|
@@ -768,7 +785,7 @@ Here, from the same `<script>` element we already write, we get a direct upgrade
|
|
|
768
785
|
</script>
|
|
769
786
|
```
|
|
770
787
|
|
|
771
|
-
**-->** *which adds up really well with the
|
|
788
|
+
**-->** *which gives us fine-grained reactivity on top of literal JavaScript syntax; and which adds up really well with the `scoped` attribute*:
|
|
772
789
|
|
|
773
790
|
```html
|
|
774
791
|
<main>
|
|
@@ -792,7 +809,7 @@ Here, from the same `<script>` element we already write, we get a direct upgrade
|
|
|
792
809
|
</main>
|
|
793
810
|
```
|
|
794
811
|
|
|
795
|
-
**-->** *with content being whatever you normally would write in a `<script>` element,
|
|
812
|
+
**-->** *with content being whatever you normally would write in a `<script>` element, minus the "manual" work for reactivity*:
|
|
796
813
|
|
|
797
814
|
```html
|
|
798
815
|
<main>
|
|
@@ -810,7 +827,7 @@ Here, from the same `<script>` element we already write, we get a direct upgrade
|
|
|
810
827
|
</main>
|
|
811
828
|
```
|
|
812
829
|
|
|
813
|
-
**-->** *within which
|
|
830
|
+
**-->** *within which dynamic application state/data, and even things like the Namespace API above, fit seamlessly*:
|
|
814
831
|
|
|
815
832
|
```html
|
|
816
833
|
<main namespace>
|
|
@@ -843,13 +860,13 @@ const removeButton = () => {
|
|
|
843
860
|
};
|
|
844
861
|
```
|
|
845
862
|
|
|
846
|
-
<details><summary>
|
|
863
|
+
<details><summary>Learn more</summary>
|
|
847
864
|
|
|
848
865
|
It's Imperative Reactive Programming ([IRP](https://en.wikipedia.org/wiki/Reactive_programming#Imperative)) right there and it's the [Quantum](https://github.com/webqit/quantum-js) runtime extension to JavaScript!
|
|
849
866
|
|
|
850
867
|
Here, the runtime executes your code in a special execution mode that gets literal JavaScript expressions to statically reflect changes. This makes a lot of things possible on the UI! The [Quantum JS](https://github.com/webqit/quantum-js) documentation has a detailed run down.
|
|
851
868
|
|
|
852
|
-
Now, in each case above, reactivity terminates on script's removal from the DOM
|
|
869
|
+
Now, in each case above, reactivity terminates on script's removal from the DOM or via a programmatic approach:
|
|
853
870
|
|
|
854
871
|
```js
|
|
855
872
|
const script = document.querySelector('script[quantum]');
|
|
@@ -863,115 +880,397 @@ But while that is automatic, DOM event handlers bound via `addEventListener()` w
|
|
|
863
880
|
|
|
864
881
|
## Data Plumbing
|
|
865
882
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
<!--
|
|
869
|
-
The last set of features covers the concept of "state", "bindings", and "reactivity" for those objects at the DOM level - in the most exciting form of the terms and as an upgrade path! This comes factored into the design as something intrinsic to the problem.
|
|
883
|
+
Components often need to manage, or be managed by, dynamic data. That could get pretty problematic and messy if all of that should go on DOM nodes as direct properties:
|
|
870
884
|
|
|
871
|
-
|
|
885
|
+
<details><summary>Example</summary>
|
|
872
886
|
|
|
873
887
|
```js
|
|
874
|
-
|
|
875
|
-
|
|
888
|
+
// Inside a custom element
|
|
889
|
+
connectedCallback() {
|
|
890
|
+
this.prop1 = 1;
|
|
891
|
+
this.prop2 = 2;
|
|
892
|
+
this.prop3 = 3;
|
|
893
|
+
this.style = 'tall-dark'; // ??? - conflict with the standard HTMLElement: style property
|
|
876
894
|
}
|
|
877
895
|
```
|
|
878
896
|
|
|
879
897
|
```js
|
|
880
|
-
|
|
881
|
-
|
|
898
|
+
// Outside the component
|
|
899
|
+
const node = document.querySelector('my-element');
|
|
900
|
+
node.prop1 = 1;
|
|
901
|
+
node.prop2 = 2;
|
|
902
|
+
node.prop3 = 3;
|
|
903
|
+
node.normalize = true; // ??? - conflict with the standard Node: normalize() method
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
</details>
|
|
907
|
+
|
|
908
|
+
This calls for a decent API and some data-flow mechanism!
|
|
909
|
+
|
|
910
|
+
### The Bindings API
|
|
911
|
+
|
|
912
|
+
A place to maintain state need not be a complex state machine! Here, that comes as a simple, read/write, data object exposed on the document object and on DOM elements as a readonly `bindings` property. This is the Bindings API.
|
|
913
|
+
|
|
914
|
+
**-->** *it's an ordinary JavaScript object that can be read and mutated*:
|
|
915
|
+
|
|
916
|
+
```js
|
|
917
|
+
// Read
|
|
918
|
+
console.log(document.bindings); // {}
|
|
919
|
+
// Modify
|
|
920
|
+
document.bindings.app = { title: 'Demo App' };
|
|
921
|
+
console.log(document.bindings.app); // { title: 'Demo App' }
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
```js
|
|
925
|
+
const node = document.querySelector('div');
|
|
926
|
+
// Read
|
|
927
|
+
console.log(node.bindings); // {}
|
|
928
|
+
// Modify
|
|
929
|
+
node.bindings.style = 'tall-dark';
|
|
930
|
+
node.bindings.normalize = true;
|
|
882
931
|
```
|
|
883
932
|
|
|
933
|
+
**-->** *with a complementary `bind()` method that lets us make mutations in batch*:
|
|
934
|
+
|
|
884
935
|
```js
|
|
885
|
-
|
|
936
|
+
// ------------
|
|
937
|
+
// Set multiple properties
|
|
938
|
+
document.bind({ name: 'James Boye', cool: '100%', app: { title: 'Demo App' } });
|
|
939
|
+
|
|
940
|
+
// ------------
|
|
941
|
+
// Replace existing properties with a new set
|
|
942
|
+
document.bind({ signedIn: false, hot: '100%' });
|
|
943
|
+
// Inspect
|
|
944
|
+
console.log(document.bindings); // { signedIn: false, hot: '100%' }
|
|
945
|
+
|
|
946
|
+
// ------------
|
|
947
|
+
// Merge a new set of properties with existing
|
|
948
|
+
document.bind({ name: 'James Boye', cool: '100%' }, { merge: true });
|
|
949
|
+
// Inspect
|
|
950
|
+
console.log(document.bindings); // { signedIn: false, hot: '100%', name: 'James Boye', cool: '100%' }
|
|
886
951
|
```
|
|
887
952
|
|
|
953
|
+
**-->** *which also provides an easy way to pass data down a component tree*:
|
|
954
|
+
|
|
888
955
|
```js
|
|
889
|
-
|
|
956
|
+
// Inside a custom element
|
|
957
|
+
connectedCallback() {
|
|
958
|
+
this.child1.bind(this.bindings.child1Data);
|
|
959
|
+
this.child2.bind(this.bindings.child2Data);
|
|
960
|
+
}
|
|
890
961
|
```
|
|
891
962
|
|
|
892
|
-
|
|
963
|
+
**-->** *and with the Observer API in the picture for reactivity*:
|
|
893
964
|
|
|
894
965
|
```js
|
|
895
|
-
|
|
896
|
-
|
|
966
|
+
Observer.observe(document.bindings, mutations => {
|
|
967
|
+
mutations.forEach(mutation => console.log(mutation));
|
|
968
|
+
});
|
|
969
|
+
```
|
|
897
970
|
|
|
898
|
-
|
|
899
|
-
|
|
971
|
+
```js
|
|
972
|
+
// Inside a custom element
|
|
973
|
+
connectedCallback() {
|
|
974
|
+
Observer.observe(this.bindings, 'style', e => {
|
|
975
|
+
// Compunonent should magically change style
|
|
976
|
+
console.log(e.value);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
```
|
|
900
980
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
981
|
+
```js
|
|
982
|
+
const node = document.querySelector('my-element');
|
|
983
|
+
node.bindings.style = 'tall-dark';
|
|
904
984
|
```
|
|
905
985
|
|
|
986
|
+
<details><summary>Details</summary>
|
|
987
|
+
|
|
988
|
+
In the current OOHTML implementation, the `document.bindings` and `Element.prototype.bindings` APIs are implemented as proxies over their actual bindings interface to enable some interface-level reactivity. This lets us have reactivity over literal property assignments and deletions on these interfaces:
|
|
989
|
+
|
|
906
990
|
```js
|
|
907
|
-
|
|
908
|
-
|
|
991
|
+
node.bindings.style = 'tall-dark'; // Reactive assignment
|
|
992
|
+
delete node.bindings.style; // Reactive deletion
|
|
993
|
+
```
|
|
909
994
|
|
|
910
|
-
|
|
911
|
-
element.bindings.isCollapsed = true;
|
|
995
|
+
For mutations at a deeper level to be reactive, the corresponding Observer API method must be used:
|
|
912
996
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
Observer.
|
|
997
|
+
```js
|
|
998
|
+
Observer.set(document.bindings.app, 'title', 'Demo App!!!');
|
|
999
|
+
Observer.deleteProperty(document.bindings.app, 'title');
|
|
916
1000
|
```
|
|
917
1001
|
|
|
918
|
-
|
|
1002
|
+
</details>
|
|
919
1003
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1004
|
+
### The Context API
|
|
1005
|
+
|
|
1006
|
+
A complex hierarchy of objects will often call for more than the normal top-down flow of data that the Bindings API facilitates. A child may require the ability to look up the component tree to directly access specific data, or in other words, get data from "context". This is where a Context API comes in.
|
|
1007
|
+
|
|
1008
|
+
Interestingly, the Context API is the resolution mechanism behind HTML Imports and Data Binding in OOHTML!
|
|
1009
|
+
|
|
1010
|
+
Here, we simply leverage the DOM's existing event system to fire a "request" event and let an arbitrary "provider" in context fulfill the request. This becomes very simple with the Context API which is exposed on the document object and on element instances as a readonly `contexts` property.
|
|
1011
|
+
|
|
1012
|
+
**-->** *with the `contexts.request()` method for firing requests*:
|
|
1013
|
+
|
|
1014
|
+
```js
|
|
1015
|
+
// ------------
|
|
1016
|
+
// Get an arbitrary
|
|
1017
|
+
const node = document.querySelector('my-element');
|
|
1018
|
+
|
|
1019
|
+
// ------------
|
|
1020
|
+
// Prepare and fire request event
|
|
1021
|
+
const requestParams = { kind: 'html-imports', detail: '/foo#fragment1' };
|
|
1022
|
+
const contextResponse = node.contexts.request(requestParams);
|
|
923
1023
|
|
|
924
|
-
|
|
925
|
-
|
|
1024
|
+
// ------------
|
|
1025
|
+
// Handle response
|
|
1026
|
+
console.log(contextResponse.value); // It works!
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
**-->** *and the `contexts.attach()` and `contexts.detach()` methods for attaching/detaching providers at arbitrary levels in the DOM tree*:
|
|
926
1030
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1031
|
+
```js
|
|
1032
|
+
// ------------
|
|
1033
|
+
// Define a CustomContext class
|
|
1034
|
+
class FakeImportsContext extends DOMContext {
|
|
1035
|
+
static kind = 'html-imports';
|
|
1036
|
+
handle(event) {
|
|
1037
|
+
console.log(event.detail); // '/foo#fragment1'
|
|
1038
|
+
event.respondWith('It works!');
|
|
930
1039
|
}
|
|
931
|
-
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ------------
|
|
1043
|
+
// Instantiate and attach to a node
|
|
1044
|
+
const fakeImportsContext = new FakeImportsContext;
|
|
1045
|
+
document.contexts.attach(fakeImportsContext);
|
|
1046
|
+
|
|
1047
|
+
// ------------
|
|
1048
|
+
// Detach anytime
|
|
1049
|
+
document.contexts.detach(fakeImportsContext);
|
|
932
1050
|
```
|
|
933
1051
|
|
|
1052
|
+
<details><summary>Details</summary>
|
|
1053
|
+
|
|
1054
|
+
In the current OOHTML implementation, the Context API interfaces are exposed via the global `webqit` object:
|
|
1055
|
+
|
|
934
1056
|
```js
|
|
935
|
-
|
|
1057
|
+
const { DOMContext, DOMContextRequestEvent, DOMContextResponse } = window.webqit;
|
|
936
1058
|
```
|
|
937
1059
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
console.log(this) // div
|
|
1060
|
+
In addition...
|
|
1061
|
+
|
|
1062
|
+
+ a provider will automatically adopt the `contextname`, if any, of its host element:
|
|
942
1063
|
|
|
943
|
-
|
|
1064
|
+
```html
|
|
1065
|
+
<div contextname="context1"></div>
|
|
1066
|
+
```
|
|
944
1067
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1068
|
+
```js
|
|
1069
|
+
// Instantiate and attach to a node
|
|
1070
|
+
const host = document.querySelector('div');
|
|
1071
|
+
const fakeImportsContext = new FakeImportsContext;
|
|
1072
|
+
host.contexts.attach(fakeImportsContext);
|
|
1073
|
+
// Inspect name
|
|
1074
|
+
console.log(fakeImportsContext.name); // context1
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
...which a request could target:
|
|
1078
|
+
|
|
1079
|
+
```js
|
|
1080
|
+
const requestParams = { kind: FakeImportsContext.kind, targetContext: 'context1', detail: '/foo#fragment1' };
|
|
1081
|
+
const contextResponse = node.contexts.request(requestParams);
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
+ and providers of same kind could be differentiated by an extra "detail" - an arbitrary value passed to the constructor:
|
|
1085
|
+
|
|
1086
|
+
```js
|
|
1087
|
+
const fakeImportsContext = new FakeImportsContext('lorem');
|
|
1088
|
+
console.log(fakeImportsContext.detail); // lorem
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
+ and a provider could indicate to manually match requests where the defualt "kind" matching, and optional "targetContext" matching, don't suffice:
|
|
1092
|
+
|
|
1093
|
+
```js
|
|
1094
|
+
// Define a CustomContext class
|
|
1095
|
+
class CustomContext extends DOMContext {
|
|
1096
|
+
static kind = 'html-imports';
|
|
1097
|
+
matchEvent(event) {
|
|
1098
|
+
// The default request matching algorithm + "detail" matching
|
|
1099
|
+
return super.matchEvent() && event.detail === this.detail;
|
|
1100
|
+
}
|
|
1101
|
+
handle(event) {
|
|
1102
|
+
console.log(event.detail);
|
|
1103
|
+
event.respondWith('It works!');
|
|
1104
|
+
}
|
|
948
1105
|
}
|
|
949
|
-
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
+ and a request could choose to stay subscribed to changes on the requested data; the request would simply add a `live` flag:
|
|
1109
|
+
|
|
1110
|
+
```js
|
|
1111
|
+
// Set the "live" flag
|
|
1112
|
+
const requestParams = { kind: FakeImportsContext.kind, targetContext: 'context1', detail: '/foo#fragment1', live: true };
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
...then stay alert to said updates on the returned `DOMContextResponse` object or specify a callback function at request time:
|
|
1116
|
+
|
|
1117
|
+
```js
|
|
1118
|
+
// Handle response without a callback
|
|
1119
|
+
const contextResponse = node.contexts.request(requestParams);
|
|
1120
|
+
console.log(contextResponse.value); // It works!
|
|
1121
|
+
Observer.observe(contextResponse, 'value', e => {
|
|
1122
|
+
console.log(e.value); // It works live!
|
|
1123
|
+
});
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
```js
|
|
1127
|
+
// Handle response with a callback
|
|
1128
|
+
node.contexts.request(requestParams, value => {
|
|
1129
|
+
console.log(value);
|
|
1130
|
+
// It works!
|
|
1131
|
+
// It works live!
|
|
1132
|
+
});
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
...while provider simply checks for the `event.live` flag and keep the updates flowing:
|
|
1136
|
+
|
|
1137
|
+
```js
|
|
1138
|
+
// Define a CustomContext class
|
|
1139
|
+
class CustomContext extends DOMContext {
|
|
1140
|
+
static kind = 'html-imports';
|
|
1141
|
+
handle(event) {
|
|
1142
|
+
event.respondWith('It works!');
|
|
1143
|
+
if (event.live) {
|
|
1144
|
+
setTimeout(() => {
|
|
1145
|
+
event.respondWith('It works live!');
|
|
1146
|
+
}, 5000);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
...and optionally implements a `subscribed` and `unsubscribed` lifecycle hook for when a "live" event enters and leaves the instance:
|
|
1153
|
+
|
|
1154
|
+
```js
|
|
1155
|
+
// Define a CustomContext class
|
|
1156
|
+
class CustomContext extends DOMContext {
|
|
1157
|
+
static kind = 'html-imports';
|
|
1158
|
+
subscribed(event) {
|
|
1159
|
+
console.log(this.subscriptions.size); // 1
|
|
1160
|
+
}
|
|
1161
|
+
unsubscribed(event) {
|
|
1162
|
+
console.log(this.subscriptions.size); // 0
|
|
1163
|
+
}
|
|
1164
|
+
handle(event) {
|
|
1165
|
+
event.respondWith('It works!');
|
|
1166
|
+
if (event.live) {
|
|
1167
|
+
setTimeout(() => {
|
|
1168
|
+
event.respondWith('It works live!');
|
|
1169
|
+
}, 5000);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
+ live requests are terminated via the returned `DOMContextResponse` object...
|
|
1176
|
+
|
|
1177
|
+
```js
|
|
1178
|
+
contextResponse.abort();
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
...or via an initially specified custom `AbortSignal`:
|
|
1182
|
+
|
|
1183
|
+
```js
|
|
1184
|
+
// Add a signal to the original request
|
|
1185
|
+
const abortController = new AbortController;
|
|
1186
|
+
const requestParams = { kind: FakeImportsContext.kind, targetContext: 'context1', detail: '/foo#fragment1', live: true, signal: abortController.signal };
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
```js
|
|
1190
|
+
abortController.abort(); // Which also calls contextResponse.abort();
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
+ now, when a node in a provider's subtree is suddenly attached an identical provider, any live requests the super provider may be serving are automatically "claimed" by the sub provider:
|
|
1194
|
+
|
|
1195
|
+
```js
|
|
1196
|
+
document: // 'fake-provider' here
|
|
1197
|
+
└── html
|
|
1198
|
+
├── head
|
|
1199
|
+
└── body: // 'fake-provider' here. Our request above is now served from here.
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
And when the sub provider is suddenly detached from said node, any live requests it may have served are automatically hoisted back to super provider.
|
|
1203
|
+
|
|
1204
|
+
```js
|
|
1205
|
+
document: // 'fake-provider' here. Our request above is now served from here.
|
|
1206
|
+
└── html
|
|
1207
|
+
├── head
|
|
1208
|
+
└── body:
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
While, in all, saving the requesting code that "admin" work!
|
|
1212
|
+
|
|
1213
|
+
</details>
|
|
1214
|
+
|
|
1215
|
+
**-->** *all of which gives us the imperative equivalent of context-based declarative features like HTMLImports and Data Binding*:
|
|
1216
|
+
|
|
1217
|
+
```html
|
|
1218
|
+
<div contextname="vendor1">
|
|
1219
|
+
<div contextname="vendor2">
|
|
1220
|
+
...
|
|
1221
|
+
|
|
1222
|
+
<my-element>
|
|
1223
|
+
<!-- Declarative import -->
|
|
1224
|
+
<import ref="@vendor1/foo#fragment1"></import>
|
|
1225
|
+
<!-- Declarative Data Binding -->
|
|
1226
|
+
<?{ @vendor2.app.title }?>
|
|
1227
|
+
</my-element>
|
|
1228
|
+
|
|
1229
|
+
...
|
|
1230
|
+
</div>
|
|
950
1231
|
</div>
|
|
951
1232
|
```
|
|
952
1233
|
|
|
953
1234
|
```js
|
|
954
|
-
|
|
1235
|
+
// ------------
|
|
1236
|
+
// Equivalent import() approach
|
|
1237
|
+
const contextResponse = myElement.import('@vendor1/foo#fragment1');
|
|
1238
|
+
|
|
1239
|
+
// ------------
|
|
1240
|
+
// Equivalent Context API approach
|
|
1241
|
+
const requestParams = { kind: 'html-imports', targetContext: 'vendor1', detail: 'foo#fragment1' };
|
|
1242
|
+
const contextResponse = myElement.contexts.request(requestParams);
|
|
1243
|
+
|
|
1244
|
+
// ------------
|
|
1245
|
+
// Handle response
|
|
1246
|
+
console.log(contextResponse.value);
|
|
955
1247
|
```
|
|
956
1248
|
|
|
957
|
-
|
|
1249
|
+
```js
|
|
1250
|
+
// ------------
|
|
1251
|
+
// Context API request for bindings
|
|
1252
|
+
const requestParams = { kind: 'bindings', targetContext: 'vendor2', detail: 'app' };
|
|
1253
|
+
const contextResponse = myElement.contexts.request(requestParams);
|
|
958
1254
|
|
|
959
|
-
|
|
1255
|
+
// ------------
|
|
1256
|
+
// Handle response
|
|
1257
|
+
console.log(contextResponse.value.title);
|
|
1258
|
+
```
|
|
960
1259
|
|
|
961
1260
|
## Polyfill
|
|
962
1261
|
|
|
963
|
-
OOHTML is being developed as something to be used today—via a polyfill. This is an
|
|
1262
|
+
OOHTML is being developed as something to be used today—via a polyfill. This is an intentional effort that's helping to ensure that the project evolves through a practice-driven process.
|
|
964
1263
|
|
|
965
1264
|
<details><summary>Load from a CDN<br>
|
|
966
|
-
└───────── <a href="https://bundlephobia.com/result?p=@webqit/oohtml"><img align="right" src="https://img.shields.io/
|
|
1265
|
+
└───────── <a href="https://bundlephobia.com/result?p=@webqit/oohtml"><img align="right" src="https://img.shields.io/badge/21.7%20kB-black"></a></summary>
|
|
967
1266
|
|
|
968
1267
|
```html
|
|
969
|
-
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
|
|
1268
|
+
<script src="https://unpkg.com/@webqit/oohtml/dist/main.lite.js"></script>
|
|
970
1269
|
```
|
|
971
1270
|
|
|
972
1271
|
└ This is to be placed early on in the document and should be a classic script without any `defer` or `async` directives!
|
|
973
1272
|
|
|
974
|
-
└ For the Scoped Styles feature, you'd
|
|
1273
|
+
└ For the Scoped Styles feature, you'd need an external polyfill like the [samthor/scoped](https://github.com/samthor/scoped) polyfill (more details below):
|
|
975
1274
|
|
|
976
1275
|
```html
|
|
977
1276
|
<head>
|
|
@@ -985,18 +1284,19 @@ OOHTML is being developed as something to be used today—via a polyfill. This i
|
|
|
985
1284
|
└───────── <a href="https://npmjs.com/package/@webqit/oohtml"><img align="right" src="https://img.shields.io/npm/v/@webqit/oohtml?style=flat&label=&colorB=black"></a></summary>
|
|
986
1285
|
|
|
987
1286
|
```bash
|
|
988
|
-
npm i @webqit/oohtml
|
|
1287
|
+
npm i @webqit/oohtml @webqit/quantum-js
|
|
989
1288
|
```
|
|
990
1289
|
|
|
991
1290
|
```js
|
|
992
1291
|
// Import
|
|
1292
|
+
import * as Quantum from '@webqit/quantum-js/lite'; // Or from '@webqit/quantum-js'; See implementation notes below
|
|
993
1293
|
import init from '@webqit/oohtml';
|
|
994
1294
|
|
|
995
1295
|
// Initialize the lib
|
|
996
|
-
init.call(
|
|
1296
|
+
init.call(window, Quantum[, options = {}]);
|
|
997
1297
|
```
|
|
998
1298
|
|
|
999
|
-
└ To use the polyfill on server-side DOM instances as made possible by libraries like [jsdom](https://github.com/jsdom/jsdom), simply install and initialize the library
|
|
1299
|
+
└ To use the polyfill on server-side DOM instances as made possible by libraries like [jsdom](https://github.com/jsdom/jsdom), simply install and initialize the library with the DOM instance as above.
|
|
1000
1300
|
|
|
1001
1301
|
└ But all things "SSR" for OOHTML are best left to the [`@webqit/oohtml-ssr`](https://github.com/webqit/oohtml-ssr) package!
|
|
1002
1302
|
|
|
@@ -1004,24 +1304,36 @@ init.call( window[, options = {} ]);
|
|
|
1004
1304
|
|
|
1005
1305
|
<details><summary>Extended usage concepts</summary>
|
|
1006
1306
|
|
|
1007
|
-
If you'll be going ahead to build a real app
|
|
1307
|
+
If you'll be going ahead to build a real app with OOHTML, you may want to consider also using:
|
|
1008
1308
|
|
|
1009
1309
|
+ the [`@webqit/oohtml-cli`](https://github.com/webqit/oohtml-cli) package for operating a file-based templating system.
|
|
1010
1310
|
|
|
1011
|
-
+ the modest, OOHTML-based [Webflo](https://github.com/webqit/webflo) framework to greatly streamline your
|
|
1311
|
+
+ the modest, OOHTML-based [Webflo](https://github.com/webqit/webflo) framework to greatly streamline your workflow!
|
|
1012
1312
|
|
|
1013
1313
|
</details>
|
|
1014
1314
|
|
|
1015
1315
|
<details><summary>Implementation Notes</summary>
|
|
1016
1316
|
|
|
1317
|
+
+ **Scoped/Quantum Scripts**. This feature is an extension of [Quantum JS](https://github.com/webqit/quantum-js). While the main OOHTML build is based on the main Quantum JS APIs, a companion "OOHTML Lite" build is also available based on the [Quantum JS Lite](https://github.com/webqit/quantum-js#quantum-js-lite) edition. The trade-off is in the execution timing of `<script quantum></script>` and `<script scoped></script>` elements: being "synchronous/blocking" with the former, and "asynchronous/non-blocking" with the latter! (See [`async`/`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attributes).)
|
|
1318
|
+
|
|
1319
|
+
Of the two, the "OOHTML Lite" edition is the recommend option (as used above) for faster load times unless there's a requirment to have classic scripts follow their [native synchronous timing](https://html.spec.whatwg.org/multipage/parsing.html#scripts-that-modify-the-page-as-it-is-being-parsed), in which case you'd need the main OOHTML build:
|
|
1320
|
+
|
|
1321
|
+
```html
|
|
1322
|
+
<head>
|
|
1323
|
+
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
|
|
1324
|
+
</head>
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
└─ <a href="https://bundlephobia.com/result?p=@webqit/oohtml"><img align="right" src="https://img.shields.io/bundlephobia/minzip/@webqit/oohtml?label=&style=flat&colorB=black"></a>
|
|
1328
|
+
|
|
1017
1329
|
+ **Loading Requirements**. As specified above, the OOHTML script tag is to be placed early on in the document and should be a classic script without any `defer` or `async` directives!
|
|
1018
1330
|
|
|
1019
|
-
If you must load the script "async", one little trade-off
|
|
1331
|
+
If you must load the script "async", one little trade-off would have to be made for `<script scoped>` and `<script quantum>` elements to have them ignored by the browser until the polyfill comes picking them up: *employing a custom MIME type in place of the standard `text/javascript` and `module` types*, in which case, a `<meta name="scoped-js">` element is used to configure the polyfill to honor the custom MIME type:
|
|
1020
1332
|
|
|
1021
1333
|
```html
|
|
1022
1334
|
<head>
|
|
1023
1335
|
<meta name="scoped-js" content="script.mimeType=some-mime">
|
|
1024
|
-
<script async src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
|
|
1336
|
+
<script async src="https://unpkg.com/@webqit/oohtml/dist/main.lite.js"></script>
|
|
1025
1337
|
</head>
|
|
1026
1338
|
<body>
|
|
1027
1339
|
<script type="some-mime" scoped>
|
|
@@ -1042,16 +1354,6 @@ If you'll be going ahead to build a real app to see OOHTML in action, you may wa
|
|
|
1042
1354
|
|
|
1043
1355
|
...still gives the `window` object in the console.
|
|
1044
1356
|
|
|
1045
|
-
+ **Scoped/Quantum Scripts**. This feature is an extension of [Quantum JS](https://github.com/webqit/quantum-js) and the default OOHTML build is based on the [Quantum JS Lite APIs](https://github.com/webqit/quantum-js#quantum-js-lite). Now, while Quantum JS Lite yields faster load times, it also means that `<script quantum></script>` and `<script scoped></script>` elements are parsed "asynchronously", in the same timing as `<script type="module"></script>`!
|
|
1046
|
-
|
|
1047
|
-
This timing works perfectly generally, but if you have a requirment to have classic scripts follow their [native synchronous timing](https://html.spec.whatwg.org/multipage/parsing.html#scripts-that-modify-the-page-as-it-is-being-parsed), then you'd need to use the *realtime* OOHTML build:
|
|
1048
|
-
|
|
1049
|
-
```html
|
|
1050
|
-
<head>
|
|
1051
|
-
<script src="https://unpkg.com/@webqit/oohtml/dist/main.realtime.js"></script>
|
|
1052
|
-
</head>
|
|
1053
|
-
```
|
|
1054
|
-
|
|
1055
1357
|
+ **Scoped CSS**. This feature is only in "concept" implementation and doesn't work right now as is. The current implementation simply wraps `<style scoped>` blocks in an `@scope {}` block - which itself isn't supported in any browser. To try this "concept" implementation, set the `style.strategy` config to `@scope`:
|
|
1056
1358
|
|
|
1057
1359
|
```html
|
|
@@ -1100,7 +1402,7 @@ If you'll be going ahead to build a real app to see OOHTML in action, you may wa
|
|
|
1100
1402
|
|
|
1101
1403
|
## Examples
|
|
1102
1404
|
|
|
1103
|
-
Here are a few examples in the wide range of use cases these features cover. While we'll demonstrate the most basic
|
|
1405
|
+
Here are a few examples in the wide range of use cases these features cover. While we'll demonstrate the most basic form of these scenarios, it takes roughly the same principles to build an intricate form and a highly interactive UI.
|
|
1104
1406
|
|
|
1105
1407
|
<details><summary>Example 1: <i>Single Page Application</i><br>
|
|
1106
1408
|
└───────── </summary>
|
|
@@ -1295,11 +1597,11 @@ The following is a hypothetical list page!
|
|
|
1295
1597
|
|
|
1296
1598
|
<!-- The "items" template -->
|
|
1297
1599
|
<template def="item" scoped>
|
|
1298
|
-
<li><a
|
|
1600
|
+
<li><a expr="~href: '/animals#'+name;"><?{ index+': '+name }?></a></li>
|
|
1299
1601
|
</template>
|
|
1300
1602
|
|
|
1301
1603
|
<!-- The loop -->
|
|
1302
|
-
<ul
|
|
1604
|
+
<ul expr="@items: (name,index) of ['dog','cat','ram'] / 'item';"></ul>
|
|
1303
1605
|
|
|
1304
1606
|
</section>
|
|
1305
1607
|
```
|