@webqit/oohtml 3.0.1-9 → 3.1.2
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 +532 -229
- 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
|
|
@@ -201,7 +203,7 @@ Here, we get the `def` attribute for defining those - either as whole *module* o
|
|
|
201
203
|
|
|
202
204
|
We shouldn't need a different mechanism to work with remote content.
|
|
203
205
|
|
|
204
|
-
Here, we get
|
|
206
|
+
Here, we get remote-loading modules using the `src` attribute:
|
|
205
207
|
|
|
206
208
|
```html
|
|
207
209
|
<template def="foo" src="/foo.html"></template>
|
|
@@ -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 borrow from how `src` works in elements like `<img>`; terminating with either a `load` or an `error` event*:
|
|
225
226
|
|
|
226
227
|
```js
|
|
227
228
|
foo.addEventListener('load', loadCallback);
|
|
@@ -230,9 +231,9 @@ foo.addEventListener('error', errorCallback);
|
|
|
230
231
|
|
|
231
232
|
### Declarative Module Imports
|
|
232
233
|
|
|
233
|
-
|
|
234
|
+
HTML snippets should be reusable in "HTML"!
|
|
234
235
|
|
|
235
|
-
Here, we get an `<import>` element that lets us do that
|
|
236
|
+
Here, we get an `<import>` element that lets us do just that:
|
|
236
237
|
|
|
237
238
|
```html
|
|
238
239
|
<body>
|
|
@@ -250,19 +251,21 @@ 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 bindings that are highly 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
|
|
259
|
-
+ the previously slotted contents have been programmatically removed and slot is empty.
|
|
261
|
+
+ the above changes resolve to no imports.
|
|
262
|
+
+ the previously slotted contents have *all* been programmatically removed and slot is empty.
|
|
260
263
|
|
|
261
264
|
</details>
|
|
262
265
|
|
|
263
|
-
<details><summary>With SSR
|
|
266
|
+
<details><summary>With SSR support</summary>
|
|
264
267
|
|
|
265
|
-
On the server, these `<import>` elements
|
|
268
|
+
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
269
|
|
|
267
270
|
The above resolved imports would thus give us something like:
|
|
268
271
|
|
|
@@ -275,23 +278,23 @@ The above resolved imports would thus give us something like:
|
|
|
275
278
|
</body>
|
|
276
279
|
```
|
|
277
280
|
|
|
278
|
-
But they also
|
|
281
|
+
But they also will need to remember the exact imported nodes that they manage so as to be able to re-establish relevant relationships on getting to the client. This information is automatically encoded as part of the serialised element itself, in something like:
|
|
279
282
|
|
|
280
283
|
```html
|
|
281
284
|
<!--<import ref="/foo/nested#fragment2" nodecount="1"></import>-->
|
|
282
285
|
```
|
|
283
286
|
|
|
284
|
-
Now that extra bit of information
|
|
287
|
+
Now, on getting to the client and getting "hydrated" back into an `<import>` element, that extra bit of information is 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
288
|
|
|
286
289
|
> 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
290
|
|
|
288
291
|
</details>
|
|
289
292
|
|
|
290
|
-
###
|
|
293
|
+
### Imperative Module Imports
|
|
291
294
|
|
|
292
295
|
JavaScript applications will need more than a declarative import mechanism.
|
|
293
296
|
|
|
294
|
-
Here, we get an *HTMLImports* API for
|
|
297
|
+
Here, we get an *HTMLImports* API for imperative module import:
|
|
295
298
|
|
|
296
299
|
```js
|
|
297
300
|
const moduleObject1 = document.import('/foo#fragment1');
|
|
@@ -303,7 +306,7 @@ const moduleObject2 = document.import('/foo/nested#fragment2');
|
|
|
303
306
|
console.log(moduleObject2.value); // divElement
|
|
304
307
|
```
|
|
305
308
|
|
|
306
|
-
**-->** *with
|
|
309
|
+
**-->** *with the `moduleObject.value` property being a live property for when results are delivered asynchronous; e.g. with an asynchronously loaded module*:
|
|
307
310
|
|
|
308
311
|
```js
|
|
309
312
|
Observer.observe(moduleObject2, 'value', e => {
|
|
@@ -319,7 +322,7 @@ document.import('/foo#fragment1', divElement => {
|
|
|
319
322
|
});
|
|
320
323
|
```
|
|
321
324
|
|
|
322
|
-
**-->** *with an optional `live` parameter for staying subscribed to
|
|
325
|
+
**-->** *with an optional `live` parameter for staying subscribed to updated results*:
|
|
323
326
|
|
|
324
327
|
```js
|
|
325
328
|
const moduleObject2 = document.import('/foo/nested#fragment2', true/*live*/);
|
|
@@ -335,38 +338,25 @@ document.import('/foo#fragment1', true/*live*/, divElement => {
|
|
|
335
338
|
});
|
|
336
339
|
```
|
|
337
340
|
|
|
338
|
-
*...both of which
|
|
341
|
+
*...both of which get notified on doing the below*:
|
|
339
342
|
|
|
340
343
|
```js
|
|
341
344
|
document.querySelector('template[def="foo"]').content.firstElementChild.remove();
|
|
342
345
|
```
|
|
343
346
|
|
|
344
|
-
**-->** *with
|
|
347
|
+
**-->** *with a `moduleObject.abort()` method for unsubscribing from live updates*:
|
|
345
348
|
|
|
346
|
-
|
|
347
|
-
const abortController = new AbortController;
|
|
348
|
-
```
|
|
349
|
+
**-->** *with an optional `signal` parameter for passing in a custom `AbortSignal` instance*:
|
|
349
350
|
|
|
350
351
|
```js
|
|
352
|
+
const abortController = new AbortController;
|
|
351
353
|
const moduleObject2 = document.import('/foo/nested#fragment2', { live: true, signal: abortController.signal });
|
|
352
354
|
```
|
|
353
355
|
|
|
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
356
|
```js
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
});
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
```js
|
|
369
|
-
setTimeout(() => abortController.abort(), 1000);
|
|
357
|
+
setTimeout(() => {
|
|
358
|
+
abortController.abort(); // which would also call moduleObject2.abort()
|
|
359
|
+
}, 1000);
|
|
370
360
|
```
|
|
371
361
|
|
|
372
362
|
<details><summary>Extended Imports concepts</summary>
|
|
@@ -375,7 +365,7 @@ setTimeout(() => abortController.abort(), 1000);
|
|
|
375
365
|
|
|
376
366
|
We can defer module loading until we really need them.
|
|
377
367
|
|
|
378
|
-
Here, we get the `loading="lazy"` directive for that; and loading is only then triggered on the first attempt to import their contents:
|
|
368
|
+
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
369
|
|
|
380
370
|
```html
|
|
381
371
|
<!-- Loading doesn't happen until the first time this is being accessed -->
|
|
@@ -391,43 +381,6 @@ Here, we get the `loading="lazy"` directive for that; and loading is only then t
|
|
|
391
381
|
const moduleObject2 = document.import('/foo#fragment1'); // Triggers module loading and resolves at moduleObject2.value on load success
|
|
392
382
|
```
|
|
393
383
|
|
|
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
384
|
### Module Inheritance
|
|
432
385
|
|
|
433
386
|
We'll often have repeating markup structures.
|
|
@@ -478,80 +431,142 @@ Here, we get module nesting with inheritance to simplify that:
|
|
|
478
431
|
|
|
479
432
|
### Imports Contexts
|
|
480
433
|
|
|
481
|
-
We should be able to
|
|
434
|
+
We should be able to have *relative* import refs that resolve against local contexts in the document tree.
|
|
435
|
+
|
|
436
|
+
Here, we call those arbitrary contexts "Imports Contexts", and these could be:
|
|
437
|
+
|
|
438
|
+
+ Simple Base Path Contexts ([below](#base-path-contexts))
|
|
439
|
+
+ Scoped Module Contexts ([below](#scoped-module-contexts))
|
|
440
|
+
+ Named Contexts ([below](#named-contexts))
|
|
441
|
+
+ Extended Scoped Module Contexts ([below](#extended-scoped-module-contexts))
|
|
442
|
+
|
|
443
|
+
Alongside which we have a complementary `Element.prototype.import()` API for thinking in contexts.
|
|
444
|
+
|
|
445
|
+
#### "Base Path" Contexts
|
|
482
446
|
|
|
483
|
-
|
|
447
|
+
Base paths may be defined at arbitrary levels in the page using the `importscontext` attribute:
|
|
484
448
|
|
|
485
449
|
```html
|
|
486
450
|
<body importscontext="/foo">
|
|
487
451
|
<section>
|
|
488
|
-
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash),
|
|
452
|
+
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash), resolving to: /foo#fragment1 -->
|
|
489
453
|
</section>
|
|
490
454
|
</body>
|
|
491
455
|
```
|
|
492
456
|
|
|
493
457
|
```html
|
|
494
458
|
<body importscontext="/foo/nested">
|
|
459
|
+
<main>
|
|
460
|
+
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolving to: /foo/nested#fragment2 -->
|
|
461
|
+
</main>
|
|
462
|
+
</body>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**-->** *with said base paths being able to "nest" nicely*:
|
|
466
|
+
|
|
467
|
+
```html
|
|
468
|
+
<body importscontext="/foo">
|
|
469
|
+
|
|
495
470
|
<section>
|
|
496
|
-
<import ref="#
|
|
471
|
+
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash), resolves to: /foo#fragment1 -->
|
|
497
472
|
</section>
|
|
473
|
+
|
|
474
|
+
<div importscontext="nested"> <!-- Relative path (beginning without a slash), resolves to: /foo/nested -->
|
|
475
|
+
|
|
476
|
+
<main>
|
|
477
|
+
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolves to: /foo/nested#fragment2 -->
|
|
478
|
+
</main>
|
|
479
|
+
|
|
480
|
+
</div>
|
|
481
|
+
|
|
498
482
|
</body>
|
|
499
483
|
```
|
|
500
484
|
|
|
485
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
486
|
+
|
|
501
487
|
```js
|
|
502
|
-
// Using the HTMLImports API
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
488
|
+
// Using the HTMLImports API to import from context
|
|
489
|
+
const contextElement = document.querySelector('section');
|
|
490
|
+
const response = contextElement.import('#fragment1'); // Relative path (beginning without a slash), resolving to: /foo#fragment1
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
// Using the HTMLImports API to import from context
|
|
495
|
+
const contextElement = document.querySelector('main');
|
|
496
|
+
const response = contextElement.import('#fragment1'); // Relative path (beginning without a slash), resolving to: /foo/nested#fragment1
|
|
506
497
|
```
|
|
507
498
|
|
|
508
|
-
|
|
499
|
+
#### "Scoped Module" Contexts
|
|
500
|
+
|
|
501
|
+
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.
|
|
502
|
+
|
|
503
|
+
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 seek to be to Custom Elements):
|
|
509
504
|
|
|
510
505
|
```html
|
|
511
|
-
<
|
|
506
|
+
<section> <!-- Host object -->
|
|
512
507
|
|
|
513
|
-
<
|
|
508
|
+
<template def="foo" scoped> <!-- Scoped to host object and not available globally -->
|
|
509
|
+
<div def="fragment1"></div>
|
|
510
|
+
</template>
|
|
514
511
|
|
|
515
|
-
<
|
|
516
|
-
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash),
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
</div>
|
|
520
|
-
</section>
|
|
512
|
+
<div>
|
|
513
|
+
<import ref="foo#fragment1"></import> <!-- Relative path (beginning without a slash), resolving to the local module: foo#fragment1 -->
|
|
514
|
+
<import ref="/foo#fragment1"></import> <!-- Absolute path, resolving to the global module: /foo#fragment1 -->
|
|
515
|
+
</div>
|
|
521
516
|
|
|
522
|
-
</
|
|
517
|
+
</section>
|
|
523
518
|
```
|
|
524
519
|
|
|
520
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
521
|
+
|
|
525
522
|
```js
|
|
526
|
-
// Using the HTMLImports API
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
console.log(globalImport2); // { value: div }
|
|
523
|
+
// Using the HTMLImports API for local import
|
|
524
|
+
const contextElement = document.querySelector('div');
|
|
525
|
+
const localModule = moduleHost.import('#fragment1'); // Relative path (beginning without a slash), resolving to the local module: foo#fragment1
|
|
530
526
|
```
|
|
531
527
|
|
|
532
|
-
|
|
528
|
+
```js
|
|
529
|
+
// Using the HTMLImports API for global import
|
|
530
|
+
const contextElement = document.querySelector('div');
|
|
531
|
+
const globalModule = contextElement.import('/foo#fragment1'); // Absolute path, resolving to the global module: /foo#fragment1
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### Named Contexts
|
|
535
|
+
|
|
536
|
+
Imports Contexts may be named:
|
|
533
537
|
|
|
534
538
|
```html
|
|
535
|
-
<body importscontext="/foo">
|
|
539
|
+
<body contextname="context1" importscontext="/foo/nested">
|
|
536
540
|
|
|
537
|
-
<import ref="#
|
|
541
|
+
<import ref="#fragment2"></import> <!-- Relative path (beginning without a slash), resolves to: /foo/nested#fragment2 -->
|
|
542
|
+
|
|
543
|
+
<section importscontext="/foo">
|
|
544
|
+
<import ref="#fragment1"></import> <!-- Relative path (beginning without a slash), resolves to: /foo#fragment1 -->
|
|
545
|
+
|
|
546
|
+
<div>
|
|
547
|
+
<import ref="@context1#fragment2"></import> <!-- Context-relative path (beginning with a context name), resolves to: /foo/nested#fragment2 -->
|
|
548
|
+
</div>
|
|
538
549
|
|
|
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
550
|
</section>
|
|
542
551
|
|
|
543
552
|
</body>
|
|
544
553
|
```
|
|
545
554
|
|
|
546
|
-
|
|
555
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
547
556
|
|
|
548
|
-
|
|
557
|
+
```js
|
|
558
|
+
// Using the HTMLImports API to import from a named Imports Context
|
|
559
|
+
const contextElement = document.querySelector('div');
|
|
560
|
+
const result = contextElement.import('@context1#fragment2'); // Resolving to the module:/foo/nested#fragment2
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
#### Extended Scoped Module Contexts
|
|
549
564
|
|
|
550
|
-
|
|
565
|
+
Scoped Module Contexts may also have a Base Path Context that they inherit from:
|
|
551
566
|
|
|
552
567
|
```html
|
|
553
568
|
<body contextname="context1" importscontext="/bar">
|
|
554
|
-
<section importscontext="nested"> <!-- object with
|
|
569
|
+
<section importscontext="nested"> <!-- object with Scoped Modules, plus inherited context: /bar/nested -->
|
|
555
570
|
|
|
556
571
|
<template def="foo" scoped> <!-- Scoped to host object and not available globally -->
|
|
557
572
|
<div def="fragment1"></div>
|
|
@@ -559,33 +574,36 @@ Here, we're able to have *one* element implement *both* at the same time - with
|
|
|
559
574
|
</template>
|
|
560
575
|
|
|
561
576
|
<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> <!--
|
|
577
|
+
<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 -->
|
|
578
|
+
<import ref="/foo#fragment1"></import> <!-- Absolute path, resolving to the global module: /foo#fragment1 -->
|
|
579
|
+
<import ref="@context1#fragment1"></import> <!-- Relative path with a named context, resolving to the global module: /bar#fragment1 -->
|
|
565
580
|
</div>
|
|
566
581
|
|
|
567
582
|
</section>
|
|
568
583
|
</body>
|
|
569
584
|
```
|
|
570
585
|
|
|
586
|
+
**-->** *with the `Element.prototype.import()` API for equivalent context-based imports*:
|
|
587
|
+
|
|
571
588
|
```js
|
|
572
589
|
// Using the HTMLImports API
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
console.log(localOrGlobalImport2); // { value: div }
|
|
590
|
+
const contextElement = document.querySelector('div');
|
|
591
|
+
const result = contextElement.import('#fragment2'); // the local module: foo#fragment2, and if not found, the inherited module: /bar/nested#fragment2
|
|
576
592
|
```
|
|
577
593
|
|
|
578
594
|
</details>
|
|
579
595
|
|
|
580
596
|
## Data Binding
|
|
581
597
|
|
|
582
|
-
Data binding is about declaratively binding the UI to application data,
|
|
598
|
+
Data binding is about declaratively binding the UI to application data, wherein the relevant parts of the UI *automatically* update as application state changes.
|
|
599
|
+
|
|
600
|
+
OOHTML makes this possible in just simple conventions - via a new comment-based data-binding syntax `<?{ }?>` and a complementary new `expr` attribute!
|
|
583
601
|
|
|
584
|
-
|
|
602
|
+
And there's one more: Quantum Scripts which brings the most advanced form of reactivity to HTML!
|
|
585
603
|
|
|
586
604
|
### Discrete Data-Binding
|
|
587
605
|
|
|
588
|
-
Here, we get a comment-based data-binding tag `<?{ }?>` which
|
|
606
|
+
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
607
|
|
|
590
608
|
```js
|
|
591
609
|
<html>
|
|
@@ -599,9 +617,9 @@ Here, we get a comment-based data-binding tag `<?{ }?>` which works like regular
|
|
|
599
617
|
</html>
|
|
600
618
|
```
|
|
601
619
|
|
|
602
|
-
<details><summary>
|
|
620
|
+
<details><summary>Resolution details</summary>
|
|
603
621
|
|
|
604
|
-
Here, JavaScript references are resolved from the closest node up the document
|
|
622
|
+
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
623
|
|
|
606
624
|
```js
|
|
607
625
|
document.bind({ name: 'James Boye', cool: '100%', app: { title: 'Demo App' } });
|
|
@@ -615,7 +633,7 @@ document: { name: 'James Boye', cool: '100%', app: { title: 'Demo App' } }
|
|
|
615
633
|
└── body: { name: 'John Doe' }
|
|
616
634
|
```
|
|
617
635
|
|
|
618
|
-
|
|
636
|
+
Now, the `name` reference remains bound to the `name` *binding* on the `<body>` element until the meaning of "closest node" changes again:
|
|
619
637
|
|
|
620
638
|
```js
|
|
621
639
|
delete document.body.bindings.name;
|
|
@@ -629,19 +647,19 @@ document.body.bindings.cool = '200%';
|
|
|
629
647
|
|
|
630
648
|
</details>
|
|
631
649
|
|
|
632
|
-
<details><summary>With SSR
|
|
650
|
+
<details><summary>With SSR support</summary>
|
|
633
651
|
|
|
634
|
-
On the server, these data-binding tags
|
|
652
|
+
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
653
|
|
|
636
|
-
The following: `<?{ 'Hello World' }?>` would thus give us: `<?{ 'Hello World' }?>Hello World`.
|
|
654
|
+
The following expression: `<?{ 'Hello World' }?>` would thus give us: `<?{ 'Hello World' }?>Hello World`.
|
|
637
655
|
|
|
638
|
-
But they also
|
|
656
|
+
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
657
|
|
|
640
658
|
```html
|
|
641
659
|
<?{ 'Hello World'; [=11] }?>Hello World
|
|
642
660
|
```
|
|
643
661
|
|
|
644
|
-
Now that extra bit of information gets decoded and original relationships are forned again
|
|
662
|
+
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
663
|
|
|
646
664
|
> 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
665
|
|
|
@@ -649,51 +667,51 @@ Now that extra bit of information gets decoded and original relationships are fo
|
|
|
649
667
|
|
|
650
668
|
### Inline Data-Binding
|
|
651
669
|
|
|
652
|
-
Here, we get the `
|
|
670
|
+
Here, we get the `expr` attribute for a declarative, neat, key/value data-binding syntax:
|
|
653
671
|
|
|
654
672
|
```html
|
|
655
|
-
<div
|
|
673
|
+
<div expr="<directive> <param>: <arg>;"></div>
|
|
656
674
|
```
|
|
657
675
|
|
|
658
676
|
**-->** *where*:
|
|
659
677
|
|
|
660
|
-
+ *`<directive>` is
|
|
678
|
+
+ *`<directive>` is a directive, which is always a symbol*
|
|
661
679
|
+ *`<param>` is the parameter being bound, which could be a CSS property, class name, attribute name, Structural Directive - depending on the givin directive*
|
|
662
680
|
+ *`<arg>` is the bound value or expression*
|
|
663
681
|
|
|
664
682
|
**-->** *which would give us the following for a CSS property*:
|
|
665
683
|
|
|
666
684
|
```html
|
|
667
|
-
<div
|
|
685
|
+
<div expr="& color:someColor; & backgroundColor:'red'"></div>
|
|
668
686
|
```
|
|
669
687
|
|
|
670
688
|
**-->** *without being space-sensitive*:
|
|
671
689
|
|
|
672
690
|
```html
|
|
673
|
-
<div
|
|
691
|
+
<div expr="& color:someColor; &backgroundColor: 'red'"></div>
|
|
674
692
|
```
|
|
675
693
|
|
|
676
694
|
**-->** *the rest of which can be seen below*:
|
|
677
695
|
|
|
678
696
|
| Directive | Type | Usage |
|
|
679
697
|
| :---- | :---- | :---- |
|
|
680
|
-
| `&` | CSS Property | `<div
|
|
681
|
-
| `%` | Class Name | `<div
|
|
682
|
-
| `~` | Attribute Name | `<a
|
|
683
|
-
| | Boolean Attribute | `<a
|
|
698
|
+
| `&` | CSS Property | `<div expr="& color:someColor; & backgroundColor:someBgColor;"></div>` |
|
|
699
|
+
| `%` | Class Name | `<div expr="% active:app.isActive; % expanded:app.isExpanded;"></div>` |
|
|
700
|
+
| `~` | Attribute Name | `<a expr="~ href:person.profileUrl+'#bio'; ~ title:'Click me';"></a>` |
|
|
701
|
+
| | Boolean Attribute | `<a expr="~ ?required:formField.required; ~ ?aria-checked: formField.checked"></a>` |
|
|
684
702
|
| `@` | Structural Directive: | *See below* |
|
|
685
|
-
| `@text` | Plain text content | `<span
|
|
686
|
-
| `@html` | Markup content | `<span
|
|
687
|
-
| `@items` | A list,
|
|
703
|
+
| `@text` | Plain text content | `<span expr="@text:firstName+' '+lastName;"></span>` |
|
|
704
|
+
| `@html` | Markup content | `<span expr="@html: '<i>'+firstName+'</i>';"></span>` |
|
|
705
|
+
| `@items` | A list, of the following format | `<declaration> <of\|in> <iterable> / <importRef>`<br>*See next two tables* |
|
|
688
706
|
|
|
689
707
|
<details><summary><code>For ... Of</code> Loops</summary>
|
|
690
708
|
|
|
691
709
|
| Idea | Usage |
|
|
692
710
|
| :---- | :---- |
|
|
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
|
|
711
|
+
| A `for...of` loop over an array/iterable | `<ul expr="@items: value of [1,2,3] / 'foo#fragment';"></ul>` |
|
|
712
|
+
| Same as above but with a `key` declaration | `<ul expr="@items: (value,key) of [1,2,3] / 'foo#fragment';"></ul>` |
|
|
713
|
+
| Same as above but with different variable names | `<ul expr="@items: (product,id) of store.products / 'foo#fragment';"></ul>` |
|
|
714
|
+
| Same as above but with a dynamic `importRef` | `<ul expr="@items: (product,id) of store.products / store.importRef;"></ul>` |
|
|
697
715
|
|
|
698
716
|
</details>
|
|
699
717
|
|
|
@@ -701,14 +719,14 @@ Here, we get the `binding` attribute for a declarative and neat, key/value data-
|
|
|
701
719
|
|
|
702
720
|
| Idea | Usage |
|
|
703
721
|
| :---- | :---- |
|
|
704
|
-
| A `for...in` loop over an object | `<ul
|
|
705
|
-
| Same as above but with a `value` and `index` declaration | `<ul
|
|
722
|
+
| A `for...in` loop over an object | `<ul expr="@items: key in {a:1,b:2} / 'foo#fragment';"></ul>` |
|
|
723
|
+
| 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
724
|
|
|
707
725
|
</details>
|
|
708
726
|
|
|
709
|
-
<details><summary>
|
|
727
|
+
<details><summary>Resolution details</summary>
|
|
710
728
|
|
|
711
|
-
Here, JavaScript references are resolved from the closest node up the document
|
|
729
|
+
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
730
|
|
|
713
731
|
```js
|
|
714
732
|
document.bind({ someColor: 'green', someBgColor: 'yellow' });
|
|
@@ -722,7 +740,7 @@ document: { someColor: 'green', someBgColor: 'yellow' }
|
|
|
722
740
|
└── body: { someBgColor: 'silver' }
|
|
723
741
|
```
|
|
724
742
|
|
|
725
|
-
|
|
743
|
+
Now, the `someBgColor` reference remains bound to the `someBgColor` *binding* on the `<body>` element until the meaning of "closest node" changes again:
|
|
726
744
|
|
|
727
745
|
```js
|
|
728
746
|
delete document.body.bindings.someBgColor;
|
|
@@ -742,9 +760,9 @@ Bindings are resolved in realtime! And in fact, for lists, in-place mutations -
|
|
|
742
760
|
|
|
743
761
|
</details>
|
|
744
762
|
|
|
745
|
-
<details><summary>With SSR
|
|
763
|
+
<details><summary>With SSR support</summary>
|
|
746
764
|
|
|
747
|
-
For lists, generated item elements are automatically assigned a corresponding
|
|
765
|
+
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
766
|
|
|
749
767
|
</details>
|
|
750
768
|
|
|
@@ -752,7 +770,7 @@ For lists, generated item elements are automatically assigned a corresponding in
|
|
|
752
770
|
|
|
753
771
|
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
772
|
|
|
755
|
-
Here, from the same `<script>` element we already write, we get a direct upgrade path to reactive programming in just an attribute: `quantum`:
|
|
773
|
+
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
774
|
|
|
757
775
|
```html
|
|
758
776
|
<script quantum>
|
|
@@ -768,7 +786,7 @@ Here, from the same `<script>` element we already write, we get a direct upgrade
|
|
|
768
786
|
</script>
|
|
769
787
|
```
|
|
770
788
|
|
|
771
|
-
**-->** *which adds up really well with the
|
|
789
|
+
**-->** *which gives us fine-grained reactivity on top of literal JavaScript syntax; and which adds up really well with the `scoped` attribute*:
|
|
772
790
|
|
|
773
791
|
```html
|
|
774
792
|
<main>
|
|
@@ -792,7 +810,7 @@ Here, from the same `<script>` element we already write, we get a direct upgrade
|
|
|
792
810
|
</main>
|
|
793
811
|
```
|
|
794
812
|
|
|
795
|
-
**-->** *with content being whatever you normally would write in a `<script>` element,
|
|
813
|
+
**-->** *with content being whatever you normally would write in a `<script>` element, minus the "manual" work for reactivity*:
|
|
796
814
|
|
|
797
815
|
```html
|
|
798
816
|
<main>
|
|
@@ -810,7 +828,7 @@ Here, from the same `<script>` element we already write, we get a direct upgrade
|
|
|
810
828
|
</main>
|
|
811
829
|
```
|
|
812
830
|
|
|
813
|
-
**-->** *within which
|
|
831
|
+
**-->** *within which dynamic application state/data, and even things like the Namespace API above, fit seamlessly*:
|
|
814
832
|
|
|
815
833
|
```html
|
|
816
834
|
<main namespace>
|
|
@@ -843,13 +861,13 @@ const removeButton = () => {
|
|
|
843
861
|
};
|
|
844
862
|
```
|
|
845
863
|
|
|
846
|
-
<details><summary>
|
|
864
|
+
<details><summary>Learn more</summary>
|
|
847
865
|
|
|
848
866
|
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
867
|
|
|
850
868
|
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
869
|
|
|
852
|
-
Now, in each case above, reactivity terminates on script's removal from the DOM
|
|
870
|
+
Now, in each case above, reactivity terminates on script's removal from the DOM or via a programmatic approach:
|
|
853
871
|
|
|
854
872
|
```js
|
|
855
873
|
const script = document.querySelector('script[quantum]');
|
|
@@ -863,115 +881,397 @@ But while that is automatic, DOM event handlers bound via `addEventListener()` w
|
|
|
863
881
|
|
|
864
882
|
## Data Plumbing
|
|
865
883
|
|
|
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.
|
|
884
|
+
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
885
|
|
|
871
|
-
|
|
886
|
+
<details><summary>Example</summary>
|
|
872
887
|
|
|
873
888
|
```js
|
|
874
|
-
|
|
875
|
-
|
|
889
|
+
// Inside a custom element
|
|
890
|
+
connectedCallback() {
|
|
891
|
+
this.prop1 = 1;
|
|
892
|
+
this.prop2 = 2;
|
|
893
|
+
this.prop3 = 3;
|
|
894
|
+
this.style = 'tall-dark'; // ??? - conflict with the standard HTMLElement: style property
|
|
876
895
|
}
|
|
877
896
|
```
|
|
878
897
|
|
|
879
898
|
```js
|
|
880
|
-
|
|
881
|
-
|
|
899
|
+
// Outside the component
|
|
900
|
+
const node = document.querySelector('my-element');
|
|
901
|
+
node.prop1 = 1;
|
|
902
|
+
node.prop2 = 2;
|
|
903
|
+
node.prop3 = 3;
|
|
904
|
+
node.normalize = true; // ??? - conflict with the standard Node: normalize() method
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
</details>
|
|
908
|
+
|
|
909
|
+
This calls for a decent API and some data-flow mechanism!
|
|
910
|
+
|
|
911
|
+
### The Bindings API
|
|
912
|
+
|
|
913
|
+
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.
|
|
914
|
+
|
|
915
|
+
**-->** *it's an ordinary JavaScript object that can be read and mutated*:
|
|
916
|
+
|
|
917
|
+
```js
|
|
918
|
+
// Read
|
|
919
|
+
console.log(document.bindings); // {}
|
|
920
|
+
// Modify
|
|
921
|
+
document.bindings.app = { title: 'Demo App' };
|
|
922
|
+
console.log(document.bindings.app); // { title: 'Demo App' }
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
```js
|
|
926
|
+
const node = document.querySelector('div');
|
|
927
|
+
// Read
|
|
928
|
+
console.log(node.bindings); // {}
|
|
929
|
+
// Modify
|
|
930
|
+
node.bindings.style = 'tall-dark';
|
|
931
|
+
node.bindings.normalize = true;
|
|
882
932
|
```
|
|
883
933
|
|
|
934
|
+
**-->** *with a complementary `bind()` method that lets us make mutations in batch*:
|
|
935
|
+
|
|
884
936
|
```js
|
|
885
|
-
|
|
937
|
+
// ------------
|
|
938
|
+
// Set multiple properties
|
|
939
|
+
document.bind({ name: 'James Boye', cool: '100%', app: { title: 'Demo App' } });
|
|
940
|
+
|
|
941
|
+
// ------------
|
|
942
|
+
// Replace existing properties with a new set
|
|
943
|
+
document.bind({ signedIn: false, hot: '100%' });
|
|
944
|
+
// Inspect
|
|
945
|
+
console.log(document.bindings); // { signedIn: false, hot: '100%' }
|
|
946
|
+
|
|
947
|
+
// ------------
|
|
948
|
+
// Merge a new set of properties with existing
|
|
949
|
+
document.bind({ name: 'James Boye', cool: '100%' }, { merge: true });
|
|
950
|
+
// Inspect
|
|
951
|
+
console.log(document.bindings); // { signedIn: false, hot: '100%', name: 'James Boye', cool: '100%' }
|
|
886
952
|
```
|
|
887
953
|
|
|
954
|
+
**-->** *which also provides an easy way to passing data down a component tree*:
|
|
955
|
+
|
|
888
956
|
```js
|
|
889
|
-
|
|
957
|
+
// Inside a custom element
|
|
958
|
+
connectedCallback() {
|
|
959
|
+
this.child1.bind(this.bindings.child1Data);
|
|
960
|
+
this.child2.bind(this.bindings.child2Data);
|
|
961
|
+
}
|
|
890
962
|
```
|
|
891
963
|
|
|
892
|
-
|
|
964
|
+
**-->** *and with the Observer API in the picture for reactivity*:
|
|
893
965
|
|
|
894
966
|
```js
|
|
895
|
-
|
|
896
|
-
|
|
967
|
+
Observer.observe(document.bindings, mutations => {
|
|
968
|
+
mutations.forEach(mutation => console.log(mutation));
|
|
969
|
+
});
|
|
970
|
+
```
|
|
897
971
|
|
|
898
|
-
|
|
899
|
-
|
|
972
|
+
```js
|
|
973
|
+
// Inside a custom element
|
|
974
|
+
connectedCallback() {
|
|
975
|
+
Observer.observe(this.bindings, 'style', e => {
|
|
976
|
+
// Compunonent should magically change style
|
|
977
|
+
console.log(e.value);
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
```
|
|
900
981
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
982
|
+
```js
|
|
983
|
+
const node = document.querySelector('my-element');
|
|
984
|
+
node.bindings.style = 'tall-dark';
|
|
904
985
|
```
|
|
905
986
|
|
|
987
|
+
<details><summary>Details</summary>
|
|
988
|
+
|
|
989
|
+
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:
|
|
990
|
+
|
|
906
991
|
```js
|
|
907
|
-
|
|
908
|
-
|
|
992
|
+
node.bindings.style = 'tall-dark'; // Reactive assignment
|
|
993
|
+
delete node.bindings.style; // Reactive deletion
|
|
994
|
+
```
|
|
909
995
|
|
|
910
|
-
|
|
911
|
-
element.bindings.isCollapsed = true;
|
|
996
|
+
For mutations at a deeper level to be reactive, the corresponding Observer API method must be used:
|
|
912
997
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
Observer.
|
|
998
|
+
```js
|
|
999
|
+
Observer.set(document.bindings.app, 'title', 'Demo App!!!');
|
|
1000
|
+
Observer.deleteProperty(document.bindings.app, 'title');
|
|
916
1001
|
```
|
|
917
1002
|
|
|
918
|
-
|
|
1003
|
+
</details>
|
|
919
1004
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1005
|
+
### The Context API
|
|
1006
|
+
|
|
1007
|
+
Component trees on the typical UI 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.
|
|
923
1008
|
|
|
924
|
-
|
|
925
|
-
|
|
1009
|
+
Interestingly, the Context API is the underlying resolution mechanism for HTML Imports and Data Binding in OOHTML!
|
|
1010
|
+
|
|
1011
|
+
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.
|
|
1012
|
+
|
|
1013
|
+
**-->** *with the `contexts.request()` method for firing requests*:
|
|
1014
|
+
|
|
1015
|
+
```js
|
|
1016
|
+
// ------------
|
|
1017
|
+
// Get an arbitrary
|
|
1018
|
+
const node = document.querySelector('my-element');
|
|
1019
|
+
|
|
1020
|
+
// ------------
|
|
1021
|
+
// Prepare and fire request event
|
|
1022
|
+
const requestParams = { kind: 'html-imports', detail: '/foo#fragment1' };
|
|
1023
|
+
const response = node.contexts.request(requestParams);
|
|
1024
|
+
|
|
1025
|
+
// ------------
|
|
1026
|
+
// Handle response
|
|
1027
|
+
console.log(response.value); // It works!
|
|
1028
|
+
```
|
|
926
1029
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1030
|
+
**-->** *and the `contexts.attach()` and `contexts.detach()` methods for attaching/detaching providers at arbitrary levels in the DOM tree*:
|
|
1031
|
+
|
|
1032
|
+
```js
|
|
1033
|
+
// ------------
|
|
1034
|
+
// Define a CustomContext class
|
|
1035
|
+
class FakeImportsContext extends DOMContext {
|
|
1036
|
+
static kind = 'html-imports';
|
|
1037
|
+
handle(event) {
|
|
1038
|
+
console.log(event.detail); // '/foo#fragment1'
|
|
1039
|
+
event.respondWith('It works!');
|
|
930
1040
|
}
|
|
931
|
-
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ------------
|
|
1044
|
+
// Instantiate and attach to a node
|
|
1045
|
+
const fakeImportsContext = new FakeImportsContext;
|
|
1046
|
+
document.contexts.attach(fakeImportsContext);
|
|
1047
|
+
|
|
1048
|
+
// ------------
|
|
1049
|
+
// Detach anytime
|
|
1050
|
+
document.contexts.detach(fakeImportsContext);
|
|
932
1051
|
```
|
|
933
1052
|
|
|
1053
|
+
<details><summary>Details</summary>
|
|
1054
|
+
|
|
1055
|
+
In the current OOHTML implementation, the Context API interfaces are exposed via the global `webqit` object:
|
|
1056
|
+
|
|
934
1057
|
```js
|
|
935
|
-
|
|
1058
|
+
const { DOMContext, DOMContextRequestEvent, DOMContextResponse } = window.webqit;
|
|
936
1059
|
```
|
|
937
1060
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1061
|
+
Now, by design...
|
|
1062
|
+
|
|
1063
|
+
+ a provider will automatically adopt the `contextname`, if any, of its host element:
|
|
1064
|
+
|
|
1065
|
+
```html
|
|
1066
|
+
<div contextname="context1"></div>
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
```js
|
|
1070
|
+
// Instantiate and attach to a node
|
|
1071
|
+
const host = document.querySelector('div');
|
|
1072
|
+
const fakeImportsContext = new FakeImportsContext;
|
|
1073
|
+
host.contexts.attach(fakeImportsContext);
|
|
1074
|
+
// Inspect name
|
|
1075
|
+
console.log(fakeImportsContext.name); // context1
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
...which a request could target:
|
|
942
1079
|
|
|
943
|
-
|
|
1080
|
+
```js
|
|
1081
|
+
const requestParams = { kind: FakeImportsContext.kind, targetContext: 'context1', detail: '/foo#fragment1' };
|
|
1082
|
+
const response = node.contexts.request(requestParams);
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
+ and providers of same kind could be differentiated by an extra "detail" - an arbitrary value passed to the constructor:
|
|
1086
|
+
|
|
1087
|
+
```js
|
|
1088
|
+
const fakeImportsContext = new FakeImportsContext('lorem');
|
|
1089
|
+
console.log(fakeImportsContext.detail); // lorem
|
|
1090
|
+
```
|
|
944
1091
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1092
|
+
+ and a provider could indicate to manually match requests where the defualt "kind" matching, and optional "targetContext" matching, don't suffice:
|
|
1093
|
+
|
|
1094
|
+
```js
|
|
1095
|
+
// Define a CustomContext class
|
|
1096
|
+
class CustomContext extends DOMContext {
|
|
1097
|
+
static kind = 'html-imports';
|
|
1098
|
+
matchEvent(event) {
|
|
1099
|
+
// The default request matching algorithm + "detail" matching
|
|
1100
|
+
return super.matchEvent() && event.detail === this.detail;
|
|
1101
|
+
}
|
|
1102
|
+
handle(event) {
|
|
1103
|
+
console.log(event.detail);
|
|
1104
|
+
event.respondWith('It works!');
|
|
1105
|
+
}
|
|
948
1106
|
}
|
|
949
|
-
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
+ and a request could choose to stay subscribed to changes on the requested data; the request would simply add a `live` flag:
|
|
1110
|
+
|
|
1111
|
+
```js
|
|
1112
|
+
// Set the "live" flag
|
|
1113
|
+
const requestParams = { kind: FakeImportsContext.kind, targetContext: 'context1', detail: '/foo#fragment1', live: true };
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
...then stay alert to said updates on the returned `DOMContextResponse` object or specify a callback function at request time:
|
|
1117
|
+
|
|
1118
|
+
```js
|
|
1119
|
+
// Handle response without a callback
|
|
1120
|
+
const response = node.contexts.request(requestParams);
|
|
1121
|
+
console.log(response.value); // It works!
|
|
1122
|
+
Observer.observe(response, 'value', e => {
|
|
1123
|
+
console.log(e.value); // It works live!
|
|
1124
|
+
});
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
```js
|
|
1128
|
+
// Handle response with a callback
|
|
1129
|
+
node.contexts.request(requestParams, value => {
|
|
1130
|
+
console.log(value);
|
|
1131
|
+
// It works!
|
|
1132
|
+
// It works live!
|
|
1133
|
+
});
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
...while provider simply checks for the `event.live` flag and keep the updates flowing:
|
|
1137
|
+
|
|
1138
|
+
```js
|
|
1139
|
+
// Define a CustomContext class
|
|
1140
|
+
class CustomContext extends DOMContext {
|
|
1141
|
+
static kind = 'html-imports';
|
|
1142
|
+
handle(event) {
|
|
1143
|
+
event.respondWith('It works!');
|
|
1144
|
+
if (event.live) {
|
|
1145
|
+
setTimeout(() => {
|
|
1146
|
+
event.respondWith('It works live!');
|
|
1147
|
+
}, 5000);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
...and optionally implements a `subscribed` and `unsubscribed` lifecycle hook for when a "live" event enters and leaves the instance:
|
|
1154
|
+
|
|
1155
|
+
```js
|
|
1156
|
+
// Define a CustomContext class
|
|
1157
|
+
class CustomContext extends DOMContext {
|
|
1158
|
+
static kind = 'html-imports';
|
|
1159
|
+
subscribed(event) {
|
|
1160
|
+
console.log(this.subscriptions.size); // 1
|
|
1161
|
+
}
|
|
1162
|
+
unsubscribed(event) {
|
|
1163
|
+
console.log(this.subscriptions.size); // 0
|
|
1164
|
+
}
|
|
1165
|
+
handle(event) {
|
|
1166
|
+
event.respondWith('It works!');
|
|
1167
|
+
if (event.live) {
|
|
1168
|
+
setTimeout(() => {
|
|
1169
|
+
event.respondWith('It works live!');
|
|
1170
|
+
}, 5000);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
+ live requests are terminated via the returned `DOMContextResponse` object...
|
|
1177
|
+
|
|
1178
|
+
```js
|
|
1179
|
+
response.abort();
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
...or via an initially specified custom `AbortSignal`:
|
|
1183
|
+
|
|
1184
|
+
```js
|
|
1185
|
+
// Add a signal to the original request
|
|
1186
|
+
const abortController = new AbortController;
|
|
1187
|
+
const requestParams = { kind: FakeImportsContext.kind, targetContext: 'context1', detail: '/foo#fragment1', live: true, signal: abortController.signal };
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
```js
|
|
1191
|
+
abortController.abort(); // Which also calls response.abort();
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
+ 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:
|
|
1195
|
+
|
|
1196
|
+
```js
|
|
1197
|
+
document: // 'fake-provider' here
|
|
1198
|
+
└── html
|
|
1199
|
+
├── head
|
|
1200
|
+
└── body: // 'fake-provider' here. Our request above is now served from here.
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
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.
|
|
1204
|
+
|
|
1205
|
+
```js
|
|
1206
|
+
document: // 'fake-provider' here. Our request above is now served from here.
|
|
1207
|
+
└── html
|
|
1208
|
+
├── head
|
|
1209
|
+
└── body:
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
While, in all, saving the requesting code that "admin" work!
|
|
1213
|
+
|
|
1214
|
+
</details>
|
|
1215
|
+
|
|
1216
|
+
**-->** *all of which gives us the imperative equivalent of declarative context-based features like HTMLImports and Data Binding*:
|
|
1217
|
+
|
|
1218
|
+
```html
|
|
1219
|
+
<div contextname="vendor1">
|
|
1220
|
+
<div contextname="vendor2">
|
|
1221
|
+
...
|
|
1222
|
+
|
|
1223
|
+
<my-element>
|
|
1224
|
+
<!-- Declarative import -->
|
|
1225
|
+
<import ref="@vendor1/foo#fragment1"></import>
|
|
1226
|
+
<!-- Declarative Data Binding -->
|
|
1227
|
+
<?{ @vendor2.app.title }?>
|
|
1228
|
+
</my-element>
|
|
1229
|
+
|
|
1230
|
+
...
|
|
1231
|
+
</div>
|
|
950
1232
|
</div>
|
|
951
1233
|
```
|
|
952
1234
|
|
|
953
1235
|
```js
|
|
954
|
-
|
|
1236
|
+
// ------------
|
|
1237
|
+
// Equivalent import() approach
|
|
1238
|
+
const response = myElement.import('@vendor1/foo#fragment1');
|
|
1239
|
+
|
|
1240
|
+
// ------------
|
|
1241
|
+
// Equivalent Context API approach
|
|
1242
|
+
const requestParams = { kind: 'html-imports', targetContext: 'vendor1', detail: 'foo#fragment1' };
|
|
1243
|
+
const response = myElement.contexts.request(requestParams);
|
|
1244
|
+
|
|
1245
|
+
// ------------
|
|
1246
|
+
// Handle response
|
|
1247
|
+
console.log(response.value);
|
|
955
1248
|
```
|
|
956
1249
|
|
|
957
|
-
|
|
1250
|
+
```js
|
|
1251
|
+
// ------------
|
|
1252
|
+
// Context API request for bindings
|
|
1253
|
+
const requestParams = { kind: 'bindings', targetContext: 'vendor2', detail: 'app' };
|
|
1254
|
+
const response = myElement.contexts.request(requestParams);
|
|
958
1255
|
|
|
959
|
-
|
|
1256
|
+
// ------------
|
|
1257
|
+
// Handle response
|
|
1258
|
+
console.log(response.value.title);
|
|
1259
|
+
```
|
|
960
1260
|
|
|
961
1261
|
## Polyfill
|
|
962
1262
|
|
|
963
|
-
OOHTML is being developed as something to be used today—via a polyfill. This is an
|
|
1263
|
+
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
1264
|
|
|
965
1265
|
<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/
|
|
1266
|
+
└───────── <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
1267
|
|
|
968
1268
|
```html
|
|
969
|
-
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
|
|
1269
|
+
<script src="https://unpkg.com/@webqit/oohtml/dist/main.lite.js"></script>
|
|
970
1270
|
```
|
|
971
1271
|
|
|
972
1272
|
└ This is to be placed early on in the document and should be a classic script without any `defer` or `async` directives!
|
|
973
1273
|
|
|
974
|
-
└ For the Scoped Styles feature, you'd
|
|
1274
|
+
└ 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
1275
|
|
|
976
1276
|
```html
|
|
977
1277
|
<head>
|
|
@@ -985,18 +1285,19 @@ OOHTML is being developed as something to be used today—via a polyfill. This i
|
|
|
985
1285
|
└───────── <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
1286
|
|
|
987
1287
|
```bash
|
|
988
|
-
npm i @webqit/oohtml
|
|
1288
|
+
npm i @webqit/oohtml @webqit/quantum-js
|
|
989
1289
|
```
|
|
990
1290
|
|
|
991
1291
|
```js
|
|
992
1292
|
// Import
|
|
1293
|
+
import * as Quantum from '@webqit/quantum-js/lite'; // Or from '@webqit/quantum-js'; See implementation notes below
|
|
993
1294
|
import init from '@webqit/oohtml';
|
|
994
1295
|
|
|
995
1296
|
// Initialize the lib
|
|
996
|
-
init.call(
|
|
1297
|
+
init.call(window, Quantum[, options = {}]);
|
|
997
1298
|
```
|
|
998
1299
|
|
|
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
|
|
1300
|
+
└ 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
1301
|
|
|
1001
1302
|
└ But all things "SSR" for OOHTML are best left to the [`@webqit/oohtml-ssr`](https://github.com/webqit/oohtml-ssr) package!
|
|
1002
1303
|
|
|
@@ -1004,24 +1305,36 @@ init.call( window[, options = {} ]);
|
|
|
1004
1305
|
|
|
1005
1306
|
<details><summary>Extended usage concepts</summary>
|
|
1006
1307
|
|
|
1007
|
-
If you'll be going ahead to build a real app
|
|
1308
|
+
If you'll be going ahead to build a real app with OOHTML, you may want to consider also using:
|
|
1008
1309
|
|
|
1009
1310
|
+ the [`@webqit/oohtml-cli`](https://github.com/webqit/oohtml-cli) package for operating a file-based templating system.
|
|
1010
1311
|
|
|
1011
|
-
+ the modest, OOHTML-based [Webflo](https://github.com/webqit/webflo) framework to greatly streamline your
|
|
1312
|
+
+ the modest, OOHTML-based [Webflo](https://github.com/webqit/webflo) framework to greatly streamline your workflow!
|
|
1012
1313
|
|
|
1013
1314
|
</details>
|
|
1014
1315
|
|
|
1015
1316
|
<details><summary>Implementation Notes</summary>
|
|
1016
1317
|
|
|
1318
|
+
+ **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).)
|
|
1319
|
+
|
|
1320
|
+
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:
|
|
1321
|
+
|
|
1322
|
+
```html
|
|
1323
|
+
<head>
|
|
1324
|
+
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
|
|
1325
|
+
</head>
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
└─ <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>
|
|
1329
|
+
|
|
1017
1330
|
+ **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
1331
|
|
|
1019
|
-
If you must load the script "async", one little trade-off
|
|
1332
|
+
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
1333
|
|
|
1021
1334
|
```html
|
|
1022
1335
|
<head>
|
|
1023
1336
|
<meta name="scoped-js" content="script.mimeType=some-mime">
|
|
1024
|
-
<script async src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
|
|
1337
|
+
<script async src="https://unpkg.com/@webqit/oohtml/dist/main.lite.js"></script>
|
|
1025
1338
|
</head>
|
|
1026
1339
|
<body>
|
|
1027
1340
|
<script type="some-mime" scoped>
|
|
@@ -1042,16 +1355,6 @@ If you'll be going ahead to build a real app to see OOHTML in action, you may wa
|
|
|
1042
1355
|
|
|
1043
1356
|
...still gives the `window` object in the console.
|
|
1044
1357
|
|
|
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
1358
|
+ **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
1359
|
|
|
1057
1360
|
```html
|
|
@@ -1100,7 +1403,7 @@ If you'll be going ahead to build a real app to see OOHTML in action, you may wa
|
|
|
1100
1403
|
|
|
1101
1404
|
## Examples
|
|
1102
1405
|
|
|
1103
|
-
Here are a few examples in the wide range of use cases these features cover. While we'll demonstrate the most basic
|
|
1406
|
+
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
1407
|
|
|
1105
1408
|
<details><summary>Example 1: <i>Single Page Application</i><br>
|
|
1106
1409
|
└───────── </summary>
|
|
@@ -1295,11 +1598,11 @@ The following is a hypothetical list page!
|
|
|
1295
1598
|
|
|
1296
1599
|
<!-- The "items" template -->
|
|
1297
1600
|
<template def="item" scoped>
|
|
1298
|
-
<li><a
|
|
1601
|
+
<li><a expr="~href: '/animals#'+name;"><?{ index+': '+name }?></a></li>
|
|
1299
1602
|
</template>
|
|
1300
1603
|
|
|
1301
1604
|
<!-- The loop -->
|
|
1302
|
-
<ul
|
|
1605
|
+
<ul expr="@items: (name,index) of ['dog','cat','ram'] / 'item';"></ul>
|
|
1303
1606
|
|
|
1304
1607
|
</section>
|
|
1305
1608
|
```
|