mount-observer 0.1.11 → 0.1.13
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/DefineCustomElementHandler.js +99 -98
- package/ElementMountExtension.js +183 -8
- package/ElementMountExtension.ts +218 -11
- package/EnhanceMountedElementHandler.js +96 -95
- package/Events.js +18 -18
- package/Events.ts +6 -6
- package/EvtRt.js +24 -17
- package/EvtRt.ts +30 -18
- package/MountObserver.js +296 -81
- package/MountObserver.ts +387 -121
- package/README.md +1508 -235
- package/RegistryMountCoordinator.js +125 -0
- package/RegistryMountCoordinator.ts +181 -0
- package/connectionMonitor.js +116 -0
- package/connectionMonitor.ts +164 -0
- package/elementIntersection.js +67 -0
- package/elementIntersection.ts +96 -0
- package/{getRootRegistryContainer.js → getRegistryRoot.js} +1 -1
- package/{getRootRegistryContainer.ts → getRegistryRoot.ts} +1 -1
- package/index.js +15 -10
- package/index.ts +15 -10
- package/mediaQuery.js +1 -1
- package/mediaQuery.ts +1 -1
- package/observedRootHas.js +87 -0
- package/package.json +67 -61
- package/playwright.config.ts +1 -0
- package/rootSizeObserver.js +124 -0
- package/rootSizeObserver.ts +157 -0
- package/upShadowSearch.js +64 -0
- package/upShadowSearch.ts +62 -0
- package/DefineCustomElementHandler.ts +0 -116
- package/EnhanceMountedElementHandler.ts +0 -110
package/README.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
[](https://bundlephobia.com/result?p=mount-observer)
|
|
4
4
|
<img src="http://img.badgesize.io/https://cdn.jsdelivr.net/npm/mount-observer?compression=gzip">
|
|
5
5
|
|
|
6
|
-
Note that much of what is described below has not yet been polyfilled.
|
|
7
6
|
|
|
8
7
|
## Implementation Status
|
|
9
8
|
|
|
@@ -11,8 +10,14 @@ The following features have been implemented and tested:
|
|
|
11
10
|
|
|
12
11
|
### Core Functionality
|
|
13
12
|
- ✅ **matching**: CSS selector-based element matching
|
|
14
|
-
- ✅ **
|
|
13
|
+
- ✅ **whereInstanceOf**: Constructor-based element filtering (single or array)
|
|
14
|
+
- ✅ **whereLocalNameMatches**: Regular expression-based localName filtering
|
|
15
|
+
- ✅ **shouldMount**: Custom JavaScript check for complex mounting conditions
|
|
16
|
+
- ✅ **Registry matching**: Automatic filtering by customElementRegistry (Chrome 146+)
|
|
15
17
|
- ✅ **withMediaMatching**: Media query-based conditional mounting (string or MediaQueryList)
|
|
18
|
+
- ✅ **whereObservedRootSizeMatches**: Container query-based conditional mounting (observes root element size)
|
|
19
|
+
- ✅ **whereElementIntersectsWith**: Intersection observer-based conditional mounting (observes element visibility)
|
|
20
|
+
- ✅ **whereConnectionHas**: Network connection-based conditional mounting (observes connection speed/type)
|
|
16
21
|
- ✅ **withScopePerimeter**: Donut hole scoping (exclude elements inside matching ancestors)
|
|
17
22
|
|
|
18
23
|
### Lifecycle & Events
|
|
@@ -26,17 +31,14 @@ The following features have been implemented and tested:
|
|
|
26
31
|
- ✅ **assignOnDismount**: Property assignment when elements dismount
|
|
27
32
|
- ✅ **stageOnMount**: Reversible property assignment (auto-restores on dismount)
|
|
28
33
|
- ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
|
|
34
|
+
- ✅ **with property**: Hierarchical observer composition with sub-observers
|
|
29
35
|
- ✅ **Element mount extension**: element.mount() method for scoped registry observation
|
|
30
36
|
- ✅ **Shared MutationObserver**: Efficient observer sharing across instances
|
|
31
37
|
- ✅ **Code splitting**: Conditional features loaded on-demand
|
|
32
38
|
- ✅ **Memory management**: WeakRef usage for DOM node references
|
|
33
39
|
|
|
34
40
|
### Not Yet Implemented
|
|
35
|
-
- ❌ Intersection observer integration
|
|
36
|
-
- ❌ Container query support
|
|
37
|
-
- ❌ Shadow DOM traversal utilities
|
|
38
41
|
- ❌ Reconnect event handling
|
|
39
|
-
- ❌ Multiple import types (CSS, JSON, HTML)
|
|
40
42
|
|
|
41
43
|
# The MountObserver API
|
|
42
44
|
|
|
@@ -81,7 +83,10 @@ There is quite a bit of functionality this proposal would open up that is exceed
|
|
|
81
83
|
|
|
82
84
|
3. Knowing when an element previously being monitored passes totally "out-of-scope" so that no more hard references to the element remain. This would allow for cleanup of no longer needed weak references without requiring polling.
|
|
83
85
|
|
|
84
|
-
4. Some CSS selectors, such as the [
|
|
86
|
+
4. Some CSS selectors, such as the [donut hole scope range](https://css-tricks.com/solved-by-css-donuts-scopes/#aa-donut-scoping-with-scope), aren't supported by oEl.querySelectorAll(...) or oEl.matches(...).
|
|
87
|
+
|
|
88
|
+
5. Scoped custom element registries form natural "islands" of DOM that have many commonalities with css "donut hole scoping", and which mutation observers aren't really designed around. The mount-observer is designed to work with scoped custom element registries as first-class citizens.
|
|
89
|
+
|
|
85
90
|
|
|
86
91
|
### Most significant use cases
|
|
87
92
|
|
|
@@ -100,29 +105,26 @@ The extra flexibility this new primitive would provide could be quite useful to
|
|
|
100
105
|
|
|
101
106
|
Before getting into the weeds, let's demonstrate the two most prominent use cases:
|
|
102
107
|
|
|
103
|
-
### Use Case 1: Custom Attribute Enhancement
|
|
108
|
+
### Use Case 1: Custom Attribute Enhancement
|
|
104
109
|
|
|
105
110
|
```html
|
|
106
111
|
<body>
|
|
107
112
|
<div log-to-console="clicked on a div">hello</div>
|
|
108
113
|
|
|
109
114
|
<script type=module>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
document.body.mount([{
|
|
114
|
-
withAttrs:{base: 'log-to-console'},
|
|
115
|
-
spawn: function(el){
|
|
115
|
+
document.mount({
|
|
116
|
+
matching: '[log-to-console]',
|
|
117
|
+
do: (el) => {
|
|
116
118
|
el.addEventListener('click', e => {
|
|
117
119
|
console.log(e.target.getAttribute('log-to-console'));
|
|
118
120
|
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
121
123
|
</script>
|
|
122
124
|
</body>
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
|
|
126
128
|
|
|
127
129
|
### Use Case 2: Lazy Global Custom Element Definition
|
|
128
130
|
|
|
@@ -153,6 +155,8 @@ document.mount({
|
|
|
153
155
|
|
|
154
156
|
This registers custom elements with the global customElements registry.
|
|
155
157
|
|
|
158
|
+
See [this extending package](https://github.com/bahrus/mount-observer-script-element) that provides for a more declarative approach.
|
|
159
|
+
|
|
156
160
|
### Scoped
|
|
157
161
|
|
|
158
162
|
To register the class in the same custom element registry as the element which calls the "mount" method (element in this case), use "builtIns.defineScopedCustomElement":
|
|
@@ -167,19 +171,19 @@ element.mount({
|
|
|
167
171
|
|
|
168
172
|
## Enhancing Elements with assign-gingerly
|
|
169
173
|
|
|
170
|
-
The `builtIns.enhanceMountedElement` handler automatically enhances mounted elements using the [assign-gingerly](https://www.npmjs.com/package/assign-gingerly) enhancement system. This allows
|
|
174
|
+
The `builtIns.enhanceMountedElement` handler automatically enhances mounted elements using the [assign-gingerly](https://www.npmjs.com/package/assign-gingerly) enhancement system. This allows us to attach behavior and state to elements without subclassing.
|
|
171
175
|
|
|
172
176
|
```JavaScript
|
|
173
177
|
// MyEnhancement.js
|
|
174
178
|
class ButtonEnhancement {
|
|
175
179
|
constructor(element, ctx, initVals) {
|
|
176
|
-
this.element = element;
|
|
180
|
+
this.element = new WeakRef(element);
|
|
177
181
|
this.ctx = ctx;
|
|
178
182
|
this.clickCount = 0;
|
|
179
183
|
|
|
180
|
-
element.addEventListener('click', () => {
|
|
184
|
+
element.addEventListener('click', ({target}) => {
|
|
181
185
|
this.clickCount++;
|
|
182
|
-
|
|
186
|
+
target.setAttribute('data-clicks', this.clickCount);
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
189
|
}
|
|
@@ -190,7 +194,6 @@ export default {
|
|
|
190
194
|
};
|
|
191
195
|
|
|
192
196
|
// main.js
|
|
193
|
-
import 'mount-observer/ElementMountExtension.js';
|
|
194
197
|
|
|
195
198
|
document.mount({
|
|
196
199
|
matching: '.enhance-me',
|
|
@@ -209,11 +212,799 @@ console.log(button.enh.buttonEnh.clickCount); // 1
|
|
|
209
212
|
```
|
|
210
213
|
|
|
211
214
|
The handler:
|
|
212
|
-
1. Searches the imported module for an export with a `spawn` property (the enhancement class)
|
|
215
|
+
1. Searches the imported module for an export with a `spawn` property (the enhancement class), starting with default.
|
|
213
216
|
2. Calls `element.enh.get(registryItem, context)` to spawn the enhancement
|
|
214
217
|
3. Stores the enhancement instance on `element.enh[enhKey]` if an `enhKey` is provided
|
|
215
218
|
|
|
216
|
-
|
|
219
|
+
|
|
220
|
+
## Loading ES Modules from Script Elements
|
|
221
|
+
|
|
222
|
+
The `builtIns.scriptNoModule` handler enables declarative module loading using `<script nomodule>` elements. This provides a way to import ES modules and JSON data directly from HTML without writing JavaScript.
|
|
223
|
+
|
|
224
|
+
```html
|
|
225
|
+
<!-- Load a JavaScript module -->
|
|
226
|
+
<script nomodule src="./config.js" id="myConfig"></script>
|
|
227
|
+
|
|
228
|
+
<!-- Load JSON data with import assertion -->
|
|
229
|
+
<script nomodule src="./data.json" with-type="json" id="myData"></script>
|
|
230
|
+
|
|
231
|
+
<script type="module">
|
|
232
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
233
|
+
|
|
234
|
+
// Handler provides matching and whereInstanceOf via static properties
|
|
235
|
+
const observer = new MountObserver({
|
|
236
|
+
do: 'builtIns.scriptNoModule'
|
|
237
|
+
});
|
|
238
|
+
observer.observe(document);
|
|
239
|
+
|
|
240
|
+
// Access the imported modules
|
|
241
|
+
const config = document.getElementById('myConfig').export;
|
|
242
|
+
const data = document.getElementById('myData').export.default;
|
|
243
|
+
|
|
244
|
+
console.log(config.mountConfig); // Access exported values
|
|
245
|
+
console.log(data); // Access JSON data
|
|
246
|
+
</script>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**How it works:**
|
|
250
|
+
1. The handler matches `script[nomodule][src]` elements (via static properties)
|
|
251
|
+
2. Reads the `src` attribute and resolves it relative to the document
|
|
252
|
+
3. Checks for optional `with-type` attribute for import assertions (e.g., `"json"`)
|
|
253
|
+
4. Dynamically imports the module: `import(src, { with: { type: withType } })`
|
|
254
|
+
5. Stores the imported module on `element.export`
|
|
255
|
+
|
|
256
|
+
**Benefits:**
|
|
257
|
+
- Declarative module loading directly in HTML
|
|
258
|
+
- Supports JSON imports with `with-type="json"` attribute
|
|
259
|
+
- No need to specify `matching` or `whereInstanceOf` (handler provides defaults)
|
|
260
|
+
- Modules are accessible via `scriptElement.export`
|
|
261
|
+
- Works with relative and absolute URLs
|
|
262
|
+
|
|
263
|
+
**Use cases:**
|
|
264
|
+
- Loading configuration files declaratively
|
|
265
|
+
- Importing JSON data without fetch
|
|
266
|
+
- Progressive enhancement with module loading
|
|
267
|
+
- Declarative dependency management in HTML
|
|
268
|
+
|
|
269
|
+
## Mount Observer Script Elements (MOSEs)
|
|
270
|
+
|
|
271
|
+
The `builtIns.mountObserverScript` handler enables fully declarative mount observer configuration using `<script type="mountobserver">` elements. This provides the ultimate in HTML-first progressive enhancement.
|
|
272
|
+
|
|
273
|
+
```html
|
|
274
|
+
<!-- Inline JSON configuration -->
|
|
275
|
+
<script type="mountobserver">
|
|
276
|
+
{
|
|
277
|
+
"matching": "my-fancy-button",
|
|
278
|
+
"import": "./fancy-button.js",
|
|
279
|
+
"do": "builtIns.defineCustomElement"
|
|
280
|
+
}
|
|
281
|
+
</script>
|
|
282
|
+
|
|
283
|
+
<!-- External JSON configuration -->
|
|
284
|
+
<script type="mountobserver" src="./observer-config.json"></script>
|
|
285
|
+
|
|
286
|
+
<!-- Bootstrap the handler -->
|
|
287
|
+
<script type="module">
|
|
288
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
289
|
+
|
|
290
|
+
// Handler provides matching and whereInstanceOf via static properties
|
|
291
|
+
const observer = new MountObserver({
|
|
292
|
+
do: 'builtIns.mountObserverScript'
|
|
293
|
+
});
|
|
294
|
+
observer.observe(document);
|
|
295
|
+
</script>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**How it works:**
|
|
299
|
+
1. The handler matches `script[type="mountobserver"]` elements (via static properties)
|
|
300
|
+
2. If the script has a `src` attribute, imports JSON from that URL
|
|
301
|
+
3. Otherwise, parses the script's textContent as JSON
|
|
302
|
+
4. Calls `scriptElement.mount(config)` with the parsed configuration
|
|
303
|
+
5. The `mount()` method creates a MountObserver for that configuration
|
|
304
|
+
|
|
305
|
+
**Benefits:**
|
|
306
|
+
- Zero JavaScript required for observer configuration
|
|
307
|
+
- Configurations are pure JSON (fully serializable)
|
|
308
|
+
- Easy to generate server-side or from build tools
|
|
309
|
+
- Supports both inline and external configurations
|
|
310
|
+
- Leverages the `element.mount()` API for automatic scope management
|
|
311
|
+
- No need to specify `matching` or `whereInstanceOf` for the handler itself
|
|
312
|
+
|
|
313
|
+
**Use cases:**
|
|
314
|
+
- Server-side rendering with progressive enhancement
|
|
315
|
+
- Build-time generation of observer configurations
|
|
316
|
+
- CMS-driven component loading
|
|
317
|
+
- Declarative micro-frontend architecture
|
|
318
|
+
- Configuration management without JavaScript bundling
|
|
319
|
+
|
|
320
|
+
**Example with multiple configurations:**
|
|
321
|
+
```html
|
|
322
|
+
<!-- Load custom elements -->
|
|
323
|
+
<script type="mountobserver">
|
|
324
|
+
{
|
|
325
|
+
"matching": "my-button",
|
|
326
|
+
"import": "./components/my-button.js",
|
|
327
|
+
"do": "builtIns.defineCustomElement"
|
|
328
|
+
}
|
|
329
|
+
</script>
|
|
330
|
+
|
|
331
|
+
<!-- Enhance existing elements -->
|
|
332
|
+
<script type="mountobserver">
|
|
333
|
+
{
|
|
334
|
+
"matching": ".interactive",
|
|
335
|
+
"import": "./enhancements/interactive.js",
|
|
336
|
+
"do": "builtIns.enhanceMountedElement"
|
|
337
|
+
}
|
|
338
|
+
</script>
|
|
339
|
+
|
|
340
|
+
<!-- Single bootstrap script activates all configurations -->
|
|
341
|
+
<script type="module">
|
|
342
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
343
|
+
|
|
344
|
+
new MountObserver({
|
|
345
|
+
do: 'builtIns.mountObserverScript'
|
|
346
|
+
}).observe(document);
|
|
347
|
+
</script>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Hoisting Templates for Performance
|
|
351
|
+
|
|
352
|
+
The `builtIns.hoistTemplate` handler optimizes template usage by moving a template element's content from shadow roots to `document.head`. This is particularly useful when templates with IDs are repeated across multiple custom elements.
|
|
353
|
+
|
|
354
|
+
**Why hoist templates?**
|
|
355
|
+
|
|
356
|
+
When HTML-first custom elements repeat throughout a page, each instance typically contains its own copy of template content. Moving these templates to a centralized location:
|
|
357
|
+
- Reduces memory usage (one template instead of many copies)
|
|
358
|
+
- Improves cloning performance (single source of truth)
|
|
359
|
+
- Maintains the same API through the `remoteContent` getter
|
|
360
|
+
|
|
361
|
+
**Basic usage:**
|
|
362
|
+
|
|
363
|
+
```html
|
|
364
|
+
<my-web-component>
|
|
365
|
+
#shadow
|
|
366
|
+
<template id="my-template">
|
|
367
|
+
<div>My content</div>
|
|
368
|
+
</template>
|
|
369
|
+
</my-web-component>
|
|
370
|
+
|
|
371
|
+
<script type="module">
|
|
372
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
373
|
+
|
|
374
|
+
const observer = new MountObserver({
|
|
375
|
+
do: 'builtIns.hoistTemplate'
|
|
376
|
+
});
|
|
377
|
+
observer.observe(document);
|
|
378
|
+
</script>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**What happens:**
|
|
382
|
+
1. The handler finds templates with IDs in shadow roots
|
|
383
|
+
2. Moves the template content to a new template in `<head>`
|
|
384
|
+
3. Updates the original template with `src="#mount-observer-0"` (unique ID)
|
|
385
|
+
4. Defines a `remoteContent` getter that returns the hoisted template's content
|
|
386
|
+
|
|
387
|
+
**Accessing hoisted content:**
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
const template = shadowRoot.querySelector('#my-template');
|
|
391
|
+
|
|
392
|
+
// After hoisting, use remoteContent to access the content
|
|
393
|
+
const content = template.remoteContent; // Returns DocumentFragment
|
|
394
|
+
const clone = content.cloneNode(true); // Clone the content
|
|
395
|
+
```
|
|
396
|
+
<details>
|
|
397
|
+
<summary>Matching criteria
|
|
398
|
+
|
|
399
|
+
The handler automatically hoists templates that:
|
|
400
|
+
- Have an `id` attribute
|
|
401
|
+
- Don't already have a `src` attribute
|
|
402
|
+
- Are in a shadow root (or disconnected, being cloned)
|
|
403
|
+
- Have content (empty templates are skipped)
|
|
404
|
+
|
|
405
|
+
**Declarative usage with MOSE:**
|
|
406
|
+
|
|
407
|
+
```html
|
|
408
|
+
<script type="mountobserver">
|
|
409
|
+
{
|
|
410
|
+
"do": "builtIns.hoistTemplate"
|
|
411
|
+
}
|
|
412
|
+
</script>
|
|
413
|
+
|
|
414
|
+
<script type="module">
|
|
415
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
416
|
+
|
|
417
|
+
new MountObserver({
|
|
418
|
+
do: 'builtIns.mountObserverScript'
|
|
419
|
+
}).observe(document);
|
|
420
|
+
</script>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
[Implemented as HoistingTemplates requirement](requirements/Done/HoistingTemplates.md)
|
|
424
|
+
|
|
425
|
+
</details>
|
|
426
|
+
|
|
427
|
+
## Intra-Document HTML Includes with HTMLInclude
|
|
428
|
+
|
|
429
|
+
The `builtIns.HTMLInclude` handler enables declarative HTML fragment reuse within a document using `<template src="#id">` syntax. Think of it as "constants for HTML" - define content once with an ID, then reference it multiple times throughout your document.
|
|
430
|
+
|
|
431
|
+
**Why use HTML includes?**
|
|
432
|
+
|
|
433
|
+
- Reduces duplication of repeated HTML structures
|
|
434
|
+
- Enables template-based content generation
|
|
435
|
+
- Supports partial updates via matching insertions
|
|
436
|
+
- Works across shadow DOM boundaries
|
|
437
|
+
- Supports declarative shadow DOM attachment
|
|
438
|
+
- Caches lookups for performance
|
|
439
|
+
- Detects circular references automatically
|
|
440
|
+
- Can be used to inherit from MOSEs
|
|
441
|
+
|
|
442
|
+
**Basic usage - Simple cloning:**
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
<!-- Define reusable content -->
|
|
446
|
+
<div id="reusable">
|
|
447
|
+
<p>This content can be reused</p>
|
|
448
|
+
<button>Click me</button>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<!-- Reference it with a template -->
|
|
452
|
+
<template src="#reusable"></template>
|
|
453
|
+
|
|
454
|
+
<!-- Results in: -->
|
|
455
|
+
<div>
|
|
456
|
+
<p>This content can be reused</p>
|
|
457
|
+
<button>Click me</button>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<script type="module">
|
|
461
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
462
|
+
|
|
463
|
+
const observer = new MountObserver({
|
|
464
|
+
do: 'builtIns.HTMLInclude'
|
|
465
|
+
});
|
|
466
|
+
observer.observe(document);
|
|
467
|
+
</script>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**What happens:**
|
|
471
|
+
1. The handler finds templates with `src` attributes starting with `#`
|
|
472
|
+
2. Searches for an element with that ID (across shadow boundaries)
|
|
473
|
+
3. Clones the content from the source element
|
|
474
|
+
4. Replaces the template with the cloned content
|
|
475
|
+
5. Removes the `id` attribute from cloned elements to avoid duplicate IDs
|
|
476
|
+
|
|
477
|
+
**Cloning priority:**
|
|
478
|
+
1. `remoteContent` property (hoisted templates) - highest priority
|
|
479
|
+
2. `content` property (regular templates)
|
|
480
|
+
3. The element itself (any element with an ID)
|
|
481
|
+
|
|
482
|
+
**Works with hoisted templates:**
|
|
483
|
+
|
|
484
|
+
```html
|
|
485
|
+
<my-web-component>
|
|
486
|
+
#shadow
|
|
487
|
+
<template id="my-template">
|
|
488
|
+
<div>Hoisted content</div>
|
|
489
|
+
</template>
|
|
490
|
+
</my-web-component>
|
|
491
|
+
|
|
492
|
+
<!-- After hoisting, this still works -->
|
|
493
|
+
<template src="#my-template"></template>
|
|
494
|
+
|
|
495
|
+
<script type="module">
|
|
496
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
497
|
+
|
|
498
|
+
// First hoist templates
|
|
499
|
+
new MountObserver({
|
|
500
|
+
do: 'builtIns.hoistTemplate'
|
|
501
|
+
}).observe(document);
|
|
502
|
+
|
|
503
|
+
// Then use them
|
|
504
|
+
new MountObserver({
|
|
505
|
+
do: 'builtIns.HTMLInclude'
|
|
506
|
+
}).observe(document);
|
|
507
|
+
</script>
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Shadow DOM Support
|
|
511
|
+
|
|
512
|
+
The HTMLInclude handler supports declarative shadow DOM attachment using the `shadowrootmodeonload` attribute. This allows you to attach cloned content directly to a parent element's shadow root, similar to the platform's [declarative shadow DOM](https://web.dev/articles/declarative-shadow-dom) feature.
|
|
513
|
+
|
|
514
|
+
**Basic shadow DOM usage:**
|
|
515
|
+
|
|
516
|
+
```html
|
|
517
|
+
<!-- Define reusable shadow content -->
|
|
518
|
+
<template id="shadow-content">
|
|
519
|
+
<style>
|
|
520
|
+
:host {
|
|
521
|
+
display: block;
|
|
522
|
+
padding: 10px;
|
|
523
|
+
}
|
|
524
|
+
.shadow-text {
|
|
525
|
+
color: blue;
|
|
526
|
+
}
|
|
527
|
+
</style>
|
|
528
|
+
<div class="shadow-text">
|
|
529
|
+
<slot name="greeting"></slot>
|
|
530
|
+
<slot></slot>
|
|
531
|
+
</div>
|
|
532
|
+
</template>
|
|
533
|
+
|
|
534
|
+
<!-- Attach to shadow root -->
|
|
535
|
+
<div class="host-element">
|
|
536
|
+
<template src="#shadow-content" shadowrootmodeonload="open"></template>
|
|
537
|
+
<span slot="greeting">Hello</span>
|
|
538
|
+
<span>World!</span>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<script type="module">
|
|
542
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
543
|
+
|
|
544
|
+
new MountObserver({
|
|
545
|
+
do: 'builtIns.HTMLInclude'
|
|
546
|
+
}).observe(document);
|
|
547
|
+
</script>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**What happens:**
|
|
551
|
+
1. The handler checks for the `shadowrootmodeonload` attribute (case-insensitive)
|
|
552
|
+
2. If present, it attaches the cloned content to the parent element's shadow root
|
|
553
|
+
3. If the parent doesn't have a shadow root, one is created with the specified mode
|
|
554
|
+
4. If a shadow root already exists, the content is appended to it
|
|
555
|
+
5. The template is removed as usual
|
|
556
|
+
|
|
557
|
+
**Shadow root modes:**
|
|
558
|
+
- `open` - Shadow root is accessible via `element.shadowRoot`
|
|
559
|
+
- `closed` - Shadow root is not accessible from outside
|
|
560
|
+
|
|
561
|
+
**Slots work automatically:**
|
|
562
|
+
|
|
563
|
+
The native browser slot mechanism handles content distribution. Light DOM elements with `slot` attributes are automatically projected into the corresponding `<slot>` elements in the shadow DOM.
|
|
564
|
+
|
|
565
|
+
**Example - Complex nested structure:**
|
|
566
|
+
|
|
567
|
+
```html
|
|
568
|
+
<template id="chorus">
|
|
569
|
+
<template src="#beautiful">
|
|
570
|
+
<span slot="subjectIs">
|
|
571
|
+
<slot name="subjectIs1"></slot>
|
|
572
|
+
</span>
|
|
573
|
+
</template>
|
|
574
|
+
<div>No matter what they say</div>
|
|
575
|
+
<div>Words <slot name="verb1"></slot> bring <slot name="pronoun1"></slot> down</div>
|
|
576
|
+
</template>
|
|
577
|
+
|
|
578
|
+
<div class="chorus">
|
|
579
|
+
<template src="#chorus" shadowrootmodeonload="open"></template>
|
|
580
|
+
<span slot="verb1">can't</span>
|
|
581
|
+
<span slot="pronoun1">me</span>
|
|
582
|
+
<span slot="subjectIs1">I am</span>
|
|
583
|
+
</div>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Nested templates in shadow DOM:**
|
|
587
|
+
|
|
588
|
+
Templates inside shadow roots are not automatically processed by the parent observer. To process nested templates, you need to observe the shadow root separately:
|
|
589
|
+
|
|
590
|
+
```javascript
|
|
591
|
+
const host = document.querySelector('.host-element');
|
|
592
|
+
if (host.shadowRoot) {
|
|
593
|
+
const shadowObserver = new MountObserver({
|
|
594
|
+
do: 'builtIns.HTMLInclude'
|
|
595
|
+
});
|
|
596
|
+
await shadowObserver.observe(host.shadowRoot);
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
**Error handling:**
|
|
601
|
+
|
|
602
|
+
- Invalid mode values: Logs warning if mode is not `"open"` or `"closed"`
|
|
603
|
+
- Missing parent: Logs warning if template has no parent element
|
|
604
|
+
- Attachment failures: Logs error if shadow root cannot be attached
|
|
605
|
+
|
|
606
|
+
### Matching Insertions - Partial Updates
|
|
607
|
+
|
|
608
|
+
When a template has children, they are used to match elements in the cloned content and selectively update them. This enables partial modifications and "nulling out" content without duplicating the entire structure.
|
|
609
|
+
|
|
610
|
+
**How it works:**
|
|
611
|
+
1. Template children generate CSS selectors (tag, classes, attributes)
|
|
612
|
+
2. Matching elements in the cloned content are found
|
|
613
|
+
3. Matched elements have their children replaced and attributes updated
|
|
614
|
+
4. The `-i` attribute specifies which attributes to update
|
|
615
|
+
|
|
616
|
+
**Example - Updating attributes:**
|
|
617
|
+
|
|
618
|
+
```html
|
|
619
|
+
<!-- Source content -->
|
|
620
|
+
<div itemscope id="love">
|
|
621
|
+
<data value="false" itemprop="todayIsFriday">It's Thursday</data>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<!-- Template with matching insertion -->
|
|
625
|
+
<template src="#love">
|
|
626
|
+
<data value="true" itemprop="todayIsFriday" -i="value"></data>
|
|
627
|
+
</template>
|
|
628
|
+
|
|
629
|
+
<!-- Results in: -->
|
|
630
|
+
<div itemscope>
|
|
631
|
+
<data value="true" itemprop="todayIsFriday">It's Thursday</data>
|
|
632
|
+
</div>
|
|
633
|
+
<!-- The value attribute is updated, but content stays "It's Thursday" -->
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**The `-i` attribute:**
|
|
637
|
+
|
|
638
|
+
The `-i` (insert) attribute is a space-separated list of attribute names to update on matched elements. Attributes listed in `-i` are:
|
|
639
|
+
- Excluded from the CSS selector (allows matching elements with different values)
|
|
640
|
+
- Updated on matched elements with values from the template child
|
|
641
|
+
|
|
642
|
+
```html
|
|
643
|
+
<template src="#form">
|
|
644
|
+
<!-- Update both value and placeholder -->
|
|
645
|
+
<input type="text" name="username" value="new" placeholder="Updated" -i="value placeholder">
|
|
646
|
+
</template>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**Example - Replacing content:**
|
|
650
|
+
|
|
651
|
+
```html
|
|
652
|
+
<!-- Source -->
|
|
653
|
+
<div id="greeting">
|
|
654
|
+
<p class="message">Hello</p>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<!-- Template replaces content -->
|
|
658
|
+
<template src="#greeting">
|
|
659
|
+
<p class="message">Goodbye</p>
|
|
660
|
+
</template>
|
|
661
|
+
|
|
662
|
+
<!-- Results in: -->
|
|
663
|
+
<div>
|
|
664
|
+
<p class="message">Goodbye</p>
|
|
665
|
+
</div>
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Example - Multiple matching elements:**
|
|
669
|
+
|
|
670
|
+
```html
|
|
671
|
+
<!-- Source with multiple items -->
|
|
672
|
+
<div id="list">
|
|
673
|
+
<span class="item">Item 1</span>
|
|
674
|
+
<span class="item">Item 2</span>
|
|
675
|
+
<span class="item">Item 3</span>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<!-- Update all matching items -->
|
|
679
|
+
<template src="#list">
|
|
680
|
+
<span class="item">Updated</span>
|
|
681
|
+
</template>
|
|
682
|
+
|
|
683
|
+
<!-- Results in: -->
|
|
684
|
+
<div>
|
|
685
|
+
<span class="item">Updated</span>
|
|
686
|
+
<span class="item">Updated</span>
|
|
687
|
+
<span class="item">Updated</span>
|
|
688
|
+
</div>
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Example - Nulling out content:**
|
|
692
|
+
|
|
693
|
+
```html
|
|
694
|
+
<!-- Source -->
|
|
695
|
+
<div id="status">
|
|
696
|
+
<span data-active="false" class="indicator">Inactive</span>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<!-- Update attribute, remove content -->
|
|
700
|
+
<template src="#status">
|
|
701
|
+
<span data-active="true" class="indicator" -i="data-active"></span>
|
|
702
|
+
</template>
|
|
703
|
+
|
|
704
|
+
<!-- Results in: -->
|
|
705
|
+
<div>
|
|
706
|
+
<span data-active="true" class="indicator"></span>
|
|
707
|
+
</div>
|
|
708
|
+
<!-- Content is removed, attribute is updated -->
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Use Case: Inheriting Groups of Mount-Observers
|
|
712
|
+
|
|
713
|
+
Matching insertions become particularly powerful when combined with Mount Observer Script Elements (MOSEs) for inheriting and customizing groups of mount-observers across shadow DOM boundaries.
|
|
714
|
+
|
|
715
|
+
**Scenario:** You have a base component with a set of mount-observers defined in its shadow root, and you want to reuse those observers in other components while making targeted modifications.
|
|
716
|
+
|
|
717
|
+
```html
|
|
718
|
+
<!-- Base component with mount-observers -->
|
|
719
|
+
<template id="base-observers">
|
|
720
|
+
<script type="mountobserver">
|
|
721
|
+
{
|
|
722
|
+
"matching": "button.primary",
|
|
723
|
+
"import": "./primary-button.js",
|
|
724
|
+
"do": "builtIns.defineCustomElement"
|
|
725
|
+
}
|
|
726
|
+
</script>
|
|
727
|
+
|
|
728
|
+
<script type="mountobserver">
|
|
729
|
+
{
|
|
730
|
+
"matching": ".interactive",
|
|
731
|
+
"import": "./interactive.js",
|
|
732
|
+
"do": "builtIns.enhanceMountedElement"
|
|
733
|
+
}
|
|
734
|
+
</script>
|
|
735
|
+
|
|
736
|
+
<script type="mountobserver">
|
|
737
|
+
{
|
|
738
|
+
"matching": "form",
|
|
739
|
+
"import": "./form-validator.js",
|
|
740
|
+
"do": "builtIns.enhanceMountedElement"
|
|
741
|
+
}
|
|
742
|
+
</script>
|
|
743
|
+
</template>
|
|
744
|
+
|
|
745
|
+
<!-- Derived component - inherit and customize -->
|
|
746
|
+
<my-derived-component>
|
|
747
|
+
#shadow
|
|
748
|
+
<!-- Include base observers -->
|
|
749
|
+
<template src="#base-observers">
|
|
750
|
+
<!-- Override the form validator with a different one -->
|
|
751
|
+
<script type="mountobserver">
|
|
752
|
+
{
|
|
753
|
+
"matching": "form",
|
|
754
|
+
"import": "./custom-form-validator.js",
|
|
755
|
+
"do": "builtIns.enhanceMountedElement"
|
|
756
|
+
}
|
|
757
|
+
</script>
|
|
758
|
+
</template>
|
|
759
|
+
|
|
760
|
+
<!-- Component content -->
|
|
761
|
+
<form>...</form>
|
|
762
|
+
<button class="primary">Submit</button>
|
|
763
|
+
</my-derived-component>
|
|
764
|
+
|
|
765
|
+
<script type="module">
|
|
766
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
767
|
+
|
|
768
|
+
// Bootstrap HTMLInclude handler
|
|
769
|
+
new MountObserver({
|
|
770
|
+
do: 'builtIns.HTMLInclude'
|
|
771
|
+
}).observe(document);
|
|
772
|
+
|
|
773
|
+
// Bootstrap MOSE handler to activate the observers
|
|
774
|
+
new MountObserver({
|
|
775
|
+
do: 'builtIns.mountObserverScript'
|
|
776
|
+
}).observe(document);
|
|
777
|
+
</script>
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**What happens:**
|
|
781
|
+
1. The `<template src="#base-observers">` clones all three MOSE scripts
|
|
782
|
+
2. The matching insertion finds the form validator script (matching by `matching` attribute)
|
|
783
|
+
3. Replaces its content with the custom validator configuration
|
|
784
|
+
4. All three scripts are inserted into the shadow root
|
|
785
|
+
5. The MOSE handler activates all observers in the shadow root's registry scope
|
|
786
|
+
|
|
787
|
+
**Benefits:**
|
|
788
|
+
- **Composition**: Build complex observer configurations from reusable pieces
|
|
789
|
+
- **Inheritance**: Derive new components with modified observer behavior
|
|
790
|
+
- **Scoped registries**: Each shadow root gets its own set of observers
|
|
791
|
+
- **Declarative**: No JavaScript required for observer inheritance
|
|
792
|
+
- **Maintainable**: Update base observers in one place, changes propagate
|
|
793
|
+
|
|
794
|
+
**Advanced pattern - Multiple inheritance:**
|
|
795
|
+
|
|
796
|
+
```html
|
|
797
|
+
<!-- Base UI observers -->
|
|
798
|
+
<template id="ui-observers">
|
|
799
|
+
<script type="mountobserver">{"matching": "button", ...}</script>
|
|
800
|
+
<script type="mountobserver">{"matching": "input", ...}</script>
|
|
801
|
+
</template>
|
|
802
|
+
|
|
803
|
+
<!-- Base data observers -->
|
|
804
|
+
<template id="data-observers">
|
|
805
|
+
<script type="mountobserver">{"matching": "[itemscope]", ...}</script>
|
|
806
|
+
</template>
|
|
807
|
+
|
|
808
|
+
<!-- Component combines both -->
|
|
809
|
+
<my-component>
|
|
810
|
+
#shadow
|
|
811
|
+
<template src="#ui-observers"></template>
|
|
812
|
+
<template src="#data-observers"></template>
|
|
813
|
+
|
|
814
|
+
<!-- Add component-specific observers -->
|
|
815
|
+
<script type="mountobserver">
|
|
816
|
+
{
|
|
817
|
+
"matching": ".special",
|
|
818
|
+
"import": "./special.js",
|
|
819
|
+
"do": "builtIns.enhanceMountedElement"
|
|
820
|
+
}
|
|
821
|
+
</script>
|
|
822
|
+
</my-component>
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
This pattern enables:
|
|
826
|
+
- **Mixins**: Combine multiple observer groups
|
|
827
|
+
- **Layering**: Stack observers from different concerns (UI, data, behavior)
|
|
828
|
+
- **Customization**: Override specific observers while keeping others
|
|
829
|
+
- **Reusability**: Share observer configurations across components
|
|
830
|
+
|
|
831
|
+
**Declarative usage with MOSE:**
|
|
832
|
+
|
|
833
|
+
```html
|
|
834
|
+
<script type="mountobserver">
|
|
835
|
+
{
|
|
836
|
+
"do": "builtIns.HTMLInclude"
|
|
837
|
+
}
|
|
838
|
+
</script>
|
|
839
|
+
|
|
840
|
+
<script type="module">
|
|
841
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
842
|
+
|
|
843
|
+
new MountObserver({
|
|
844
|
+
do: 'builtIns.mountObserverScript'
|
|
845
|
+
}).observe(document);
|
|
846
|
+
</script>
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
**Error handling:**
|
|
850
|
+
|
|
851
|
+
The handler provides helpful error messages:
|
|
852
|
+
- Missing elements: `data-include-error="Element with id='foo' not found"`
|
|
853
|
+
- Circular references: `data-include-error="Circular reference detected: #foo"`
|
|
854
|
+
- Clone failures: `data-include-error="Unable to clone content from #foo"`
|
|
855
|
+
|
|
856
|
+
**Performance:**
|
|
857
|
+
|
|
858
|
+
- Uses WeakMap caching for repeated ID lookups
|
|
859
|
+
- Efficient for scenarios like periodic tables with many repeated elements
|
|
860
|
+
- Searches across shadow boundaries using `upShadowSearch`
|
|
861
|
+
- Cleans up cache entries when elements are garbage collected
|
|
862
|
+
|
|
863
|
+
[Implemented as MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes requirement](requirements/Done/MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes.md)
|
|
864
|
+
|
|
865
|
+
## Automatic ID Generation with genIds
|
|
866
|
+
|
|
867
|
+
The `builtIns.generateIds` handler automatically generates unique IDs for elements within scoped containers using the [id-generation](https://www.npmjs.com/package/id-generation) package. This is particularly useful for forms, microdata structures, and any scenario where you need unique IDs for accessibility or linking purposes.
|
|
868
|
+
|
|
869
|
+
**Why use automatic ID generation?**
|
|
870
|
+
|
|
871
|
+
- Eliminates manual ID management and conflicts
|
|
872
|
+
- Supports scoped ID generation within fieldsets or itemscope containers
|
|
873
|
+
- Automatically updates ID references in attributes (aria-labelledby, for, etc.)
|
|
874
|
+
- Provides shorthand syntax for common patterns
|
|
875
|
+
- Handles deferred attribute activation
|
|
876
|
+
|
|
877
|
+
**Basic usage:**
|
|
878
|
+
|
|
879
|
+
```html
|
|
880
|
+
<fieldset disabled>
|
|
881
|
+
<label defer-for="for: #{{username}}">Username:</label>
|
|
882
|
+
<input data-id="username" type="text">
|
|
883
|
+
|
|
884
|
+
<label defer-for="for: #{{password}}">Password:</label>
|
|
885
|
+
<input data-id="password" type="password">
|
|
886
|
+
|
|
887
|
+
<button -id>Generate IDs</button>
|
|
888
|
+
</fieldset>
|
|
889
|
+
|
|
890
|
+
<script type="module">
|
|
891
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
892
|
+
|
|
893
|
+
const observer = new MountObserver({
|
|
894
|
+
do: 'builtIns.generateIds'
|
|
895
|
+
});
|
|
896
|
+
observer.observe(document);
|
|
897
|
+
</script>
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**What happens:**
|
|
901
|
+
|
|
902
|
+
1. The handler watches for elements with the `-id` attribute (the trigger)
|
|
903
|
+
2. Finds the nearest scope container (fieldset, [itemscope], or root)
|
|
904
|
+
3. Generates unique IDs for elements with `data-id`, `#`, `@`, or `|` attributes
|
|
905
|
+
4. Replaces `#{{name}}` references with generated IDs in attributes
|
|
906
|
+
5. Removes `-id` and `defer-*` attributes after processing
|
|
907
|
+
6. Removes `disabled` from fieldset containers
|
|
908
|
+
|
|
909
|
+
**Shorthand attributes:**
|
|
910
|
+
|
|
911
|
+
```html
|
|
912
|
+
<fieldset>
|
|
913
|
+
<!-- # uses element's tag name -->
|
|
914
|
+
<input # type="text"> <!-- becomes data-id="input" -->
|
|
915
|
+
|
|
916
|
+
<!-- @ uses element's name attribute -->
|
|
917
|
+
<input @ name="email" type="email"> <!-- becomes data-id="email" -->
|
|
918
|
+
|
|
919
|
+
<!-- | uses element's itemprop attribute -->
|
|
920
|
+
<span | itemprop="price">$99</span> <!-- becomes data-id="price" -->
|
|
921
|
+
|
|
922
|
+
<button -id>Generate IDs</button>
|
|
923
|
+
</fieldset>
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
**Side effects with data-id:**
|
|
927
|
+
|
|
928
|
+
The `data-id` attribute supports special symbols that trigger side effects:
|
|
929
|
+
|
|
930
|
+
```html
|
|
931
|
+
<fieldset>
|
|
932
|
+
<!-- @ sets name attribute -->
|
|
933
|
+
<input data-id="@ username" type="text">
|
|
934
|
+
<!-- Result: id="gid-0" name="username" data-id="username" -->
|
|
935
|
+
|
|
936
|
+
<!-- | sets itemprop attribute -->
|
|
937
|
+
<span data-id="| price">$99</span>
|
|
938
|
+
<!-- Result: id="gid-1" itemprop="price" data-id="price" -->
|
|
939
|
+
|
|
940
|
+
<!-- $ sets itemscope and itemprop -->
|
|
941
|
+
<div data-id="$ product">...</div>
|
|
942
|
+
<!-- Result: id="gid-2" itemscope itemprop="product" data-id="product" -->
|
|
943
|
+
|
|
944
|
+
<!-- . adds to class attribute -->
|
|
945
|
+
<div data-id=". highlight">Content</div>
|
|
946
|
+
<!-- Result: id="gid-3" class="highlight" data-id="highlight" -->
|
|
947
|
+
|
|
948
|
+
<!-- % adds to part attribute -->
|
|
949
|
+
<div data-id="% header">Header</div>
|
|
950
|
+
<!-- Result: id="gid-4" part="header" data-id="header" -->
|
|
951
|
+
|
|
952
|
+
<button -id>Generate IDs</button>
|
|
953
|
+
</fieldset>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Deferred attributes:**
|
|
957
|
+
|
|
958
|
+
Use `defer-*` prefix to prevent attributes from being applied until IDs are generated:
|
|
959
|
+
|
|
960
|
+
```html
|
|
961
|
+
<fieldset disabled>
|
|
962
|
+
<!-- These attributes won't work until IDs are generated -->
|
|
963
|
+
<label defer-for="for: #{{email}}">Email:</label>
|
|
964
|
+
<input data-id="email" type="email" defer-aria-describedby="aria-describedby: #{{emailHelp}}">
|
|
965
|
+
<span data-id="emailHelp">Enter your email address</span>
|
|
966
|
+
|
|
967
|
+
<button -id>Activate Form</button>
|
|
968
|
+
</fieldset>
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**Supported reference attributes:**
|
|
972
|
+
|
|
973
|
+
The handler automatically replaces `#{{name}}` references in these attributes:
|
|
974
|
+
- ARIA: `aria-labelledby`, `aria-describedby`, `aria-controls`, `aria-owns`, `aria-flowto`, `aria-activedescendant`
|
|
975
|
+
- Form: `for`, `form`, `list`
|
|
976
|
+
- Microdata: `itemref`
|
|
977
|
+
- Any `data-*` attribute
|
|
978
|
+
- Any attribute with a `defer-*` prefix
|
|
979
|
+
|
|
980
|
+
**Declarative usage with MOSE:**
|
|
981
|
+
|
|
982
|
+
```html
|
|
983
|
+
<script type="mountobserver">
|
|
984
|
+
{
|
|
985
|
+
"do": "builtIns.generateIds"
|
|
986
|
+
}
|
|
987
|
+
</script>
|
|
988
|
+
|
|
989
|
+
<script type="module">
|
|
990
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
991
|
+
|
|
992
|
+
new MountObserver({
|
|
993
|
+
do: 'builtIns.mountObserverScript'
|
|
994
|
+
}).observe(document);
|
|
995
|
+
</script>
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**Scope containers:**
|
|
999
|
+
|
|
1000
|
+
The handler looks for the nearest scope container using `.closest()`:
|
|
1001
|
+
- `<fieldset>` elements
|
|
1002
|
+
- Elements with `[itemscope]` attribute
|
|
1003
|
+
- Falls back to the root node if no scope is found
|
|
1004
|
+
|
|
1005
|
+
**Global counter:**
|
|
1006
|
+
|
|
1007
|
+
IDs are generated using a global counter (via `Symbol.for`) to ensure uniqueness across multiple module instances. Generated IDs follow the pattern `gid-0`, `gid-1`, `gid-2`, etc.
|
|
217
1008
|
|
|
218
1009
|
|
|
219
1010
|
# Thorough Exposition Begins Here
|
|
@@ -286,187 +1077,478 @@ This polyfill in fact only supports this latter option ("matching"), and leaves
|
|
|
286
1077
|
|
|
287
1078
|
[Implemented as Requirement 1](requirements/Done/Requirement1.md).
|
|
288
1079
|
|
|
1080
|
+
## The observe() method
|
|
1081
|
+
|
|
1082
|
+
The `observe()` method begins observation of elements within the provided node:
|
|
1083
|
+
|
|
1084
|
+
```typescript
|
|
1085
|
+
async observe(observedNode: Node): Promise<void>
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Parameter: `observedNode`**
|
|
1089
|
+
|
|
1090
|
+
The `observedNode` parameter is the node where observation takes place. In order to support the polyfill, a mutation observer is registered on this node to detect when matching elements are added or removed. All matching elements within this node and its descendants will trigger mount callbacks, as long as it belongs to the same scoped custom element registry as the observed node.
|
|
1091
|
+
|
|
1092
|
+
**Common usage:**
|
|
1093
|
+
```javascript
|
|
1094
|
+
const observer = new MountObserver({
|
|
1095
|
+
matching: '.my-element',
|
|
1096
|
+
do: (el) => console.log('Mounted:', el)
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// Observe the entire document
|
|
1100
|
+
await observer.observe(document);
|
|
1101
|
+
|
|
1102
|
+
// Or observe a specific subtree
|
|
1103
|
+
const container = document.querySelector('#container');
|
|
1104
|
+
await observer.observe(container);
|
|
1105
|
+
|
|
1106
|
+
// Or observe within a shadow DOM
|
|
1107
|
+
const shadowRoot = element.shadowRoot;
|
|
1108
|
+
await observer.observe(shadowRoot);
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
**Note:** An observer can only observe one node at a time. Calling `observe()` again will throw an error. Call `disconnect()` first to observe a different node.
|
|
1112
|
+
|
|
1113
|
+
**Relationship with element.mount():**
|
|
1114
|
+
|
|
1115
|
+
When using the `element.mount()` convenience method, it internally determines which node to pass to `observe()` based on the `scope` option:
|
|
1116
|
+
- `'self'` - Observes the element itself
|
|
1117
|
+
- `'registryRoot'` - Finds and observes the element's registry root
|
|
1118
|
+
- `'registry'` - Finds and observes all DOM nodes that have the same custom element registry
|
|
1119
|
+
- `'shadow'` - Observes the element's shadow root
|
|
1120
|
+
- `'root'` - Observes the element's root node (via `getRootNode()`)
|
|
1121
|
+
|
|
1122
|
+
## The import key
|
|
1123
|
+
|
|
1124
|
+
This proposal has been amended to support multiple imports, including of different types:
|
|
1125
|
+
|
|
1126
|
+
```JavaScript
|
|
1127
|
+
const observer = new MountObserver({
|
|
1128
|
+
matching:'my-element',
|
|
1129
|
+
import: [
|
|
1130
|
+
['./my-element-small.css', {type: 'css'}],
|
|
1131
|
+
'./my-element.js',
|
|
1132
|
+
],
|
|
1133
|
+
do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
|
|
1134
|
+
...
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
observer.observe(document);
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
Once again, the key can accept either a single import, but alternatively it can also support multiple imports (via an array).
|
|
1141
|
+
|
|
1142
|
+
The do function won't be invoked until all the imports have been successfully completed and inserted into the modules array.
|
|
1143
|
+
|
|
1144
|
+
Previously, this proposal called for allowing arrow functions as well, thinking that could be a good interim way to support bundlers, as well as multiple imports. But the valuable input provided by [doeixd](https://github.com/doeixd) makes me think that that interim support could more effectively be done by the developer in the do methods.
|
|
1145
|
+
|
|
1146
|
+
This proposal would also include support for JSON and HTML module imports (really, all types).
|
|
1147
|
+
|
|
1148
|
+
[Implemented as Requirement 1](requirements/Done/Requirement1.md).
|
|
1149
|
+
|
|
1150
|
+
## Preemptive downloading
|
|
1151
|
+
|
|
1152
|
+
There are two significant steps to imports, each of which imposes a cost:
|
|
1153
|
+
|
|
1154
|
+
1. Downloading the resource.
|
|
1155
|
+
2. Loading the resource into memory.
|
|
1156
|
+
|
|
1157
|
+
What if we want to *download* the resource ahead of time, but only load into memory when needed?
|
|
1158
|
+
|
|
1159
|
+
The link rel=modulepreload option (and maybe the new defer tc39 proposal) provides an already existing platform support for this, but the browser complains when no use of the resource is used within a short time span of page load. That doesn't really fit the bill for lazy loading custom elements and other resources.
|
|
1160
|
+
|
|
1161
|
+
So for this we add loadingEagerness:
|
|
1162
|
+
|
|
1163
|
+
```JavaScript
|
|
1164
|
+
const observer = new MountObserver({
|
|
1165
|
+
select: 'my-element', //not supported by this polyfill
|
|
1166
|
+
loadingEagerness: 'eager', //partially supported by this polyfill
|
|
1167
|
+
import: './my-element.js',
|
|
1168
|
+
do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
|
|
1169
|
+
});
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
So what this does is only check for the presence of an element with tag name "my-element", and it starts downloading the resource, even before the element has "mounted" based on other criteria.
|
|
1173
|
+
|
|
1174
|
+
The polyfill just loads the module into memory right away.
|
|
1175
|
+
|
|
1176
|
+
> [!NOTE]
|
|
1177
|
+
> As a result of the google IO 2024 talks, I became aware that there is some similarity between this proposal and the [speculation rules api](https://developer.chrome.com/blog/speculation-rules-improvements). This motivated the change to the property from "loading" to loadingEagerness above.
|
|
1178
|
+
|
|
1179
|
+
## Importing Configuration with configFrom
|
|
1180
|
+
|
|
1181
|
+
The `configFrom` property provides a clean way to import MountConfig settings from external modules, enabling better code organization and reusability.
|
|
1182
|
+
|
|
1183
|
+
**Key benefit for JSON serialization**: One of the most important advantages of `configFrom` is that it allows us to separate non-JSON-serializable settings (like functions and class constructors) from JSON-serializable settings. This makes it possible to keep our inline MountConfig 100% JSON-serializable while still leveraging the full power of JavaScript in our imported configuration modules when needed.
|
|
1184
|
+
|
|
1185
|
+
```JavaScript
|
|
1186
|
+
// Inline config - 100% JSON serializable
|
|
1187
|
+
const observer = new MountObserver({
|
|
1188
|
+
matching: '.my-element',
|
|
1189
|
+
configFrom: './my-handlers.js' // Non-serializable code lives here
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// my-handlers.js - Contains functions and class references
|
|
1193
|
+
export const mountConfig = {
|
|
1194
|
+
whereInstanceOf: HTMLButtonElement, // Class constructor
|
|
1195
|
+
do: (element, context) => { // Function
|
|
1196
|
+
element.addEventListener('click', () => console.log('clicked'));
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
This separation is crucial for scenarios like Mount Observer Script Elements (MOSEs) where configuration needs to be embedded in HTML as JSON, but we still want to leverage imperative JavaScript code.
|
|
1202
|
+
|
|
1203
|
+
### Basic Usage
|
|
1204
|
+
|
|
1205
|
+
Create a configuration module that exports a `mountConfig` constant:
|
|
1206
|
+
|
|
1207
|
+
```JavaScript
|
|
1208
|
+
// my-config.js
|
|
1209
|
+
export const mountConfig = {
|
|
1210
|
+
matching: '.my-element',
|
|
1211
|
+
do: (element, context) => {
|
|
1212
|
+
element.textContent = 'Configured!';
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
Then reference it in your observer:
|
|
1218
|
+
|
|
1219
|
+
```JavaScript
|
|
1220
|
+
const observer = new MountObserver({
|
|
1221
|
+
configFrom: './my-config.js'
|
|
1222
|
+
});
|
|
1223
|
+
observer.observe(document);
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Multiple Configuration Modules
|
|
1227
|
+
|
|
1228
|
+
You can import multiple config modules. Later configs override earlier ones (left-to-right merge):
|
|
1229
|
+
|
|
1230
|
+
```JavaScript
|
|
1231
|
+
const observer = new MountObserver({
|
|
1232
|
+
configFrom: ['./base-config.js', './override-config.js']
|
|
1233
|
+
});
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
### Inline Config Takes Precedence
|
|
1237
|
+
|
|
1238
|
+
Inline configuration always overrides imported configuration:
|
|
1239
|
+
|
|
1240
|
+
```JavaScript
|
|
1241
|
+
const observer = new MountObserver({
|
|
1242
|
+
configFrom: './base-config.js',
|
|
1243
|
+
matching: '.custom-selector' // Overrides matching from base-config.js
|
|
1244
|
+
});
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
### Merge Semantics
|
|
1248
|
+
|
|
1249
|
+
- **Shallow merge**: Uses `Object.assign()` for merging
|
|
1250
|
+
- **Merge order**: First configFrom module → second configFrom module → ... → inline config
|
|
1251
|
+
- **Arrays are replaced**: If multiple configs define the same array property, the later array completely replaces the earlier one
|
|
1252
|
+
- **Inline wins**: Inline configuration always takes final precedence
|
|
1253
|
+
|
|
1254
|
+
### Supported Properties
|
|
1255
|
+
|
|
1256
|
+
Config modules can export any valid MountConfig property, including:
|
|
1257
|
+
- `matching`, `whereInstanceOf`, `withMediaMatching`
|
|
1258
|
+
- `whereObservedRootSizeMatches`, `whereElementIntersectsWith`
|
|
1259
|
+
- `whereConnectionHas`, `withScopePerimeter`
|
|
1260
|
+
- `import`, `do`, `loadingEagerness`
|
|
1261
|
+
- `assignOnMount`, `assignOnDismount`, `stageOnMount`
|
|
1262
|
+
- `mountedElemEmits`, `customData`, `getPlayByPlay`
|
|
1263
|
+
|
|
1264
|
+
### Functions and Class References
|
|
1265
|
+
|
|
1266
|
+
Config modules can include non-JSON-serializable values like functions and class constructors:
|
|
1267
|
+
|
|
1268
|
+
```JavaScript
|
|
1269
|
+
// button-config.js
|
|
1270
|
+
export const mountConfig = {
|
|
1271
|
+
matching: 'button',
|
|
1272
|
+
whereInstanceOf: HTMLButtonElement,
|
|
1273
|
+
do: (element, context) => {
|
|
1274
|
+
element.addEventListener('click', () => {
|
|
1275
|
+
console.log('Button clicked!');
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
### Error Handling
|
|
1282
|
+
|
|
1283
|
+
**Missing mountConfig export:**
|
|
1284
|
+
```JavaScript
|
|
1285
|
+
// This will throw an error
|
|
1286
|
+
const observer = new MountObserver({
|
|
1287
|
+
configFrom: './module-without-mountConfig.js'
|
|
1288
|
+
});
|
|
1289
|
+
// Error: Module './module-without-mountConfig.js' does not export 'mountConfig'
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
**Duplicate modules:**
|
|
1293
|
+
```JavaScript
|
|
1294
|
+
// This will throw an error
|
|
1295
|
+
const observer = new MountObserver({
|
|
1296
|
+
configFrom: ['./config.js', './config.js']
|
|
1297
|
+
});
|
|
1298
|
+
// Error: Duplicate configFrom module: './config.js'
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
### Circular Dependency Warning
|
|
1302
|
+
|
|
1303
|
+
Be careful to avoid circular dependencies when using `configFrom`. Config modules should only export configuration and avoid importing modules that create MountObserver instances.
|
|
1304
|
+
|
|
1305
|
+
**Safe pattern:**
|
|
1306
|
+
```JavaScript
|
|
1307
|
+
// config.js - Only exports configuration
|
|
1308
|
+
export const mountConfig = {
|
|
1309
|
+
matching: '.element',
|
|
1310
|
+
do: (el) => { /* ... */ }
|
|
1311
|
+
};
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
**Avoid:**
|
|
1315
|
+
```JavaScript
|
|
1316
|
+
// config.js - Creates circular dependency
|
|
1317
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
1318
|
+
// This could cause issues if the importing module also imports MountObserver
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
## Media / container queries / instanceOf
|
|
1322
|
+
|
|
1323
|
+
Unlike traditional CSS @import, CSS Modules don't support specifying different imports based on media queries. That can be another condition we can attach (and why not throw in container queries, based on the rootNode?):
|
|
1324
|
+
|
|
1325
|
+
```JavaScript
|
|
1326
|
+
const observer = new MountObserver({
|
|
1327
|
+
// not supported by polyfill
|
|
1328
|
+
select: 'div > p + p ~ span[class$="name"]',
|
|
1329
|
+
withMediaMatching: '(max-width: 1250px)',
|
|
1330
|
+
whereObservedRootSizeMatches: '(min-width: 700px)',
|
|
1331
|
+
whereElementIntersectsWith:{
|
|
1332
|
+
rootMargin: "0px",
|
|
1333
|
+
threshold: 1.0,
|
|
1334
|
+
},
|
|
1335
|
+
whereInstanceOf: [HTMLMarqueeElement], //or 'HTMLMarqueeElement'
|
|
1336
|
+
whereLangIn: ['en-GB'], // Cannot be implemented - see https://github.com/whatwg/html/issues/7039
|
|
1337
|
+
whereConnectionHas:{
|
|
1338
|
+
effectiveTypeIn: ["slow-2g"],
|
|
1339
|
+
},
|
|
1340
|
+
import: ['./my-element-small.css', {type: 'css'}],
|
|
1341
|
+
do: function(mountedElement, ctx){
|
|
1342
|
+
console.log({mountedElement, ctx});
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
[whereInstanceOf implemented as [Requirement5](requirements/Done/Requirement5.md)]
|
|
1348
|
+
[whereObservedRootSizeMatches implemented]
|
|
1349
|
+
[whereElementIntersectsWith implemented]
|
|
1350
|
+
[whereConnectionHas implemented]
|
|
1351
|
+
[whereLocalNameMatches implemented as [RegularExpressionNameMatching](requirements/Done/RegularExpressionNameMatching.md)]
|
|
1352
|
+
|
|
1353
|
+
[withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
|
|
289
1354
|
|
|
290
|
-
##
|
|
1355
|
+
## LocalName Pattern Matching
|
|
291
1356
|
|
|
292
|
-
This
|
|
1357
|
+
The `whereLocalNameMatches` property allows filtering elements by their `localName` using regular expressions. This is useful when you need to match elements based on naming patterns rather than CSS selectors.
|
|
293
1358
|
|
|
294
|
-
```
|
|
1359
|
+
```javascript
|
|
295
1360
|
const observer = new MountObserver({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
|
|
302
|
-
...
|
|
303
|
-
}
|
|
1361
|
+
matching: '*', // Match all elements
|
|
1362
|
+
whereLocalNameMatches: /^my-/, // Only mount elements starting with 'my-'
|
|
1363
|
+
do: (element) => {
|
|
1364
|
+
console.log('Mounted:', element.localName);
|
|
1365
|
+
}
|
|
304
1366
|
});
|
|
305
1367
|
observer.observe(document);
|
|
306
1368
|
```
|
|
307
1369
|
|
|
308
|
-
|
|
1370
|
+
**String patterns are automatically converted to RegExp:**
|
|
309
1371
|
|
|
310
|
-
|
|
1372
|
+
```javascript
|
|
1373
|
+
// These are equivalent
|
|
1374
|
+
whereLocalNameMatches: 'button|input'
|
|
1375
|
+
whereLocalNameMatches: /button|input/
|
|
1376
|
+
```
|
|
311
1377
|
|
|
312
|
-
|
|
1378
|
+
**Common use cases:**
|
|
313
1379
|
|
|
314
|
-
|
|
1380
|
+
```javascript
|
|
1381
|
+
// Match custom elements with a specific prefix
|
|
1382
|
+
whereLocalNameMatches: /^app-/
|
|
315
1383
|
|
|
316
|
-
|
|
1384
|
+
// Match elements ending with a suffix
|
|
1385
|
+
whereLocalNameMatches: /-widget$/
|
|
317
1386
|
|
|
318
|
-
|
|
1387
|
+
// Match multiple element types
|
|
1388
|
+
whereLocalNameMatches: /^(button|input|select)$/
|
|
319
1389
|
|
|
320
|
-
|
|
1390
|
+
// Match elements containing a pattern
|
|
1391
|
+
whereLocalNameMatches: /dialog/
|
|
1392
|
+
```
|
|
321
1393
|
|
|
322
|
-
|
|
323
|
-
2. Loading the resource into memory.
|
|
1394
|
+
**AND condition logic:**
|
|
324
1395
|
|
|
325
|
-
|
|
1396
|
+
Like all `where*` properties, `whereLocalNameMatches` forms an AND condition with other filters:
|
|
326
1397
|
|
|
327
|
-
|
|
1398
|
+
```javascript
|
|
1399
|
+
const observer = new MountObserver({
|
|
1400
|
+
matching: '[data-enhanced]', // Must have data-enhanced attribute
|
|
1401
|
+
whereLocalNameMatches: /^custom-/, // AND localName starts with 'custom-'
|
|
1402
|
+
whereInstanceOf: HTMLElement, // AND is an HTMLElement instance
|
|
1403
|
+
do: (element) => { /* ... */ }
|
|
1404
|
+
});
|
|
1405
|
+
```
|
|
328
1406
|
|
|
329
|
-
|
|
1407
|
+
This will only mount elements that satisfy ALL three conditions.
|
|
330
1408
|
|
|
331
|
-
|
|
1409
|
+
## Custom JavaScript Checks with shouldMount
|
|
1410
|
+
|
|
1411
|
+
The `shouldMount` property provides a final JavaScript-based check that runs after all declarative `where*` conditions have passed. This is useful for complex logic that can't be expressed declaratively.
|
|
1412
|
+
|
|
1413
|
+
It's useful to be able to provide this check outside of the do method for separation of concerns reasons, and also because the do function only gets called once, and having this extra check allows us to combine all the checks together in a consistent way.
|
|
1414
|
+
|
|
1415
|
+
```javascript
|
|
332
1416
|
const observer = new MountObserver({
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1417
|
+
matching: '.protected-feature',
|
|
1418
|
+
shouldMount: (el, ctx) => {
|
|
1419
|
+
// Check user permission
|
|
1420
|
+
const requiredRole = el.dataset.requiredRole;
|
|
1421
|
+
return currentUser.hasRole(requiredRole);
|
|
1422
|
+
},
|
|
1423
|
+
do: (el) => {
|
|
1424
|
+
// Only called if shouldMount returned true
|
|
1425
|
+
enhanceProtectedFeature(el);
|
|
1426
|
+
}
|
|
337
1427
|
});
|
|
338
1428
|
```
|
|
339
1429
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1430
|
+
**Behavior:**
|
|
1431
|
+
- `shouldMount` is called after ALL `where*` conditions pass
|
|
1432
|
+
- If it returns `true`, the element mounts (do callback + mount event)
|
|
1433
|
+
- If it returns `false`, the element does NOT mount (no do callback, no mount event)
|
|
1434
|
+
- If it throws an error, it's treated as `false` and the error is logged
|
|
1435
|
+
- The element can be re-evaluated if removed and re-added to the DOM
|
|
343
1436
|
|
|
344
|
-
|
|
345
|
-
> As a result of the google IO 2024 talks, I became aware that there is some similarity between this proposal and the [speculation rules api](https://developer.chrome.com/blog/speculation-rules-improvements). This motivated the change to the property from "loading" to loadingEagerness above.
|
|
1437
|
+
**Use Cases:**
|
|
346
1438
|
|
|
347
|
-
|
|
1439
|
+
Authorization checks:
|
|
1440
|
+
```javascript
|
|
1441
|
+
shouldMount: (el) => currentUser.hasPermission(el.dataset.permission)
|
|
1442
|
+
```
|
|
348
1443
|
|
|
1444
|
+
Feature flags:
|
|
1445
|
+
```javascript
|
|
1446
|
+
shouldMount: (el) => featureFlags.isEnabled(el.dataset.feature)
|
|
1447
|
+
```
|
|
349
1448
|
|
|
350
|
-
|
|
1449
|
+
Data validation:
|
|
1450
|
+
```javascript
|
|
1451
|
+
shouldMount: (el) => {
|
|
1452
|
+
return el.dataset.apiKey &&
|
|
1453
|
+
el.dataset.apiEndpoint &&
|
|
1454
|
+
el.dataset.apiKey.length > 0;
|
|
1455
|
+
}
|
|
1456
|
+
```
|
|
351
1457
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
observer.disconnectedSignal.abort();
|
|
1458
|
+
Complex conditional logic:
|
|
1459
|
+
```javascript
|
|
1460
|
+
shouldMount: (el, ctx) => {
|
|
1461
|
+
const parent = el.closest('[data-context]');
|
|
1462
|
+
if (!parent) return false;
|
|
1463
|
+
|
|
1464
|
+
const isActive = parent.dataset.context === 'active';
|
|
1465
|
+
const widgetType = el.dataset.widgetType;
|
|
1466
|
+
const enabledWidgets = parent.dataset.enabledWidgets?.split(',') || [];
|
|
1467
|
+
|
|
1468
|
+
return isActive && enabledWidgets.includes(widgetType);
|
|
365
1469
|
}
|
|
366
|
-
|
|
1470
|
+
```
|
|
367
1471
|
|
|
368
|
-
|
|
1472
|
+
**Note:** For event-driven mounting (waiting for user clicks, etc.), use the `do` callback with event listeners rather than `shouldMount`. The `shouldMount` callback is for checking conditions, not waiting for events.
|
|
369
1473
|
|
|
370
|
-
|
|
371
|
-
matching:'my-element',
|
|
372
|
-
import: [
|
|
373
|
-
'./my-element.js',
|
|
374
|
-
['./my-element-small.css', {type: 'css'}],
|
|
375
|
-
'./myActions.js'
|
|
376
|
-
],
|
|
377
|
-
reference: 2
|
|
378
|
-
});
|
|
379
|
-
observer.observe(document);
|
|
1474
|
+
[Implemented as SupportForShouldMount requirement](requirements/Done/SupportForShouldMount.md)
|
|
380
1475
|
|
|
381
|
-
|
|
1476
|
+
## InstanceOf checks in detail
|
|
382
1477
|
|
|
383
|
-
|
|
1478
|
+
Carving out the special "whereInstanceOf" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "shouldMount" callback logic (discussed later). For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
|
|
384
1479
|
|
|
385
|
-
|
|
1480
|
+
The picture becomes murkier for custom elements. The best solution in that case seems to be to utilize customElements.getName(...) as a basis for the match, but at first glance, that could preclude being able to use base classes which a family of custom elements subclass, if that superclass isn't itself a custom element. I suppose the solution to this conundrum, when warranted, is simply to burden the developer with defining a custom element for the superclass, and thus assigning it a name, applicable within ShadowDOM scopes as needed, even though it isn't actually necessarily used for any live custom elements. This would require already having imported the base class, only benefitting from lazy loading the code needed for each sub class, which might not always be all that high as a percentage, compared to the base class.
|
|
386
1481
|
|
|
387
|
-
|
|
1482
|
+
However, where this support for "whereInstanceOf" would be *most* helpful is when it comes to [*custom enhancements*](https://github.com/WICG/webcomponents/issues/1000) that only wish to lazily layer some heavy lifting functionality on top of certain families of already loaded and upgraded custom elements (possibly in addition to some (specified) built in elements). Here, the lazy loading of the *entire custom **enhancement***, based on the presence in the DOM of a member of the family of custom elements, would, if my calculations are correct, result in providing a significant benefit.
|
|
1483
|
+
|
|
388
1484
|
|
|
389
|
-
|
|
390
|
-
- The `reference` property can be a single number or an array of numbers, each referring to an import index
|
|
391
|
-
- Referenced modules must be JavaScript modules (not CSS, JSON, or HTML imports)
|
|
392
|
-
- If a referenced module exports a `do` function, it will be called after the inline `do` callback (if present)
|
|
393
|
-
- If a referenced module doesn't export a `do` function, it's silently skipped
|
|
394
|
-
- The inline `do` callback runs first, then referenced `do` functions run in the order specified
|
|
1485
|
+
<!--
|
|
395
1486
|
|
|
396
|
-
|
|
397
|
-
```javascript
|
|
398
|
-
const doFunction = function(element, context) { /* ... */ };
|
|
399
|
-
export { doFunction as do };
|
|
400
|
-
```
|
|
1487
|
+
[TODO] Maybe should also (optionally?) pass back which checks failed and which succeeded on dismount. Not sure I really see a use case for it, but leaving the thought here for now
|
|
401
1488
|
|
|
402
|
-
|
|
403
|
-
- Throws an error if `import` is not defined
|
|
404
|
-
- Throws an error if any index is out of bounds
|
|
405
|
-
- Throws an error if any index points to a non-JS module (e.g., CSS or JSON import)
|
|
1489
|
+
-->
|
|
406
1490
|
|
|
407
|
-
|
|
1491
|
+
## Custom Element Registry Matching
|
|
408
1492
|
|
|
409
|
-
|
|
1493
|
+
MountObserver automatically respects scoped custom element registry boundaries. When observing a root node, only elements that share the same `customElementRegistry` as the root node will be mounted by default. This is an implicit AND condition that applies to all observations.
|
|
410
1494
|
|
|
411
|
-
|
|
1495
|
+
**How it works:**
|
|
412
1496
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
'
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
```
|
|
1497
|
+
```javascript
|
|
1498
|
+
// Observe document - only mounts elements in the global registry
|
|
1499
|
+
const observer1 = new MountObserver({
|
|
1500
|
+
matching: '.my-element',
|
|
1501
|
+
do: (el) => { /* ... */ }
|
|
1502
|
+
});
|
|
1503
|
+
observer1.observe(document);
|
|
421
1504
|
|
|
422
|
-
|
|
1505
|
+
// Observe shadow root - only mounts elements in that shadow root's registry
|
|
1506
|
+
const shadowRoot = host.attachShadow({ mode: 'open' });
|
|
1507
|
+
const observer2 = new MountObserver({
|
|
1508
|
+
matching: '.my-element',
|
|
1509
|
+
do: (el) => { /* ... */ }
|
|
1510
|
+
});
|
|
1511
|
+
observer2.observe(shadowRoot);
|
|
1512
|
+
```
|
|
423
1513
|
|
|
424
|
-
|
|
1514
|
+
**Registry matching logic:**
|
|
425
1515
|
|
|
426
|
-
|
|
1516
|
+
The implementation is straightforward - it compares the `customElementRegistry` property of the root node with the `customElementRegistry` property of each candidate element:
|
|
427
1517
|
|
|
428
1518
|
```javascript
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if(!customElements.get(localName)) {
|
|
432
|
-
customElements.define(localName, modules[1].MyElement);
|
|
433
|
-
}
|
|
434
|
-
observer.disconnectedSignal.abort();
|
|
435
|
-
};
|
|
1519
|
+
const registriesMatch = rootNode.customElementRegistry === element.customElementRegistry;
|
|
1520
|
+
```
|
|
436
1521
|
|
|
437
|
-
|
|
1522
|
+
**Default behavior** (same registry):
|
|
1523
|
+
- Elements with matching registries → mount ✓
|
|
1524
|
+
- Elements with different registries → don't mount ✓
|
|
438
1525
|
|
|
439
|
-
|
|
1526
|
+
**Inverted behavior** with `whereDifferentCustomElementRegistry: true`:
|
|
1527
|
+
- Elements with matching registries → don't mount ✓
|
|
1528
|
+
- Elements with different registries → mount ✓
|
|
440
1529
|
|
|
441
|
-
|
|
1530
|
+
**Use case for inverted matching:**
|
|
1531
|
+
```javascript
|
|
1532
|
+
// Mount elements from OTHER registries (cross-registry observation)
|
|
442
1533
|
const observer = new MountObserver({
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
'./my-element.js',
|
|
447
|
-
'./mySettings.js'
|
|
448
|
-
],
|
|
449
|
-
reference: 2
|
|
1534
|
+
matching: '.external-component',
|
|
1535
|
+
whereDifferentCustomElementRegistry: true,
|
|
1536
|
+
do: (el) => { /* Handle elements from different registries */ }
|
|
450
1537
|
});
|
|
451
|
-
observer.observe(
|
|
1538
|
+
observer.observe(shadowRoot);
|
|
452
1539
|
```
|
|
453
1540
|
|
|
454
|
-
**Behavior:**
|
|
455
|
-
- **
|
|
456
|
-
- **
|
|
457
|
-
- **Validation**: Referenced `withInstance` is validated after imports load. Throws an error if not a Constructor or array of Constructors
|
|
458
|
-
- **Optional export**: If a referenced module doesn't export `withInstance`, it's silently ignored
|
|
459
|
-
- **Timing**:
|
|
460
|
-
- With lazy loading (default): Inline `withInstance` is checked first (before imports), then referenced checks happen after imports load
|
|
461
|
-
- With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
|
|
1541
|
+
**Behavior across browser versions:**
|
|
1542
|
+
- **Pre-Chrome 146**: Both `customElementRegistry` properties are `undefined`, so `undefined === undefined` is `true` and elements match (backward compatible)
|
|
1543
|
+
- **Chrome 146+ with scoped registries**: Elements are filtered by registry reference equality
|
|
462
1544
|
|
|
463
|
-
This
|
|
1545
|
+
This ensures that when we observe a shadow root with a scoped registry, we won't accidentally mount elements from the parent document or other shadow roots with different registries (unless explicitly requested with `whereDifferentCustomElementRegistry: true`). The registry check happens automatically before any other `where*` conditions are evaluated.
|
|
464
1546
|
|
|
465
|
-
[Implemented as [
|
|
1547
|
+
[Implemented as [ExcludeMatchingElementsWhereCustomElementRegistriesDon'tMatch](requirements/ExcludeMatchingElementsWhereCustomElementRegistriesDon'tMatch.md)]
|
|
466
1548
|
|
|
467
1549
|
## Element Mount Extension
|
|
468
1550
|
|
|
469
|
-
For even more convenience,
|
|
1551
|
+
For even more convenience, we can use the `element.mount()` method to observe elements within their scoped custom element registry context. This is particularly useful with scoped custom element registries (Chrome 146+, latest WebKit/Safari).
|
|
470
1552
|
|
|
471
1553
|
```JavaScript
|
|
472
1554
|
import 'mount-observer/ElementMountExtension.js';
|
|
@@ -491,7 +1573,7 @@ Scope options (via `options.scope`):
|
|
|
491
1573
|
- `'self'`: Observes only this element
|
|
492
1574
|
- `'root'`: Observes the root node (document or shadow root)
|
|
493
1575
|
- `'shadow'`: Observes the element's shadowRoot (throws error if none exists)
|
|
494
|
-
- `Element`: Observes a custom element
|
|
1576
|
+
- `Element`: Observes a custom element we specify
|
|
495
1577
|
|
|
496
1578
|
This is especially useful for web components that want to observe their own shadow DOM or scoped registry:
|
|
497
1579
|
|
|
@@ -526,14 +1608,183 @@ class MyComponent extends HTMLElement {
|
|
|
526
1608
|
Browser support: Works in all browsers, but scoped registry features require Chrome 146+ or latest WebKit/Safari.
|
|
527
1609
|
|
|
528
1610
|
[Implemented as CustomElementRegistryMounting requirement](requirements/Done/CustomElementRegistryMounting.md).
|
|
529
|
-
|
|
530
1611
|
|
|
1612
|
+
### Global Propagation with `mountGlobally()`
|
|
1613
|
+
|
|
1614
|
+
The `mountGlobally()` method extends `mount()` to automatically propagate mount observers across custom element registry boundaries and shadow DOM scopes. This is useful for bootstrapping core handlers that should work everywhere, regardless of scoped registries. It should be used sparingly, as a last resort, probably limited to things that should arguably be built into the platform.
|
|
1615
|
+
|
|
1616
|
+
```JavaScript
|
|
1617
|
+
import 'mount-observer/ElementMountExtension.js';
|
|
1618
|
+
|
|
1619
|
+
// Mount globally - propagates to all child registries and shadow roots
|
|
1620
|
+
await document.mountGlobally({
|
|
1621
|
+
matching: '.target',
|
|
1622
|
+
do: (el) => {
|
|
1623
|
+
el.setAttribute('data-mounted', 'true');
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
The `mountGlobally()` method:
|
|
1629
|
+
- Mounts the config in the current registry first (using `mount()`)
|
|
1630
|
+
- Creates two propagators that automatically mount in:
|
|
1631
|
+
- Elements with different custom element registries (`whereDifferentCustomElementRegistry: true`)
|
|
1632
|
+
- Shadow roots within the same registry (custom elements with shadow DOM)
|
|
1633
|
+
- Waits for custom elements to be defined before mounting (ensures shadow roots exist)
|
|
1634
|
+
- Recursively propagates through nested shadow roots
|
|
1635
|
+
|
|
1636
|
+
This enables "viral" propagation of mount observers, perfect for bootstrapping core handlers like `builtIns.mountObserverScript`:
|
|
1637
|
+
|
|
1638
|
+
```JavaScript
|
|
1639
|
+
// Bootstrap mount observer script support globally
|
|
1640
|
+
await document.mountGlobally({
|
|
1641
|
+
do: 'builtIns.mountObserverScript'
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// Now MOSE scripts work everywhere, even in scoped registries
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
Both `Element.prototype.mountGlobally()` and `ShadowRoot.prototype.mountGlobally()` are available.
|
|
1648
|
+
|
|
1649
|
+
[Implemented as goViral requirement](requirements/Done/goViral.md).
|
|
1650
|
+
|
|
1651
|
+
## Hierarchical Observer Composition with the `with` Property
|
|
1652
|
+
|
|
1653
|
+
The `with` property enables hierarchical composition of MountObservers, allowing a parent observer to declaratively create and manage multiple sub-observers that observe the same root node. This provides a clean way to organize complex observation scenarios and coordinate multiple observers.
|
|
1654
|
+
|
|
1655
|
+
### Basic Usage
|
|
1656
|
+
|
|
1657
|
+
```JavaScript
|
|
1658
|
+
const observer = new MountObserver({
|
|
1659
|
+
matching: '.container',
|
|
1660
|
+
with: {
|
|
1661
|
+
// Sub-observer for custom elements
|
|
1662
|
+
registry: {
|
|
1663
|
+
matching: 'my-element',
|
|
1664
|
+
import: './my-element.js',
|
|
1665
|
+
do: 'builtIns.defineCustomElement'
|
|
1666
|
+
},
|
|
1667
|
+
// Sub-observer for styles
|
|
1668
|
+
styles: {
|
|
1669
|
+
matching: '.styled',
|
|
1670
|
+
import: './styles.css'
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
await observer.observe(document);
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
### How It Works
|
|
1679
|
+
|
|
1680
|
+
1. **Automatic Creation**: When the parent observer's `observe()` method is called, it automatically creates sub-observers for each entry in the `with` property.
|
|
1681
|
+
|
|
1682
|
+
2. **Same Root Node**: All sub-observers observe the same root node as the parent.
|
|
1683
|
+
|
|
1684
|
+
3. **Independent Configuration**: Each sub-observer operates independently with its own configuration. Sub-observers do NOT inherit properties from the parent.
|
|
1685
|
+
|
|
1686
|
+
4. **Automatic Lifecycle**: Sub-observers are automatically disconnected when the parent disconnects.
|
|
1687
|
+
|
|
1688
|
+
5. **Unlimited Nesting**: Sub-observers can have their own `with` property for unlimited nesting depth.
|
|
1689
|
+
|
|
1690
|
+
### Accessing Sub-Observers in Handlers
|
|
1691
|
+
|
|
1692
|
+
Sub-observers are accessible in mount handlers via the `context.withObservers` property:
|
|
1693
|
+
|
|
1694
|
+
```JavaScript
|
|
1695
|
+
const observer = new MountObserver({
|
|
1696
|
+
matching: '.parent',
|
|
1697
|
+
with: {
|
|
1698
|
+
registry: { matching: 'custom-element' },
|
|
1699
|
+
styles: { matching: '.styled' }
|
|
1700
|
+
},
|
|
1701
|
+
do: (el, ctx) => {
|
|
1702
|
+
// Access sub-observers with type safety
|
|
1703
|
+
const registryObserver = ctx.withObservers?.registry;
|
|
1704
|
+
const stylesObserver = ctx.withObservers?.styles;
|
|
1705
|
+
|
|
1706
|
+
if (registryObserver) {
|
|
1707
|
+
console.log('Registry observer:', registryObserver);
|
|
1708
|
+
console.log('Mounted elements:', registryObserver.mountedElements);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
### Nested Sub-Observers
|
|
1715
|
+
|
|
1716
|
+
Sub-observers can have their own sub-observers, creating a tree structure:
|
|
1717
|
+
|
|
1718
|
+
```JavaScript
|
|
1719
|
+
const observer = new MountObserver({
|
|
1720
|
+
matching: '.root',
|
|
1721
|
+
with: {
|
|
1722
|
+
level1: {
|
|
1723
|
+
matching: '.level1',
|
|
1724
|
+
with: {
|
|
1725
|
+
level2: {
|
|
1726
|
+
matching: '.level2',
|
|
1727
|
+
do: (el) => console.log('Level 2 mounted:', el)
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
### Use Case: Cross-Scope Registry Management
|
|
1736
|
+
|
|
1737
|
+
A practical use case is managing custom elements across different scoped registries:
|
|
1738
|
+
|
|
1739
|
+
```JavaScript
|
|
1740
|
+
const observer = new MountObserver({
|
|
1741
|
+
matching: 'div[shadowroot]',
|
|
1742
|
+
with: {
|
|
1743
|
+
// Observe elements in the main registry
|
|
1744
|
+
mainRegistry: {
|
|
1745
|
+
matching: 'my-element',
|
|
1746
|
+
whereDifferentCustomElementRegistry: false,
|
|
1747
|
+
do: 'builtIns.defineCustomElement'
|
|
1748
|
+
},
|
|
1749
|
+
// Observe elements in shadow DOM registries
|
|
1750
|
+
shadowRegistry: {
|
|
1751
|
+
matching: 'shadow-element',
|
|
1752
|
+
whereDifferentCustomElementRegistry: true,
|
|
1753
|
+
do: 'builtIns.defineScopedCustomElement'
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
### Type Safety
|
|
531
1760
|
|
|
1761
|
+
When using TypeScript, the keys in the `with` property are inferred and provide autocomplete:
|
|
532
1762
|
|
|
1763
|
+
```TypeScript
|
|
1764
|
+
const observer = new MountObserver({
|
|
1765
|
+
matching: '.parent',
|
|
1766
|
+
with: {
|
|
1767
|
+
registry: { matching: 'my-element' },
|
|
1768
|
+
styles: { import: './styles.css' }
|
|
1769
|
+
},
|
|
1770
|
+
do: (el, ctx) => {
|
|
1771
|
+
ctx.withObservers?.registry // ✓ TypeScript knows this exists
|
|
1772
|
+
ctx.withObservers?.unknown // ✗ TypeScript error
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
```
|
|
533
1776
|
|
|
1777
|
+
### Key Benefits
|
|
534
1778
|
|
|
1779
|
+
1. **Declarative Composition**: Define complex observer hierarchies in a single configuration
|
|
1780
|
+
2. **Automatic Lifecycle**: Sub-observers are created and cleaned up automatically
|
|
1781
|
+
3. **Independent Operation**: Each sub-observer has its own configuration and state
|
|
1782
|
+
4. **Type Safety**: Full TypeScript support with key inference
|
|
1783
|
+
5. **Unlimited Nesting**: Create arbitrarily deep observer hierarchies
|
|
535
1784
|
|
|
1785
|
+
### Known Limitations
|
|
536
1786
|
|
|
1787
|
+
- **Circular References**: The library does not detect or prevent circular references in `with` configurations. Avoid configurations where observer A's `with` references observer B, and B's `with` references A, as this will cause a stack overflow.
|
|
537
1788
|
|
|
538
1789
|
## Mount Observer Script Elements (MOSEs)
|
|
539
1790
|
|
|
@@ -541,14 +1792,15 @@ Following an approach similar to the [speculation api](https://developer.chrome.
|
|
|
541
1792
|
|
|
542
1793
|
```JavaScript
|
|
543
1794
|
// myPackage/myDefiner.js
|
|
544
|
-
//
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
customElements.
|
|
1795
|
+
// My all powerful custom element definer
|
|
1796
|
+
export const mountConfig = {
|
|
1797
|
+
do: function({localName}, {modules, observer}) {
|
|
1798
|
+
if(!customElements.get(localName)) {
|
|
1799
|
+
customElements.define(localName, modules[1].MyElement);
|
|
1800
|
+
}
|
|
1801
|
+
observer.disconnectedSignal.abort();
|
|
548
1802
|
}
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
export { doFunction as do };
|
|
1803
|
+
};
|
|
552
1804
|
```
|
|
553
1805
|
|
|
554
1806
|
```html
|
|
@@ -556,18 +1808,14 @@ export { doFunction as do };
|
|
|
556
1808
|
{
|
|
557
1809
|
"select":"my-element",
|
|
558
1810
|
"import": [
|
|
559
|
-
["./my-element-small.css", {type: "css"}],
|
|
560
|
-
"./my-element.js"
|
|
561
|
-
"myPackage/myDefiner.js
|
|
1811
|
+
["./my-element-small.css", {"type": "css"}],
|
|
1812
|
+
"./my-element.js"
|
|
562
1813
|
],
|
|
563
|
-
"
|
|
1814
|
+
"configFrom": "myPackage/myDefiner.js"
|
|
564
1815
|
}
|
|
565
1816
|
</script>
|
|
566
1817
|
```
|
|
567
1818
|
|
|
568
|
-
To keep this proposal / polyfill of reasonable size, mount observer script elements has its own [repo / sub-proposal](https://github.com/bahrus/mount-observer-script-element). There's much more to it, but it is awaiting implementation of scoped custom element registry before finalizing the requirements and (re)-implementing.
|
|
569
|
-
|
|
570
|
-
But I think it's important to think about this way of making the mount observer declarative, as it provides one significant reason why we place so much emphasis on making sure that the mount observer settings (MountConfig) is as JSON serializable as possible.
|
|
571
1819
|
|
|
572
1820
|
|
|
573
1821
|
## Binding from a distance
|
|
@@ -607,7 +1855,7 @@ This allows developers to create "stylesheet" like capabilities.
|
|
|
607
1855
|
|
|
608
1856
|
## Registering reusable handlers with MountObserver.define
|
|
609
1857
|
|
|
610
|
-
To make MountConfig configurations more JSON-serializable and encourage code reuse,
|
|
1858
|
+
To make MountConfig configurations more JSON-serializable and encourage code reuse, we can register handler classes with string names and reference them by name:
|
|
611
1859
|
|
|
612
1860
|
```JavaScript
|
|
613
1861
|
import {EvtRt} from 'mount-observer/EvtRt.js';
|
|
@@ -660,25 +1908,6 @@ const observer = new MountObserver({
|
|
|
660
1908
|
|
|
661
1909
|
Handlers execute in the order specified. If a handler constructor throws an error, execution stops and subsequent handlers won't run.
|
|
662
1910
|
|
|
663
|
-
### Interaction with the reference property
|
|
664
|
-
|
|
665
|
-
When both `do` (with string/array) and `reference` are specified, the execution order is:
|
|
666
|
-
|
|
667
|
-
1. Inline `do` functions and registered handlers (from `do` strings), in whatever order they appear
|
|
668
|
-
2. Referenced `do` functions (from `reference` property)
|
|
669
|
-
|
|
670
|
-
```JavaScript
|
|
671
|
-
MountObserver.define('setup', SetupHandler);
|
|
672
|
-
|
|
673
|
-
const observer = new MountObserver({
|
|
674
|
-
matching: 'button',
|
|
675
|
-
import: './button-actions.js',
|
|
676
|
-
reference: 0,
|
|
677
|
-
do: ['setup', (el) => { el.dataset.ready = 'true'; }]
|
|
678
|
-
});
|
|
679
|
-
// Execution order: setup handler, inline function, then imported do function
|
|
680
|
-
```
|
|
681
|
-
|
|
682
1911
|
### Handler requirements
|
|
683
1912
|
|
|
684
1913
|
Registered handlers must be classes (constructors) that accept `(mountedElement: Element, ctx: MountContext)` as constructor parameters. They can be:
|
|
@@ -717,10 +1946,87 @@ MountObserver.define('myHandler', Handler2); // Error: myHandler already in use
|
|
|
717
1946
|
|
|
718
1947
|
### Global registry
|
|
719
1948
|
|
|
720
|
-
The handler registry is global and shared across all MountObserver instances, similar to the custom elements registry. Once a handler is registered, it can be used by any MountObserver instance in your application.
|
|
1949
|
+
The handler registry is global and shared across all MountObserver instances, similar to the global custom elements registry. Once a handler is registered, it can be used by any MountObserver instance in your application.
|
|
721
1950
|
|
|
722
1951
|
[Implemented as [Requirement14](requirements/Done/Requirement14.md)]
|
|
723
1952
|
|
|
1953
|
+
### Handler defaults with static properties
|
|
1954
|
+
|
|
1955
|
+
Registered handler classes can specify default MountConfig properties using static class properties. When we reference a handler by name, its static properties are automatically merged with your inline configuration, with inline config always taking precedence:
|
|
1956
|
+
|
|
1957
|
+
```JavaScript
|
|
1958
|
+
import {EvtRt} from 'mount-observer/EvtRt.js';
|
|
1959
|
+
|
|
1960
|
+
class MyHandler extends EvtRt {
|
|
1961
|
+
static matching = 'div > p + p ~ span[class$="name"]';
|
|
1962
|
+
static whereInstanceOf = HTMLSpanElement;
|
|
1963
|
+
|
|
1964
|
+
mount(mountedElement, MountConfig, context){
|
|
1965
|
+
mountedElement.textContent = 'hello';
|
|
1966
|
+
}
|
|
1967
|
+
dismount(mountedElement, MountConfig){
|
|
1968
|
+
mountedElement.textContent = 'bye';
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Register the handler
|
|
1973
|
+
MountObserver.define('myHandler', MyHandler);
|
|
1974
|
+
|
|
1975
|
+
// Use with defaults - will use handler's matching and whereInstanceOf
|
|
1976
|
+
const observer1 = new MountObserver({
|
|
1977
|
+
do: 'myHandler'
|
|
1978
|
+
});
|
|
1979
|
+
observer1.observe(document);
|
|
1980
|
+
|
|
1981
|
+
// Override specific properties - inline config trumps handler defaults
|
|
1982
|
+
const observer2 = new MountObserver({
|
|
1983
|
+
matching: 'span.special', // This overrides the handler's matching
|
|
1984
|
+
do: 'myHandler' // Still uses handler's whereInstanceOf
|
|
1985
|
+
});
|
|
1986
|
+
observer2.observe(document);
|
|
1987
|
+
```
|
|
1988
|
+
|
|
1989
|
+
**How it works:**
|
|
1990
|
+
1. When `do` is a string reference to a registered handler, the handler's static properties are extracted
|
|
1991
|
+
2. Static properties are merged with the inline config using object spread
|
|
1992
|
+
3. Inline config properties always override handler defaults (inline trumps)
|
|
1993
|
+
4. All MountConfig properties can be specified as static properties (matching, whereInstanceOf, withMediaMatching, etc.)
|
|
1994
|
+
|
|
1995
|
+
**Benefits:**
|
|
1996
|
+
- **DRY principle**: Define common configuration once in the handler class
|
|
1997
|
+
- **Flexibility**: Override any property when needed for specific use cases
|
|
1998
|
+
- **Composability**: Handlers become self-contained with their own default behavior
|
|
1999
|
+
- **JSON serialization**: Configurations remain JSON-serializable since only the handler name is referenced
|
|
2000
|
+
|
|
2001
|
+
**Example with multiple properties:**
|
|
2002
|
+
|
|
2003
|
+
```JavaScript
|
|
2004
|
+
class InputHandler extends EvtRt {
|
|
2005
|
+
static matching = 'input[type="text"]';
|
|
2006
|
+
static whereInstanceOf = HTMLInputElement;
|
|
2007
|
+
static withMediaMatching = '(min-width: 768px)';
|
|
2008
|
+
|
|
2009
|
+
mount(mountedElement, MountConfig, context){
|
|
2010
|
+
mountedElement.placeholder = 'Enter text...';
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
MountObserver.define('inputHandler', InputHandler);
|
|
2015
|
+
|
|
2016
|
+
// Uses all handler defaults
|
|
2017
|
+
const observer = new MountObserver({
|
|
2018
|
+
do: 'inputHandler'
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// Partially override - keeps whereInstanceOf and withMediaMatching from handler
|
|
2022
|
+
const observer2 = new MountObserver({
|
|
2023
|
+
matching: 'input[type="email"]', // Override matching only
|
|
2024
|
+
do: 'inputHandler'
|
|
2025
|
+
});
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
[Implemented as [SupportWhereCriteriaWithRegisteredActions](requirements/SupportWhereCriteriaWithRegisteredActions.md)]
|
|
2029
|
+
|
|
724
2030
|
### Built in handlers
|
|
725
2031
|
|
|
726
2032
|
This proposal advocates having the platform provide some built in handlers, that extend EvtRt, that is included with this Polyfill.
|
|
@@ -751,7 +2057,7 @@ export default class MyElement extends HTMLElement {
|
|
|
751
2057
|
}
|
|
752
2058
|
|
|
753
2059
|
// main.js
|
|
754
|
-
import { MountObserver } from 'mount-observer';
|
|
2060
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
755
2061
|
|
|
756
2062
|
const observer = new MountObserver({
|
|
757
2063
|
matching: 'my-element',
|
|
@@ -901,7 +2207,7 @@ Using `assignOn*` provides several benefits:
|
|
|
901
2207
|
|
|
902
2208
|
### Dynamically updating assignGingerly configuration
|
|
903
2209
|
|
|
904
|
-
The `MountObserver` class provides a public `assignGingerly()` method that allows
|
|
2210
|
+
The `MountObserver` class provides a public `assignGingerly()` method that allows us to merge new updates into the observer. This is useful for responding to user actions or application state changes:
|
|
905
2211
|
|
|
906
2212
|
```JavaScript
|
|
907
2213
|
const observer = new MountObserver({
|
|
@@ -928,7 +2234,7 @@ await observer.assignGingerly({
|
|
|
928
2234
|
|
|
929
2235
|
3. **Applies to future elements**: Future elements that mount will receive the merged configuration.
|
|
930
2236
|
|
|
931
|
-
4. **Starting without initial config**:
|
|
2237
|
+
4. **Starting without initial config**: We can call the method even if no `assignGingerly` was specified in the constructor:
|
|
932
2238
|
|
|
933
2239
|
```JavaScript
|
|
934
2240
|
const observer = new MountObserver({
|
|
@@ -962,7 +2268,7 @@ The method is async because the assign-gingerly library is loaded dynamically wh
|
|
|
962
2268
|
|
|
963
2269
|
## Reversible property assignment with stageOnMount
|
|
964
2270
|
|
|
965
|
-
While `assignOnMount` and `assignOnDismount` provide permanent property assignments, sometimes
|
|
2271
|
+
While `assignOnMount` and `assignOnDismount` provide permanent property assignments, sometimes we need temporary changes that automatically reverse when elements dismount. The `stageOnMount` property provides this capability using the `assignTentatively` function from assign-gingerly:
|
|
966
2272
|
|
|
967
2273
|
```JavaScript
|
|
968
2274
|
const observer = new MountObserver({
|
|
@@ -986,7 +2292,7 @@ When a matching button mounts, these properties are applied. When it dismounts (
|
|
|
986
2292
|
2. **Applies the new properties** when elements mount
|
|
987
2293
|
3. **Automatically reverses** to original values when elements dismount
|
|
988
2294
|
|
|
989
|
-
This is different from `assignOnMount`/`assignOnDismount`, where
|
|
2295
|
+
This is different from `assignOnMount`/`assignOnDismount`, where we must explicitly specify both the mount and dismount values.
|
|
990
2296
|
|
|
991
2297
|
### When to use stageOnMount vs assignOnMount
|
|
992
2298
|
|
|
@@ -1110,7 +2416,7 @@ const observer = new MountObserver({
|
|
|
1110
2416
|
observer.observe(document);
|
|
1111
2417
|
```
|
|
1112
2418
|
|
|
1113
|
-
This dispatches a `custom-ready` event from each matching button element when it mounts. Events bubble by default, so
|
|
2419
|
+
This dispatches a `custom-ready` event from each matching button element when it mounts. Events bubble by default, so we can listen at the document level:
|
|
1114
2420
|
|
|
1115
2421
|
```JavaScript
|
|
1116
2422
|
document.addEventListener('custom-ready', (e) => {
|
|
@@ -1278,7 +2584,7 @@ observer.observe(document);
|
|
|
1278
2584
|
|
|
1279
2585
|
## Element-specific lifecycle notifications with getNotifier
|
|
1280
2586
|
|
|
1281
|
-
While the MountObserver dispatches lifecycle events (mount, dismount, disconnect) at the observer level, sometimes
|
|
2587
|
+
While the MountObserver dispatches lifecycle events (mount, dismount, disconnect) at the observer level, sometimes we need to listen for events specific to a single element. The `getNotifier()` method returns an EventTarget that dispatches filtered events for only that element.
|
|
1282
2588
|
|
|
1283
2589
|
### Basic usage
|
|
1284
2590
|
|
|
@@ -1376,7 +2682,7 @@ getNotifier(element: Element): EventTarget
|
|
|
1376
2682
|
[Implemented as [Requirement13](requirements/Done/Requirement13.md)]
|
|
1377
2683
|
|
|
1378
2684
|
|
|
1379
|
-
## Extra lazy loading
|
|
2685
|
+
<!-- ## Extra lazy loading
|
|
1380
2686
|
|
|
1381
2687
|
By default, the matches would be reported as soon as an element matching the criterion is found or added into the DOM, inside the node specified by rootNode.
|
|
1382
2688
|
|
|
@@ -1391,53 +2697,17 @@ const observer = new MountObserver({
|
|
|
1391
2697
|
},
|
|
1392
2698
|
import: './my-element.js'
|
|
1393
2699
|
});
|
|
1394
|
-
```
|
|
1395
|
-
|
|
1396
|
-
## Media / container queries / instanceOf / custom checks [TODO] out of date
|
|
1397
|
-
|
|
1398
|
-
Unlike traditional CSS @import, CSS Modules don't support specifying different imports based on media queries. That can be another condition we can attach (and why not throw in container queries, based on the rootNode?):
|
|
1399
|
-
|
|
1400
|
-
```JavaScript
|
|
1401
|
-
const observer = new MountObserver({
|
|
1402
|
-
select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
|
|
1403
|
-
withMediaMatching: '(max-width: 1250px)',
|
|
1404
|
-
whereSizeOfContainerMatches: '(min-width: 700px)',
|
|
1405
|
-
whereContainerHas: '[itemprop=isActive][value="true"]',
|
|
1406
|
-
withInstance: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
|
|
1407
|
-
whereLangIn: ['en-GB'],
|
|
1408
|
-
whereConnectionHas:{
|
|
1409
|
-
effectiveTypeIn: ["slow-2g"],
|
|
1410
|
-
},
|
|
1411
|
-
import: ['./my-element-small.css', {type: 'css'}],
|
|
1412
|
-
do: ...
|
|
1413
|
-
});
|
|
1414
|
-
```
|
|
1415
|
-
|
|
1416
|
-
[withInstance implemented as [Requirement5](requirements/Done/Requirement5.md)]
|
|
1417
|
-
|
|
1418
|
-
[withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
|
|
1419
|
-
|
|
1420
|
-
## InstanceOf checks in detail
|
|
2700
|
+
``` -->
|
|
1421
2701
|
|
|
1422
|
-
Carving out the special "withInstance" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "confirm" callback logic (discussed later). For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
|
|
1423
|
-
|
|
1424
|
-
The picture becomes murkier for custom elements. The best solution in that case seems to be to utilize customElements.getName(...) as a basis for the match, but at first glance, that could preclude being able to use base classes which a family of custom elements subclass, if that superclass isn't itself a custom element. I suppose the solution to this conundrum, when warranted, is simply to burden the developer with defining a custom element for the superclass, and thus assigning it a name, applicable within ShadowDOM scopes as needed, even though it isn't actually necessarily used for any live custom elements. This would require already having imported the base class, only benefitting from lazy loading the code needed for each sub class, which might not always be all that high as a percentage, compared to the base class.
|
|
1425
|
-
|
|
1426
|
-
However, where this support for "withInstance" would be *most* helpful is when it comes to [*custom enhancements*](https://github.com/WICG/webcomponents/issues/1000) that only wish to lazily layer some heavy lifting functionality on top of certain families of already loaded and upgraded custom elements (possibly in addition to some (specified) built in elements). Here, the lazy loading of the *entire custom **enhancement***, based on the presence in the DOM of a member of the family of custom elements, would, if my calculations are correct, result in providing a significant benefit.
|
|
1427
2702
|
|
|
1428
2703
|
|
|
1429
|
-
<!--
|
|
1430
|
-
|
|
1431
|
-
[TODO] Maybe should also (optionally?) pass back which checks failed and which succeeded on dismount. Not sure I really see a use case for it, but leaving the thought here for now
|
|
1432
|
-
|
|
1433
|
-
-->
|
|
1434
|
-
|
|
1435
2704
|
## Subscribing
|
|
1436
2705
|
|
|
1437
2706
|
Subscribing can be done via:
|
|
1438
2707
|
|
|
1439
2708
|
```JavaScript
|
|
1440
|
-
|
|
2709
|
+
//[TODO] not implemented yet
|
|
2710
|
+
observer.addEventListener('shouldMount', e => {
|
|
1441
2711
|
e.isSatisfied = true; //or false to prevent the mount event below
|
|
1442
2712
|
});
|
|
1443
2713
|
observer.addEventListener('mount', e => {
|
|
@@ -1452,18 +2722,23 @@ observer.addEventListener('dismount', e => {
|
|
|
1452
2722
|
observer.addEventListener('disconnect', e => {
|
|
1453
2723
|
...
|
|
1454
2724
|
});
|
|
2725
|
+
//[TODO]
|
|
1455
2726
|
observer.addEventListener('move', e => {
|
|
1456
2727
|
...
|
|
1457
2728
|
});
|
|
2729
|
+
//[TODO]
|
|
1458
2730
|
observer.addEventListener('reconnect', e => {
|
|
1459
2731
|
...
|
|
1460
2732
|
});
|
|
2733
|
+
//[TODO]
|
|
1461
2734
|
observer.addEventListener('reconfirm', e => {
|
|
1462
2735
|
...
|
|
1463
2736
|
});
|
|
2737
|
+
//[TODO]
|
|
1464
2738
|
observer.addEventListener('exit', e => {
|
|
1465
2739
|
...
|
|
1466
2740
|
});
|
|
2741
|
+
//[TODO]
|
|
1467
2742
|
observer.addEventListener('forget', e => {
|
|
1468
2743
|
...
|
|
1469
2744
|
});
|
|
@@ -1514,7 +2789,7 @@ So the dismount event should provide a "checklist" of all the conditions, and th
|
|
|
1514
2789
|
mediaMatches: true,
|
|
1515
2790
|
containerMatches: true,
|
|
1516
2791
|
satisfiesCustomConditiselect: true,
|
|
1517
|
-
whereLangIn: ['en-GB'],
|
|
2792
|
+
// whereLangIn: ['en-GB'], // Not implemented - requires platform support
|
|
1518
2793
|
whereConnectiselect:{
|
|
1519
2794
|
effectiveTypeMatches: true
|
|
1520
2795
|
},
|
|
@@ -1826,8 +3101,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
|
|
|
1826
3101
|
<template mount='{
|
|
1827
3102
|
"select": ":not([defer-loading])",
|
|
1828
3103
|
"loadingEagerness": "eager",
|
|
1829
|
-
"withMediaMatching": "(min-width: 700px)"
|
|
1830
|
-
"whereLangIn": ["en-GB"],
|
|
3104
|
+
"withMediaMatching": "(min-width: 700px)"
|
|
1831
3105
|
}'>
|
|
1832
3106
|
<div>I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot></div>
|
|
1833
3107
|
</template>
|
|
@@ -1835,8 +3109,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
|
|
|
1835
3109
|
<template mount='{
|
|
1836
3110
|
"select": ":not([defer-loading])",
|
|
1837
3111
|
"loadingEagerness": "lazy",
|
|
1838
|
-
"withMediaMatching": "(max-width: 700px)"
|
|
1839
|
-
"whereLangIn": ["fr"],
|
|
3112
|
+
"withMediaMatching": "(max-width: 700px)"
|
|
1840
3113
|
}'>
|
|
1841
3114
|
<div>Je ne sais pas pourquoi tu dis <slot name=slot2></slot> je dis <slot name=slot1></slot></div>
|
|
1842
3115
|
</template>
|