mount-observer 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  [![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/mount-observer?style=for-the-badge)](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,7 +10,11 @@ The following features have been implemented and tested:
11
10
 
12
11
  ### Core Functionality
13
12
  - ✅ **matching**: CSS selector-based element matching
13
+ - ✅ **whenDefined**: Wait for custom elements to be defined before mounting
14
14
  - ✅ **whereInstanceOf**: Constructor-based element filtering (single or array)
15
+ - ✅ **whereLocalNameMatches**: Regular expression-based localName filtering
16
+ - ✅ **shouldMount**: Custom JavaScript check for complex mounting conditions
17
+ - ✅ **Registry matching**: Automatic filtering by customElementRegistry (Chrome 146+)
15
18
  - ✅ **withMediaMatching**: Media query-based conditional mounting (string or MediaQueryList)
16
19
  - ✅ **whereObservedRootSizeMatches**: Container query-based conditional mounting (observes root element size)
17
20
  - ✅ **whereElementIntersectsWith**: Intersection observer-based conditional mounting (observes element visibility)
@@ -29,6 +32,7 @@ The following features have been implemented and tested:
29
32
  - ✅ **assignOnDismount**: Property assignment when elements dismount
30
33
  - ✅ **stageOnMount**: Reversible property assignment (auto-restores on dismount)
31
34
  - ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
35
+ - ✅ **with property**: Hierarchical observer composition with sub-observers
32
36
  - ✅ **Element mount extension**: element.mount() method for scoped registry observation
33
37
  - ✅ **Shared MutationObserver**: Efficient observer sharing across instances
34
38
  - ✅ **Code splitting**: Conditional features loaded on-demand
@@ -80,7 +84,9 @@ There is quite a bit of functionality this proposal would open up that is exceed
80
84
 
81
85
  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.
82
86
 
83
- 4. Some CSS selectors, such as the [scope donut hole 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
+ 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(...).
88
+
89
+ 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. Learn more about [scoped custom element registries](https://developer.chrome.com/blog/scoped-registries).
84
90
 
85
91
 
86
92
  ### Most significant use cases
@@ -98,7 +104,7 @@ The extra flexibility this new primitive would provide could be quite useful to
98
104
 
99
105
  ## Quick Examples of the Most Common Use Cases
100
106
 
101
- Before getting into the weeds, let's demonstrate the two most prominent use cases:
107
+ Before getting into the weeds, let's demonstrate a few of the most prominent use cases:
102
108
 
103
109
  ### Use Case 1: Custom Attribute Enhancement
104
110
 
@@ -107,7 +113,7 @@ Before getting into the weeds, let's demonstrate the two most prominent use case
107
113
  <div log-to-console="clicked on a div">hello</div>
108
114
 
109
115
  <script type=module>
110
- document.body.mount({
116
+ document.mount({
111
117
  matching: '[log-to-console]',
112
118
  do: (el) => {
113
119
  el.addEventListener('click', e => {
@@ -166,7 +172,7 @@ element.mount({
166
172
 
167
173
  ## Enhancing Elements with assign-gingerly
168
174
 
169
- The `builtIns.enhanceMountedElement` handler automatically enhances mounted elements using the [assign-gingerly](https://www.npmjs.com/package/assign-gingerly) enhancement system. This allows you to attach behavior and state to elements without subclassing.
175
+ 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.
170
176
 
171
177
  ```JavaScript
172
178
  // MyEnhancement.js
@@ -188,6 +194,8 @@ export default {
188
194
  enhKey: 'buttonEnh'
189
195
  };
190
196
 
197
+ // main.js
198
+
191
199
  document.mount({
192
200
  matching: '.enhance-me',
193
201
  import: './MyEnhancement.js',
@@ -205,251 +213,1604 @@ console.log(button.enh.buttonEnh.clickCount); // 1
205
213
  ```
206
214
 
207
215
  The handler:
208
- 1. Searches the imported module for an export with a `spawn` property (the enhancement class)
216
+ 1. Searches the imported module for an export with a `spawn` property (the enhancement class), starting with default.
209
217
  2. Calls `element.enh.get(registryItem, context)` to spawn the enhancement
210
218
  3. Stores the enhancement instance on `element.enh[enhKey]` if an `enhKey` is provided
211
219
 
212
- This works with browsers that don't support scoped custom element registries by polyfilling the `customElementRegistry` property on elements.
213
220
 
221
+ ## Exposing Module Exports from Script Elements
214
222
 
215
- # Thorough Exposition Begins Here
223
+ The `builtIns.scriptExport` handler solves a long-standing limitation: accessing ES module exports from script elements. It also provides a clean way to import JSON and other data formats declaratively.
216
224
 
217
- Okay, let's get into the weeds. First, we strongly recommend studying the core package that mount-observer extends, [assign-gingerly](https://www.npmjs.com/package/assign-gingerly).
225
+ ### Problem 1: ES Module Export Access
218
226
 
219
- ## First use case -- lazy loading custom elements without sugar coating
227
+ The browser doesn't expose module exports from `<script type="module">` elements. There's been a [decade-old proposal](https://github.com/whatwg/html/issues/1013) to add this, but it remains unimplemented.
220
228
 
221
- This registers the custom element in the global registry.
229
+ **The Solution:**
222
230
 
223
- ```JavaScript
224
- const observer = new MountObserver({
225
- select:'my-element', //not supported by this polyfill
226
- import: './my-element.js',
227
- do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
228
- if(!customElements.get(localName)) {
229
- customElements.define(localName, modules[0].MyElement);
230
- }
231
- observer.disconnectedSignal.abort();
232
- }
233
-
234
- }, {disconnectedSignal: new AbortController().signal});
235
- observer.observe(document);
231
+ ```html
232
+ <!-- Use nomodule to prevent browser from loading it separately -->
233
+ <script nomodule src="./config.js" id="myConfig"></script>
234
+
235
+ <script type="module">
236
+ import { MountObserver } from 'mount-observer/MountObserver.js';
237
+
238
+ const observer = new MountObserver({
239
+ do: 'builtIns.scriptExport'
240
+ });
241
+ observer.observe(document);
242
+
243
+ // Access the module's exports via element.export
244
+ const config = document.getElementById('myConfig').export;
245
+ console.log(config.apiKey);
246
+ console.log(config.endpoints);
247
+ </script>
236
248
  ```
237
249
 
238
- The do function will *only be called once per matching element* -- i.e. if the element stops matching the "select" criteria, then matches again, the do function won't be called again. It will be called for all elements when they match within the scope passed in to the observe method. However, the events discussed below, will continue to be called repeatedly.
250
+ **Why `nomodule`?**
251
+ - Prevents the browser from loading the module separately
252
+ - Avoids having the module loaded twice in memory
253
+ - The handler imports it once and exposes exports via `element.export`
239
254
 
240
- The constructor argument can also be an array of objects that fit the pattern shown above.
255
+ ### Problem 2: Declarative JSON Import
241
256
 
242
- In fact, as we will see, where it makes sense, where we see examples that are strings, we will also allow for arrays of such strings. For example, the "select" key can point to an array of CSS selectors (and in this case the mount/dismount callbacks would need to provide an index of which one matched). I only recommend adding this complexity if what I suspect is true -- providing this support can reduce "context switching" between threads / memory spaces (c++ vs JavaScript), and thus improve performance. If multiple "on" selectors are provided, and multiple ones match, I think it makes sense to indicate the one with the highest specifier that matches. It would probably be helpful in this case to provide a special event that allows for knowing when the matching selector with the highest specificity changes for mounted elements.
257
+ Importing JSON typically requires `fetch()` or dynamic `import()` with assertions. This handler provides a declarative alternative.
243
258
 
244
- If no imports are specified, it would go straight to do (if any such callbacks are specified), and it will also dispatch events as discussed below.
259
+ **The Solution:**
245
260
 
246
- This only searches for elements matching 'my-element' outside any shadow DOM.
261
+ ```html
262
+ <!-- Load JSON data -->
263
+ <script src="./data.json" type="json" id="myData"></script>
247
264
 
248
- But the observe method can accept a node within the document, or a shadowRoot, or a node inside a shadowRoot as well.
265
+ <!-- Also supports full MIME types -->
266
+ <script src="./config.json" type="application/json" id="config"></script>
267
+ <script src="./linked-data.json" type="application/ld+json" id="linkedData"></script>
249
268
 
250
- The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
269
+ <script type="module">
270
+ import { MountObserver } from 'mount-observer/MountObserver.js';
271
+
272
+ const observer = new MountObserver({
273
+ do: 'builtIns.scriptExport'
274
+ });
275
+ observer.observe(document);
276
+
277
+ // Access the JSON data via element.export.default
278
+ const data = document.getElementById('myData').export.default;
279
+ console.log(data.items);
280
+ console.log(data.config);
281
+ </script>
282
+ ```
251
283
 
252
- > [!Note]
253
- > Reading through the historical links tied to the selector-observer proposal this proposal helped spawn, I may have painted an overly optimistic picture of [what the platform is capable of](https://github.com/whatwg/dom/issues/398). It does leave me a little puzzled why this isn't an issue when it comes to styling, and also if some of the advances that were utilized to support :has could be applied to this problem space, so that maybe the arguments raised there have weakened. Even if the concerns raised are as relevant today, I think considering the use cases this proposal envisions, that the objections could be overcome, for the following reasons: 1. For scenarios where lazy loading is the primary objective, "bunching" multiple DOM mutations together and only reevaluating when things are quite idle is perfectly reasonable. Also, for binding from a distance, most of the mutations that need responding to quickly will be when the *state of the host* changes, so DOM mutations play a somewhat muted role in that regard. Again, bunching multiple DOM mutations together, even if adds a bit of a delay, also seems reasonable. I also think the platform could add an "analysis" step to look at the query and categorize it as "simple" queries vs complex. Selector queries that are driven by the characteristics of the element itself (localName, attributes, etc) could be handled in a more expedited fashion. Those that the platform does expect to require more babysitting could be monitored for less vigilantly. Maybe in the latter case, a console.warning could be emitted during initialization. The other use case, for lazy loading custom elements and custom enhancements based on attributes, I think most of the time this would fit the "simple" scenario, so again there wouldn't be much of an issue.
284
+ <details>
285
+ <summary>Talking points</summary>
254
286
 
255
- In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but have to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable in some circumstances.
287
+ **Why no `nomodule` for JSON?**
288
+ - The browser ignores script elements with non-standard type attributes
289
+ - No risk of double-loading since the browser won't load it at all
290
+ - The handler imports it with the appropriate JSON assertion
291
+
292
+ **Supported JSON types:**
293
+ - `type="json"` - Simple and clean
294
+ - `type="application/json"` - Standard MIME type
295
+ - `type="application/ld+json"` - JSON-LD linked data
296
+ - Any type containing "json" triggers JSON import assertion
297
+
298
+ **How it works:**
299
+ 1. Matches `script[src]` elements (via static properties)
300
+ 2. Skips `type="module"` scripts (browser-handled)
301
+ 3. Processes scripts with `nomodule` attribute OR type containing "json"
302
+ 4. Resolves the `src` relative to the document
303
+ 5. Imports with appropriate assertion (JSON if type contains "json")
304
+ 6. Stores the imported module on `element.export`
305
+ 7. Dispatches a `resolved` event with the imported module
306
+
307
+ **Reusing imported modules:**
308
+
309
+ The handler stores the imported module on the script element's `export` property and dispatches a `resolved` event. This allows other code to access the module without re-importing:
310
+
311
+ ```html
312
+ <script src="./data.json" type="json" id="myData"></script>
313
+
314
+ <script type="module">
315
+ const dataScript = document.getElementById('myData');
316
+
317
+ // Listen for the resolved event
318
+ dataScript.addEventListener('resolved', (e) => {
319
+ console.log('Data loaded:', e.export);
320
+ // e.export contains the imported module
321
+ // For JSON: e.export.default contains the data
322
+ });
323
+
324
+ // Or access directly after processing
325
+ // dataScript.export will contain the imported module
326
+ </script>
327
+ ```
328
+
329
+ This is particularly useful when multiple components need to access the same data or configuration without triggering multiple imports.
330
+
331
+ **Benefits:**
332
+ - Access ES module exports from script elements (finally!)
333
+ - Declarative JSON loading without fetch
334
+ - Prevents double-loading of modules
335
+ - Clean, intuitive syntax
336
+ - Works with relative and absolute URLs
337
+ - No need to specify `matching` or `whereInstanceOf` (handler provides defaults)
338
+
339
+ **Use cases:**
340
+ - Accessing configuration module exports in HTML
341
+ - Loading JSON data declaratively
342
+ - Progressive enhancement with module loading
343
+ - Declarative dependency management
344
+ - Loading JSON-LD structured data
345
+
346
+ </details>
347
+
348
+ ## Mount Observer Script Elements (MOSEs)
349
+
350
+ Inspired by the [speculation rules api](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) `builtIns.mountObserverScript` handler enables fully declarative mount observer configuration using `<script type="mountobserver">` elements. This provides the ultimate in HTML-first progressive enhancement.
351
+
352
+ ```html
353
+ <!-- Inline JSON configuration -->
354
+ <script type="mountobserver">
355
+ {
356
+ "matching": "my-fancy-button",
357
+ "import": "./fancy-button.js",
358
+ "do": "builtIns.defineCustomElement"
359
+ }
360
+ </script>
361
+
362
+ <!-- External JSON configuration -->
363
+ <script type="mountobserver" src="./observer-config.json"></script>
364
+
365
+ <!-- Bootstrap the handler -->
366
+ <script type="module">
367
+ import { MountObserver } from 'mount-observer/MountObserver.js';
368
+
369
+ // Handler provides matching and whereInstanceOf via static properties
370
+ const observer = new MountObserver({
371
+ do: 'builtIns.mountObserverScript'
372
+ });
373
+ observer.observe(document);
374
+ </script>
375
+ ```
376
+
377
+ <details>
378
+ <summary>Talking points</summary>
379
+
380
+ **How it works:**
381
+ 1. The handler matches `script[type="mountobserver"]` elements (via static properties)
382
+ 2. If the script has a `src` attribute, imports JSON from that URL
383
+ 3. Otherwise, parses the script's textContent as JSON
384
+ 4. Supports both single config objects and arrays of configs
385
+ 5. Stores the parsed config on `scriptElement.export` for reuse
386
+ 6. Dispatches a `resolved` event with the parsed config
387
+ 7. Calls `scriptElement.mount(config)` for each configuration
388
+ 8. The `mount()` method creates a MountObserver for that configuration
389
+
390
+ **Reusing parsed configurations:**
391
+
392
+ The handler optimizes performance by storing the parsed configuration on the script element's `export` property and dispatching a `resolved` event. When the same script element is observed again (e.g., after being cloned or moved), the handler reuses the existing `export` instead of re-parsing:
393
+
394
+ ```html
395
+ <script type="mountobserver" id="myConfig">
396
+ {
397
+ "matching": ".my-element",
398
+ "do": "builtIns.logToConsole"
399
+ }
400
+ </script>
401
+
402
+ <script type="module">
403
+ const configScript = document.getElementById('myConfig');
404
+
405
+ // Listen for the resolved event (fires only once on first parse)
406
+ configScript.addEventListener('resolved', (e) => {
407
+ console.log('Config loaded:', e.export);
408
+ // e.export contains the parsed configuration
409
+ });
410
+
411
+ // Or access directly after processing
412
+ // configScript.export will contain the parsed config
413
+
414
+ // If observed again, the handler will reuse configScript.export
415
+ // without re-parsing or firing the resolved event
416
+ </script>
417
+ ```
418
+
419
+ This is particularly useful when inheriting mount observer configurations across shadow DOM boundaries, as the parsed config can be reused without re-parsing JSON. The `resolved` event fires only once (on first parse), but the handler will still process the configuration on subsequent observations.
420
+
421
+ **Multiple configs in one script:**
422
+
423
+ You can define multiple mount observer configurations in a single script element using a JSON array:
424
+
425
+ ```html
426
+ <script type="mountobserver">
427
+ [
428
+ {
429
+ "do": "builtIns.hoistTemplate"
430
+ },
431
+ {
432
+ "do": "builtIns.HTMLInclude"
433
+ },
434
+ {
435
+ "matching": "my-button",
436
+ "import": "./my-button.js",
437
+ "do": "builtIns.defineCustomElement"
438
+ }
439
+ ]
440
+ </script>
441
+ ```
442
+
443
+ This is equivalent to having three separate `<script type="mountobserver">` elements, but more concise. Each config in the array is processed independently and creates its own MountObserver.
444
+
445
+ **Benefits:**
446
+ - Zero JavaScript required for observer configuration
447
+ - Configurations are pure JSON (fully serializable)
448
+ - Easy to generate server-side or from build tools
449
+ - Supports both inline and external configurations
450
+ - Leverages the `element.mount()` API for automatic scope management
451
+ - No need to specify `matching` or `whereInstanceOf` for the handler itself
452
+
453
+ **Use cases:**
454
+ - Server-side rendering with progressive enhancement
455
+ - Build-time generation of observer configurations
456
+ - CMS-driven component loading
457
+ - Declarative micro-frontend architecture
458
+ - Configuration management without JavaScript bundling
459
+
460
+ **Example with multiple configurations:**
461
+ ```html
462
+ <!-- Load custom elements -->
463
+ <script type="mountobserver">
464
+ {
465
+ "matching": "my-button",
466
+ "import": "./components/my-button.js",
467
+ "do": "builtIns.defineCustomElement"
468
+ }
469
+ </script>
470
+
471
+ <!-- Enhance existing elements -->
472
+ <script type="mountobserver">
473
+ {
474
+ "matching": ".interactive",
475
+ "import": "./enhancements/interactive.js",
476
+ "do": "builtIns.enhanceMountedElement"
477
+ }
478
+ </script>
479
+
480
+ <!-- Single bootstrap script activates all configurations -->
481
+ <script type="module">
482
+ import { MountObserver } from 'mount-observer/MountObserver.js';
483
+
484
+ new MountObserver({
485
+ do: 'builtIns.mountObserverScript'
486
+ }).observe(document);
487
+ </script>
488
+ ```
489
+
490
+ </details>
491
+
492
+ ## Hoisting Templates for Performance
493
+
494
+ The `builtIns.hoistTemplate` handler optimizes template usage by moving a template element's content from shadow roots to `document.head`.
495
+
496
+ **Why hoist templates?**
497
+
498
+ Template hoisting is particularly useful when you need to share conditional or repeated templates across multiple custom element instances.
499
+
500
+ 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:
501
+ - Reduces memory usage (one template instead of many copies)
502
+ - Improves cloning performance (single source of truth)
503
+ - Maintains the same API through the `remoteContent` getter
504
+
505
+ **Basic usage:**
506
+
507
+ ```html
508
+ <my-web-component>
509
+ #shadow
510
+ <template id="my-template">
511
+ <div>My content</div>
512
+ </template>
513
+ </my-web-component>
514
+
515
+ <script type="module">
516
+ import { MountObserver } from 'mount-observer/MountObserver.js';
517
+
518
+ const observer = new MountObserver({
519
+ do: 'builtIns.hoistTemplate'
520
+ });
521
+ observer.observe(document);
522
+ </script>
523
+ ```
524
+
525
+ **What happens:**
526
+ 1. The handler finds templates with IDs in shadow roots
527
+ 2. Moves the template content to a new template in `<head>`
528
+ 3. Updates the original template with `src="#mount-observer-0"` (unique ID)
529
+ 4. Defines a `remoteContent` getter that returns the hoisted template's content
530
+
531
+ **Accessing hoisted content:**
532
+
533
+ ```javascript
534
+ const template = shadowRoot.querySelector('#my-template');
535
+
536
+ // After hoisting, use remoteContent to access the content
537
+ const content = template.remoteContent; // Returns DocumentFragment
538
+ const clone = content.cloneNode(true); // Clone the content
539
+ ```
540
+ <details>
541
+ <summary>Matching criteria
542
+
543
+ The handler automatically hoists templates that:
544
+ - Have an `id` attribute
545
+ - Don't already have a `src` attribute
546
+ - Are in a shadow root (or disconnected, being cloned)
547
+ - Have content (empty templates are skipped)
548
+
549
+ **Declarative usage with MOSE:**
550
+
551
+ ```html
552
+ <script type="mountobserver">
553
+ {
554
+ "do": "builtIns.hoistTemplate"
555
+ }
556
+ </script>
557
+
558
+ <script type="module">
559
+ import { MountObserver } from 'mount-observer/MountObserver.js';
560
+
561
+ new MountObserver({
562
+ do: 'builtIns.mountObserverScript'
563
+ }).observe(document);
564
+ </script>
565
+ ```
566
+
567
+ [Implemented as HoistingTemplates requirement](requirements/Done/HoistingTemplates.md)
568
+
569
+ </details>
570
+
571
+ ## Intra-Document HTML Includes with HTMLInclude
572
+
573
+ 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.
574
+
575
+ **Why use HTML includes?**
576
+
577
+ - Reduces duplication of repeated HTML structures
578
+ - Enables template-based content generation
579
+ - Supports partial updates via matching insertions
580
+ - Works across shadow DOM boundaries
581
+ - Supports declarative shadow DOM attachment
582
+ - Caches lookups for performance
583
+ - Detects circular references automatically
584
+ - Can be used to inherit from MOSEs
585
+
586
+ **Basic usage - Simple cloning:**
587
+
588
+ ```html
589
+ <!-- Define reusable content -->
590
+ <div id="reusable">
591
+ <p>This content can be reused</p>
592
+ <button>Click me</button>
593
+ </div>
594
+
595
+ <!-- Reference it with a template -->
596
+ <template src="#reusable"></template>
597
+
598
+ <!-- Results in: -->
599
+ <div>
600
+ <p>This content can be reused</p>
601
+ <button>Click me</button>
602
+ </div>
603
+
604
+ <script type="module">
605
+ import { MountObserver } from 'mount-observer/MountObserver.js';
606
+
607
+ const observer = new MountObserver({
608
+ do: 'builtIns.HTMLInclude'
609
+ });
610
+ observer.observe(document);
611
+ </script>
612
+ ```
613
+
614
+ <details>
615
+ <summary>More discussion
616
+
617
+ **What happens:**
618
+ 1. The handler finds templates with `src` attributes starting with `#`
619
+ 2. Searches for an element with that ID (across shadow boundaries)
620
+ 3. Clones the content from the source element
621
+ 4. Replaces the template with the cloned content
622
+ 5. Removes the `id` attribute from cloned elements to avoid duplicate IDs
623
+
624
+ **Cloning priority:**
625
+ 1. `remoteContent` property (hoisted templates) - highest priority
626
+ 2. `content` property (regular templates)
627
+ 3. The element itself (any element with an ID)
628
+
629
+ **Works with hoisted templates:**
630
+
631
+ ```html
632
+ <my-web-component>
633
+ #shadow
634
+ <template id="my-template">
635
+ <div>Hoisted content</div>
636
+ </template>
637
+ </my-web-component>
638
+
639
+ <!-- After hoisting, this still works -->
640
+ <template src="#my-template"></template>
641
+
642
+ <script type="module">
643
+ import { MountObserver } from 'mount-observer/MountObserver.js';
644
+
645
+ // First hoist templates
646
+ new MountObserver({
647
+ do: 'builtIns.hoistTemplate'
648
+ }).observe(document);
649
+
650
+ // Then use them
651
+ new MountObserver({
652
+ do: 'builtIns.HTMLInclude'
653
+ }).observe(document);
654
+ </script>
655
+ ```
656
+
657
+ </details>
658
+
659
+ ### Shadow DOM Support
660
+
661
+ 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.
662
+
663
+ **Basic shadow DOM usage:**
664
+
665
+ ```html
666
+ <!-- Define reusable shadow content -->
667
+ <template id="shadow-content">
668
+ <style>
669
+ :host {
670
+ display: block;
671
+ padding: 10px;
672
+ }
673
+ .shadow-text {
674
+ color: blue;
675
+ }
676
+ </style>
677
+ <div class="shadow-text">
678
+ <slot name="greeting"></slot>
679
+ <slot></slot>
680
+ </div>
681
+ </template>
682
+
683
+ <!-- Attach to shadow root -->
684
+ <div class="host-element">
685
+ <template src="#shadow-content" shadowrootmodeonload="open"></template>
686
+ <span slot="greeting">Hello</span>
687
+ <span>World!</span>
688
+ </div>
689
+
690
+ <script type="module">
691
+ import { MountObserver } from 'mount-observer/MountObserver.js';
692
+
693
+ new MountObserver({
694
+ do: 'builtIns.HTMLInclude'
695
+ }).observe(document);
696
+ </script>
697
+ ```
698
+
699
+ **What happens:**
700
+ 1. The handler checks for the `shadowrootmodeonload` attribute (case-insensitive)
701
+ 2. If present, it attaches the cloned content to the parent element's shadow root
702
+ 3. If the parent doesn't have a shadow root, one is created with the specified mode
703
+ 4. If a shadow root already exists, the content is appended to it
704
+ 5. The template is removed as usual
705
+
706
+ **Shadow root modes:**
707
+ - `open` - Shadow root is accessible via `element.shadowRoot`
708
+ - `closed` - Shadow root is not accessible from outside
709
+
710
+ **Slots work automatically:**
711
+
712
+ 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.
713
+
714
+ **Example - Complex nested structure:**
715
+
716
+ ```html
717
+ <template id="chorus">
718
+ <template src="#beautiful">
719
+ <span slot="subjectIs">
720
+ <slot name="subjectIs1"></slot>
721
+ </span>
722
+ </template>
723
+ <div>No matter what they say</div>
724
+ <div>Words <slot name="verb1"></slot> bring <slot name="pronoun1"></slot> down</div>
725
+ </template>
726
+
727
+ <div class="chorus">
728
+ <template src="#chorus" shadowrootmodeonload="open"></template>
729
+ <span slot="verb1">can't</span>
730
+ <span slot="pronoun1">me</span>
731
+ <span slot="subjectIs1">I am</span>
732
+ </div>
733
+ ```
734
+
735
+ **Nested templates in shadow DOM:**
736
+
737
+ Templates inside shadow roots are not automatically processed by the parent observer. To process nested templates, you need to observe the shadow root separately:
738
+
739
+ ```javascript
740
+ const host = document.querySelector('.host-element');
741
+ if (host.shadowRoot) {
742
+ const shadowObserver = new MountObserver({
743
+ do: 'builtIns.HTMLInclude'
744
+ });
745
+ await shadowObserver.observe(host.shadowRoot);
746
+ }
747
+ ```
748
+
749
+ **Error handling:**
750
+
751
+ - Invalid mode values: Logs warning if mode is not `"open"` or `"closed"`
752
+ - Missing parent: Logs warning if template has no parent element
753
+ - Attachment failures: Logs error if shadow root cannot be attached
754
+
755
+ ### Matching Insertions - Partial Updates
756
+
757
+ 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.
758
+
759
+ **How it works:**
760
+ 1. Template children generate CSS selectors (tag, classes, attributes)
761
+ 2. Matching elements in the cloned content are found
762
+ 3. Matched elements have their children replaced and attributes updated
763
+ 4. The `-i` attribute specifies which attributes to update
764
+
765
+ **Example - Updating attributes:**
766
+
767
+ ```html
768
+ <!-- Source content -->
769
+ <div itemscope id="love">
770
+ <data value="false" itemprop="todayIsFriday">It's Thursday</data>
771
+ </div>
772
+
773
+ <!-- Template with matching insertion -->
774
+ <template src="#love">
775
+ <data value="true" itemprop="todayIsFriday" -i="value"></data>
776
+ </template>
777
+
778
+ <!-- Results in: -->
779
+ <div itemscope>
780
+ <data value="true" itemprop="todayIsFriday">It's Thursday</data>
781
+ </div>
782
+ <!-- The value attribute is updated, but content stays "It's Thursday" -->
783
+ ```
784
+
785
+ **The `-i` attribute:**
786
+
787
+ The `-i` (insert) attribute is a space-separated list of attribute names to update on matched elements. Attributes listed in `-i` are:
788
+ - Excluded from the CSS selector (allows matching elements with different values)
789
+ - Updated on matched elements with values from the template child
790
+
791
+ ```html
792
+ <template src="#form">
793
+ <!-- Update both value and placeholder -->
794
+ <input type="text" name="username" value="new" placeholder="Updated" -i="value placeholder">
795
+ </template>
796
+ ```
797
+
798
+ **Example - Replacing content:**
799
+
800
+ ```html
801
+ <!-- Source -->
802
+ <div id="greeting">
803
+ <p class="message">Hello</p>
804
+ </div>
805
+
806
+ <!-- Template replaces content -->
807
+ <template src="#greeting">
808
+ <p class="message">Goodbye</p>
809
+ </template>
810
+
811
+ <!-- Results in: -->
812
+ <div>
813
+ <p class="message">Goodbye</p>
814
+ </div>
815
+ ```
816
+
817
+ **Example - Multiple matching elements:**
818
+
819
+ ```html
820
+ <!-- Source with multiple items -->
821
+ <div id="list">
822
+ <span class="item">Item 1</span>
823
+ <span class="item">Item 2</span>
824
+ <span class="item">Item 3</span>
825
+ </div>
826
+
827
+ <!-- Update all matching items -->
828
+ <template src="#list">
829
+ <span class="item">Updated</span>
830
+ </template>
831
+
832
+ <!-- Results in: -->
833
+ <div>
834
+ <span class="item">Updated</span>
835
+ <span class="item">Updated</span>
836
+ <span class="item">Updated</span>
837
+ </div>
838
+ ```
839
+
840
+ **Example - Nulling out content:**
841
+
842
+ ```html
843
+ <!-- Source -->
844
+ <div id="status">
845
+ <span data-active="false" class="indicator">Inactive</span>
846
+ </div>
847
+
848
+ <!-- Update attribute, remove content -->
849
+ <template src="#status">
850
+ <span data-active="true" class="indicator" -i="data-active"></span>
851
+ </template>
852
+
853
+ <!-- Results in: -->
854
+ <div>
855
+ <span data-active="true" class="indicator"></span>
856
+ </div>
857
+ <!-- Content is removed, attribute is updated -->
858
+ ```
859
+
860
+ ### Use Case: Inheriting Groups of Mount-Observers
861
+
862
+ 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.
863
+
864
+ **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.
865
+
866
+ ```html
867
+ <!-- Base component with mount-observers -->
868
+ <template id="base-observers">
869
+ <script type="mountobserver">
870
+ {
871
+ "matching": "button.primary",
872
+ "import": "./primary-button.js",
873
+ "do": "builtIns.defineCustomElement"
874
+ }
875
+ </script>
876
+
877
+ <script type="mountobserver">
878
+ {
879
+ "matching": ".interactive",
880
+ "import": "./interactive.js",
881
+ "do": "builtIns.enhanceMountedElement"
882
+ }
883
+ </script>
884
+
885
+ <script type="mountobserver">
886
+ {
887
+ "matching": "form",
888
+ "import": "./form-validator.js",
889
+ "do": "builtIns.enhanceMountedElement"
890
+ }
891
+ </script>
892
+ </template>
893
+
894
+ <!-- Derived component - inherit and customize -->
895
+ <my-derived-component>
896
+ #shadow
897
+ <!-- Include base observers -->
898
+ <template src="#base-observers">
899
+ <!-- Override the form validator with a different one -->
900
+ <script type="mountobserver">
901
+ {
902
+ "matching": "form",
903
+ "import": "./custom-form-validator.js",
904
+ "do": "builtIns.enhanceMountedElement"
905
+ }
906
+ </script>
907
+ </template>
908
+
909
+ <!-- Component content -->
910
+ <form>...</form>
911
+ <button class="primary">Submit</button>
912
+ </my-derived-component>
913
+
914
+ <script type="module">
915
+ import { MountObserver } from 'mount-observer/MountObserver.js';
916
+
917
+ // Bootstrap HTMLInclude handler
918
+ new MountObserver({
919
+ do: 'builtIns.HTMLInclude'
920
+ }).observe(document);
921
+
922
+ // Bootstrap MOSE handler to activate the observers
923
+ new MountObserver({
924
+ do: 'builtIns.mountObserverScript'
925
+ }).observe(document);
926
+ </script>
927
+ ```
928
+
929
+ **What happens:**
930
+ 1. The `<template src="#base-observers">` clones all three MOSE scripts
931
+ 2. The matching insertion finds the form validator script (matching by `matching` attribute)
932
+ 3. Replaces its content with the custom validator configuration
933
+ 4. All three scripts are inserted into the shadow root
934
+ 5. The MOSE handler activates all observers in the shadow root's registry scope
935
+
936
+ **Benefits:**
937
+ - **Composition**: Build complex observer configurations from reusable pieces
938
+ - **Inheritance**: Derive new components with modified observer behavior
939
+ - **Scoped registries**: Each shadow root gets its own set of observers
940
+ - **Declarative**: No JavaScript required for observer inheritance
941
+ - **Maintainable**: Update base observers in one place, changes propagate
942
+
943
+ **Advanced pattern - Multiple inheritance:**
944
+
945
+ ```html
946
+ <!-- Base UI observers -->
947
+ <template id="ui-observers">
948
+ <script type="mountobserver">{"matching": "button", ...}</script>
949
+ <script type="mountobserver">{"matching": "input", ...}</script>
950
+ </template>
951
+
952
+ <!-- Base data observers -->
953
+ <template id="data-observers">
954
+ <script type="mountobserver">{"matching": "[itemscope]", ...}</script>
955
+ </template>
956
+
957
+ <!-- Component combines both -->
958
+ <my-component>
959
+ #shadow
960
+ <template src="#ui-observers"></template>
961
+ <template src="#data-observers"></template>
962
+
963
+ <!-- Add component-specific observers -->
964
+ <script type="mountobserver">
965
+ {
966
+ "matching": ".special",
967
+ "import": "./special.js",
968
+ "do": "builtIns.enhanceMountedElement"
969
+ }
970
+ </script>
971
+ </my-component>
972
+ ```
973
+
974
+ This pattern enables:
975
+ - **Mixins**: Combine multiple observer groups
976
+ - **Layering**: Stack observers from different concerns (UI, data, behavior)
977
+ - **Customization**: Override specific observers while keeping others
978
+ - **Reusability**: Share observer configurations across components
979
+
980
+ **Declarative usage with MOSE:**
981
+
982
+ ```html
983
+ <script type="mountobserver">
984
+ {
985
+ "do": "builtIns.HTMLInclude"
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
+ **Error handling:**
999
+
1000
+ The handler provides helpful error messages:
1001
+ - Missing elements: `data-include-error="Element with id='foo' not found"`
1002
+ - Circular references: `data-include-error="Circular reference detected: #foo"`
1003
+ - Clone failures: `data-include-error="Unable to clone content from #foo"`
1004
+
1005
+ **Performance:**
1006
+
1007
+ - Uses WeakMap caching for repeated ID lookups
1008
+ - Efficient for scenarios like periodic tables with many repeated elements
1009
+ - Searches across shadow boundaries using `upShadowSearch` (registry-aware)
1010
+ - Respects scoped custom element registry boundaries
1011
+ - Cleans up cache entries when elements are garbage collected
1012
+
1013
+ **MOSE Export Optimization:**
1014
+
1015
+ When cloning live DOM elements (not templates) that contain Mount Observer Script Elements (MOSEs) across shadow DOM boundaries, the HTMLInclude handler automatically copies the parsed `export` property from source scripts to cloned scripts. This optimization avoids re-parsing JSON when the same MOSE configuration is reused in multiple shadow roots.
1016
+
1017
+ **How it works:**
1018
+ 1. Detects when cloning a live element (not a template) from a different root node
1019
+ 2. Finds all `script[type="mountobserver"]` elements in both source and clone
1020
+ 3. Matches scripts by their `id` attribute
1021
+ 4. Copies the `export` property from source to clone (by reference)
1022
+ 5. If source script hasn't been processed yet, waits for the `resolved` event
1023
+
1024
+ **Example:**
1025
+
1026
+ ```html
1027
+ <!-- Source element with MOSE in light DOM -->
1028
+ <div id="observer-config">
1029
+ <script type="mountobserver" id="my-config">
1030
+ {
1031
+ "matching": ".interactive",
1032
+ "import": "./interactive.js",
1033
+ "do": "builtIns.enhanceMountedElement"
1034
+ }
1035
+ </script>
1036
+ <div class="interactive">Content</div>
1037
+ </div>
1038
+
1039
+ <!-- Clone into shadow DOM -->
1040
+ <my-component>
1041
+ #shadow
1042
+ <template src="#observer-config"></template>
1043
+ </my-component>
1044
+
1045
+ <script type="module">
1046
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1047
+
1048
+ // Process MOSEs in light DOM
1049
+ new MountObserver({
1050
+ do: 'builtIns.mountObserverScript'
1051
+ }).observe(document.body);
1052
+
1053
+ // Clone into shadow roots
1054
+ new MountObserver({
1055
+ do: 'builtIns.HTMLInclude'
1056
+ }).observe(document);
1057
+ </script>
1058
+ ```
1059
+
1060
+ **Benefits:**
1061
+ - **Performance**: JSON is parsed only once, not for each clone
1062
+ - **Memory efficiency**: Cloned scripts share the same export object
1063
+ - **Consistency**: All clones use identical configuration
1064
+ - **Automatic**: No manual intervention required
1065
+
1066
+ **Requirements:**
1067
+ - Source and clone must be in different root nodes (document vs shadow root)
1068
+ - MOSE scripts must have `id` attributes for matching
1069
+ - Source script must be processed by `builtIns.mountObserverScript` or `builtIns.scriptExport` before cloning
1070
+
1071
+ [Implemented as MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes requirement](requirements/Done/MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes.md)
1072
+
1073
+ ## Automatic ID Generation with genIds
1074
+
1075
+ 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.
1076
+
1077
+ **Why use automatic ID generation?**
1078
+
1079
+ - Eliminates manual ID management and conflicts
1080
+ - Supports scoped ID generation within fieldsets or itemscope containers
1081
+ - Automatically updates ID references in attributes (aria-labelledby, for, etc.)
1082
+ - Provides shorthand syntax for common patterns
1083
+ - Handles deferred attribute activation
1084
+ - Removes `disabled` from fieldsets after processing
1085
+
1086
+ **Basic usage:**
1087
+
1088
+ ```html
1089
+ <fieldset disabled>
1090
+ <label>
1091
+ LHS: <input data-id={{lhs}}>
1092
+ </label>
1093
+
1094
+ <label for=rhs>
1095
+ RHS: <input data-id={{rhs}}>
1096
+ </label>
1097
+
1098
+ <template -id defer-🎚️ 🎚️='on if isEqual, based on #{{lhs}} and #{{rhs}}.'>
1099
+ <div>LHS === RHS</div>
1100
+ </template>
1101
+ </fieldset>
1102
+
1103
+ <script type="module">
1104
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1105
+
1106
+ const observer = new MountObserver({
1107
+ do: 'builtIns.generateIds'
1108
+ });
1109
+ observer.observe(document);
1110
+ </script>
1111
+ ```
1112
+
1113
+ **What happens:**
1114
+
1115
+ 1. The handler watches for elements with the `-id` attribute (the trigger)
1116
+ 2. Finds the nearest scope container (fieldset, [itemscope], or root)
1117
+ 3. Generates unique IDs for elements with `data-id={{name}}`, `#`, `@`, or `|` attributes
1118
+ 4. Replaces `#{{name}}` references with generated IDs in attributes
1119
+ 5. Removes `-id` and `defer-*` attributes after processing
1120
+ 6. Removes `disabled` from fieldset containers
1121
+
1122
+ **Result:**
1123
+
1124
+ ```html
1125
+ <fieldset>
1126
+ <label>
1127
+ LHS: <input id=gid-0 data-id=lhs>
1128
+ </label>
1129
+
1130
+ <label for=rhs>
1131
+ RHS: <input id=gid-1 data-id=rhs>
1132
+ </label>
1133
+
1134
+ <template 🎚️='on if isEqual, based on #gid-0 and #gid-1.'>
1135
+ <div>LHS === RHS</div>
1136
+ </template>
1137
+ </fieldset>
1138
+ ```
1139
+
1140
+ **Shorthand attributes:**
1141
+
1142
+ ```html
1143
+ <fieldset disabled>
1144
+ <!-- # uses element's tag name -->
1145
+ <my-element #></my-element> <!-- becomes id=gid-0 data-id=my-element -->
1146
+
1147
+ <!-- @ uses element's name attribute -->
1148
+ <input @ name="email" type="email"> <!-- becomes id=gid-1 data-id=email -->
1149
+
1150
+ <!-- | uses element's itemprop attribute -->
1151
+ <span | itemprop="price">$99</span> <!-- becomes id=gid-2 data-id=price -->
1152
+
1153
+ <button -id>Generate IDs</button>
1154
+ </fieldset>
1155
+ ```
1156
+
1157
+ **Side effects with data-id:**
1158
+
1159
+ The `data-id` attribute supports special symbols that trigger side effects:
1160
+
1161
+ ```html
1162
+ <form>
1163
+ <fieldset disabled>
1164
+ <label>
1165
+ LHS: <input data-id="{{@. lhs}}">
1166
+ </label>
1167
+
1168
+ <label for=rhs>
1169
+ RHS: <span contenteditable data-id="{{|.% rhs}}">
1170
+ </label>
1171
+
1172
+ <template -id defer-🎚️ 🎚️='on if isEqual, based on #{{lhs}} and #{{rhs}}.'>
1173
+ <div>LHS === RHS</div>
1174
+ </template>
1175
+ </fieldset>
1176
+ </form>
1177
+ ```
1178
+
1179
+ **Result:**
1180
+
1181
+ ```html
1182
+ <form>
1183
+ <fieldset>
1184
+ <label>
1185
+ LHS: <input name=lhs class=lhs id=gid-0 data-id=lhs>
1186
+ </label>
1187
+
1188
+ <label for=rhs>
1189
+ RHS: <span contenteditable itemprop=rhs class=rhs part=rhs id=gid-1 data-id=rhs>
1190
+ </label>
1191
+
1192
+ <template 🎚️='on if isEqual, based on #gid-0 and #gid-1.'>
1193
+ <div>LHS === RHS</div>
1194
+ </template>
1195
+ </fieldset>
1196
+ </form>
1197
+ ```
1198
+
1199
+ **Symbol meanings:**
1200
+
1201
+ | Symbol | Attribute | Meaning |
1202
+ |--------|-------------------|------------------------------------------------------------------------------|
1203
+ | @ | name | Second letter of name, common in social media for selecting names |
1204
+ | \| | itemprop | "Pipe" resembles itemprop, half of dollar sign, looks like an I |
1205
+ | $ | itemscope+itemprop| Combination of S for Scope and Pipe |
1206
+ | % | part | Starts with p, percent indicates proportion |
1207
+ | . | class | CSS selector |
1208
+
1209
+ Multiple symbols can be combined: `data-id="{{@.% myName}}"` adds name, class, and part attributes.
1210
+
1211
+ **Deferred attributes:**
1212
+
1213
+ Use `defer-*` prefix to prevent attributes from being applied until IDs are generated:
1214
+
1215
+ ```html
1216
+ <fieldset disabled>
1217
+ <!-- These attributes won't work until IDs are generated -->
1218
+ <label defer-for="for: #{{email}}">Email:</label>
1219
+ <input data-id={{email}} type="email">
1220
+
1221
+ <button -id>Activate Form</button>
1222
+ </fieldset>
1223
+ ```
1224
+
1225
+ **Supported reference attributes:**
1226
+
1227
+ The handler automatically replaces `#{{name}}` references in these attributes:
1228
+ - ARIA: `aria-labelledby`, `aria-describedby`, `aria-controls`, `aria-owns`, `aria-flowto`, `aria-activedescendant`
1229
+ - Form: `for`, `form`, `list`
1230
+ - Microdata: `itemref`
1231
+ - Any `data-*` attribute
1232
+ - Any attribute with a `defer-*` prefix
1233
+
1234
+ **Declarative usage with MOSE:**
1235
+
1236
+ ```html
1237
+ <script type="mountobserver">
1238
+ {
1239
+ "do": "builtIns.generateIds"
1240
+ }
1241
+ </script>
1242
+
1243
+ <script type="module">
1244
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1245
+
1246
+ new MountObserver({
1247
+ do: 'builtIns.mountObserverScript'
1248
+ }).observe(document);
1249
+ </script>
1250
+ ```
1251
+
1252
+ **Scope containers:**
1253
+
1254
+ The handler looks for the nearest scope container using `.closest()`:
1255
+ - `<fieldset>` elements
1256
+ - Elements with `[itemscope]` attribute
1257
+ - Falls back to the root node if no scope is found
1258
+
1259
+ **Global counter:**
1260
+
1261
+ 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.
1262
+
1263
+
1264
+ # Thorough Exposition Begins Here
1265
+
1266
+ Okay, let's get into the weeds. First, we strongly recommend studying the core package that mount-observer extends, [assign-gingerly](https://www.npmjs.com/package/assign-gingerly).
1267
+
1268
+ ## First use case -- lazy loading custom elements without sugar coating
1269
+
1270
+ This registers the custom element in the global registry.
1271
+
1272
+ ```JavaScript
1273
+ const observer = new MountObserver({
1274
+ select:'my-element', //not supported by this polyfill
1275
+ import: './my-element.js',
1276
+ do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
1277
+ if(!customElements.get(localName)) {
1278
+ customElements.define(localName, modules[0].MyElement);
1279
+ }
1280
+ observer.disconnectedSignal.abort();
1281
+ }
1282
+
1283
+ }, {disconnectedSignal: new AbortController().signal});
1284
+ observer.observe(document);
1285
+ ```
1286
+
1287
+ The do function will *only be called once per matching element* -- i.e. if the element stops matching the "select" criteria, then matches again, the do function won't be called again. It will be called for all elements when they match within the scope passed in to the observe method. However, the events discussed below, will continue to be called repeatedly.
1288
+
1289
+ The constructor argument can also be an array of objects that fit the pattern shown above.
1290
+
1291
+ In fact, as we will see, where it makes sense, where we see examples that are strings, we will also allow for arrays of such strings. For example, the "select" key can point to an array of CSS selectors (and in this case the mount/dismount callbacks would need to provide an index of which one matched). I only recommend adding this complexity if what I suspect is true -- providing this support can reduce "context switching" between threads / memory spaces (c++ vs JavaScript), and thus improve performance. If multiple "on" selectors are provided, and multiple ones match, I think it makes sense to indicate the one with the highest specifier that matches. It would probably be helpful in this case to provide a special event that allows for knowing when the matching selector with the highest specificity changes for mounted elements.
1292
+
1293
+ If no imports are specified, it would go straight to do (if any such callbacks are specified), and it will also dispatch events as discussed below.
1294
+
1295
+ This only searches for elements matching 'my-element' outside any shadow DOM.
1296
+
1297
+ But the observe method can accept a node within the document, or a shadowRoot, or a node inside a shadowRoot as well.
1298
+
1299
+ The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
1300
+
1301
+ > [!Note]
1302
+ > Reading through the historical links tied to the selector-observer proposal this proposal helped spawn, I may have painted an overly optimistic picture of [what the platform is capable of](https://github.com/whatwg/dom/issues/398). It does leave me a little puzzled why this isn't an issue when it comes to styling, and also if some of the advances that were utilized to support :has could be applied to this problem space, so that maybe the arguments raised there have weakened. Even if the concerns raised are as relevant today, I think considering the use cases this proposal envisions, that the objections could be overcome, for the following reasons: 1. For scenarios where lazy loading is the primary objective, "bunching" multiple DOM mutations together and only reevaluating when things are quite idle is perfectly reasonable. Also, for binding from a distance, most of the mutations that need responding to quickly will be when the *state of the host* changes, so DOM mutations play a somewhat muted role in that regard. Again, bunching multiple DOM mutations together, even if adds a bit of a delay, also seems reasonable. I also think the platform could add an "analysis" step to look at the query and categorize it as "simple" queries vs complex. Selector queries that are driven by the characteristics of the element itself (localName, attributes, etc) could be handled in a more expedited fashion. Those that the platform does expect to require more babysitting could be monitored for less vigilantly. Maybe in the latter case, a console.warning could be emitted during initialization. The other use case, for lazy loading custom elements and custom enhancements based on attributes, I think most of the time this would fit the "simple" scenario, so again there wouldn't be much of an issue.
1303
+
1304
+ In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but have to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable in some circumstances.
1305
+
1306
+ If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "select" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support, maybe even after the browser vendors provide a selector-observer (if ever).
1307
+
1308
+ So the developer could use:
1309
+
1310
+ ## Polyfill Supported Mount Observer
1311
+
1312
+ ```JavaScript
1313
+ const observer = new MountObserver({
1314
+ //supported by this polyfill
1315
+ matching:'my-element',
1316
+ import: './my-element.js',
1317
+ do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
1318
+ if(!customElements.get(localName)) {
1319
+ customElements.define(localName, modules[0].MyElement);
1320
+ }
1321
+ observer.disconnectedSignal.abort();
1322
+ }
1323
+
1324
+ }, {disconnectedSignal: new AbortController().signal});
1325
+ observer.observe(document);
1326
+ ```
1327
+
1328
+ and could perhaps expect faster binding as a result of the more limited supported expressions. Since "select" is not specified, it is assumed to be "*".
1329
+
1330
+ This polyfill in fact only supports this latter option ("matching"), and leaves "select" for such a time as when a selector observer is available in the platform.
1331
+
1332
+ [Implemented as Requirement 1](requirements/Done/Requirement1.md).
1333
+
1334
+ ## The observe() method
1335
+
1336
+ The `observe()` method begins observation of elements within the provided node:
1337
+
1338
+ ```typescript
1339
+ async observe(observedNode: Node): Promise<void>
1340
+ ```
1341
+
1342
+ **Parameter: `observedNode`**
1343
+
1344
+ 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.
1345
+
1346
+ **Common usage:**
1347
+ ```javascript
1348
+ const observer = new MountObserver({
1349
+ matching: '.my-element',
1350
+ do: (el) => console.log('Mounted:', el)
1351
+ });
1352
+
1353
+ // Observe the entire document
1354
+ await observer.observe(document);
1355
+
1356
+ // Or observe a specific subtree
1357
+ const container = document.querySelector('#container');
1358
+ await observer.observe(container);
1359
+
1360
+ // Or observe within a shadow DOM
1361
+ const shadowRoot = element.shadowRoot;
1362
+ await observer.observe(shadowRoot);
1363
+ ```
1364
+
1365
+ **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.
1366
+
1367
+ **Relationship with element.mount():**
1368
+
1369
+ When using the `element.mount()` convenience method, it internally determines which node to pass to `observe()` based on the `scope` option:
1370
+ - `'self'` - Observes the element itself
1371
+ - `'registryRoot'` - Finds and observes the element's registry root
1372
+ - `'registry'` - Finds and observes all DOM nodes that have the same custom element registry
1373
+ - `'shadow'` - Observes the element's shadow root
1374
+ - `'root'` - Observes the element's root node (via `getRootNode()`)
1375
+
1376
+ ## The import key
1377
+
1378
+ This proposal has been amended to support multiple imports, including of different types:
1379
+
1380
+ ```JavaScript
1381
+ const observer = new MountObserver({
1382
+ matching:'my-element',
1383
+ import: [
1384
+ ['./my-element-small.css', {type: 'css'}],
1385
+ './my-element.js',
1386
+ ],
1387
+ do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
1388
+ ...
1389
+ }
1390
+ });
1391
+ observer.observe(document);
1392
+ ```
1393
+
1394
+ Once again, the key can accept either a single import, but alternatively it can also support multiple imports (via an array).
1395
+
1396
+ The do function won't be invoked until all the imports have been successfully completed and inserted into the modules array.
1397
+
1398
+ 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.
1399
+
1400
+ This proposal would also include support for JSON and HTML module imports (really, all types).
1401
+
1402
+ [Implemented as Requirement 1](requirements/Done/Requirement1.md).
1403
+
1404
+ ## Preemptive downloading
1405
+
1406
+ There are two significant steps to imports, each of which imposes a cost:
1407
+
1408
+ 1. Downloading the resource.
1409
+ 2. Loading the resource into memory.
1410
+
1411
+ What if we want to *download* the resource ahead of time, but only load into memory when needed?
1412
+
1413
+ 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.
1414
+
1415
+ So for this we add loadingEagerness:
1416
+
1417
+ ```JavaScript
1418
+ const observer = new MountObserver({
1419
+ select: 'my-element', //not supported by this polyfill
1420
+ loadingEagerness: 'eager', //partially supported by this polyfill
1421
+ import: './my-element.js',
1422
+ do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
1423
+ });
1424
+ ```
1425
+
1426
+ 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.
1427
+
1428
+ The polyfill just loads the module into memory right away.
1429
+
1430
+ > [!NOTE]
1431
+ > 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.
1432
+
1433
+ ## Importing Configuration with configFrom
1434
+
1435
+ The `configFrom` property provides a clean way to import MountConfig settings from external modules, enabling better code organization and reusability.
1436
+
1437
+ **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.
1438
+
1439
+ ```JavaScript
1440
+ // Inline config - 100% JSON serializable
1441
+ const observer = new MountObserver({
1442
+ matching: '.my-element',
1443
+ configFrom: './my-handlers.js' // Non-serializable code lives here
1444
+ });
1445
+
1446
+ // my-handlers.js - Contains functions and class references
1447
+ export const mountConfig = {
1448
+ whereInstanceOf: HTMLButtonElement, // Class constructor
1449
+ do: (element, context) => { // Function
1450
+ element.addEventListener('click', () => console.log('clicked'));
1451
+ }
1452
+ };
1453
+ ```
1454
+
1455
+ 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.
1456
+
1457
+ ### Basic Usage
1458
+
1459
+ Create a configuration module that exports a `mountConfig` constant:
1460
+
1461
+ ```JavaScript
1462
+ // my-config.js
1463
+ export const mountConfig = {
1464
+ matching: '.my-element',
1465
+ do: (element, context) => {
1466
+ element.textContent = 'Configured!';
1467
+ }
1468
+ };
1469
+ ```
1470
+
1471
+ Then reference it in your observer:
1472
+
1473
+ ```JavaScript
1474
+ const observer = new MountObserver({
1475
+ configFrom: './my-config.js'
1476
+ });
1477
+ observer.observe(document);
1478
+ ```
1479
+
1480
+ ### Multiple Configuration Modules
1481
+
1482
+ You can import multiple config modules. Later configs override earlier ones (left-to-right merge):
1483
+
1484
+ ```JavaScript
1485
+ const observer = new MountObserver({
1486
+ configFrom: ['./base-config.js', './override-config.js']
1487
+ });
1488
+ ```
1489
+
1490
+ ### Inline Config Takes Precedence
1491
+
1492
+ Inline configuration always overrides imported configuration:
1493
+
1494
+ ```JavaScript
1495
+ const observer = new MountObserver({
1496
+ configFrom: './base-config.js',
1497
+ matching: '.custom-selector' // Overrides matching from base-config.js
1498
+ });
1499
+ ```
1500
+
1501
+ ### Merge Semantics
1502
+
1503
+ - **Shallow merge**: Uses `Object.assign()` for merging
1504
+ - **Merge order**: First configFrom module → second configFrom module → ... → inline config
1505
+ - **Arrays are replaced**: If multiple configs define the same array property, the later array completely replaces the earlier one
1506
+ - **Inline wins**: Inline configuration always takes final precedence
1507
+
1508
+ ### Supported Properties
1509
+
1510
+ Config modules can export any valid MountConfig property, including:
1511
+ - `matching`, `whereInstanceOf`, `withMediaMatching`
1512
+ - `whereObservedRootSizeMatches`, `whereElementIntersectsWith`
1513
+ - `whereConnectionHas`, `withScopePerimeter`
1514
+ - `import`, `do`, `loadingEagerness`
1515
+ - `assignOnMount`, `assignOnDismount`, `stageOnMount`
1516
+ - `mountedElemEmits`, `customData`, `getPlayByPlay`
1517
+
1518
+ ### Functions and Class References
1519
+
1520
+ Config modules can include non-JSON-serializable values like functions and class constructors:
1521
+
1522
+ ```JavaScript
1523
+ // button-config.js
1524
+ export const mountConfig = {
1525
+ matching: 'button',
1526
+ whereInstanceOf: HTMLButtonElement,
1527
+ do: (element, context) => {
1528
+ element.addEventListener('click', () => {
1529
+ console.log('Button clicked!');
1530
+ });
1531
+ }
1532
+ };
1533
+ ```
1534
+
1535
+ ### Error Handling
1536
+
1537
+ **Missing mountConfig export:**
1538
+ ```JavaScript
1539
+ // This will throw an error
1540
+ const observer = new MountObserver({
1541
+ configFrom: './module-without-mountConfig.js'
1542
+ });
1543
+ // Error: Module './module-without-mountConfig.js' does not export 'mountConfig'
1544
+ ```
1545
+
1546
+ **Duplicate modules:**
1547
+ ```JavaScript
1548
+ // This will throw an error
1549
+ const observer = new MountObserver({
1550
+ configFrom: ['./config.js', './config.js']
1551
+ });
1552
+ // Error: Duplicate configFrom module: './config.js'
1553
+ ```
1554
+
1555
+ ### Circular Dependency Warning
1556
+
1557
+ Be careful to avoid circular dependencies when using `configFrom`. Config modules should only export configuration and avoid importing modules that create MountObserver instances.
1558
+
1559
+ **Safe pattern:**
1560
+ ```JavaScript
1561
+ // config.js - Only exports configuration
1562
+ export const mountConfig = {
1563
+ matching: '.element',
1564
+ do: (el) => { /* ... */ }
1565
+ };
1566
+ ```
1567
+
1568
+ **Avoid:**
1569
+ ```JavaScript
1570
+ // config.js - Creates circular dependency
1571
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1572
+ // This could cause issues if the importing module also imports MountObserver
1573
+ ```
1574
+
1575
+ ## Media / container queries / instanceOf
1576
+
1577
+ 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?):
1578
+
1579
+ ```JavaScript
1580
+ const observer = new MountObserver({
1581
+ // not supported by polyfill
1582
+ select: 'div > p + p ~ span[class$="name"]',
1583
+ withMediaMatching: '(max-width: 1250px)',
1584
+ whereObservedRootSizeMatches: '(min-width: 700px)',
1585
+ whereElementIntersectsWith:{
1586
+ rootMargin: "0px",
1587
+ threshold: 1.0,
1588
+ },
1589
+ whereInstanceOf: [HTMLMarqueeElement], //or 'HTMLMarqueeElement'
1590
+ whereLangIn: ['en-GB'], // Cannot be implemented - see https://github.com/whatwg/html/issues/7039
1591
+ whereConnectionHas:{
1592
+ effectiveTypeIn: ["slow-2g"],
1593
+ },
1594
+ import: ['./my-element-small.css', {type: 'css'}],
1595
+ do: function(mountedElement, ctx){
1596
+ console.log({mountedElement, ctx});
1597
+ }
1598
+ });
1599
+ ```
1600
+
1601
+ [whereInstanceOf implemented as [Requirement5](requirements/Done/Requirement5.md)]
1602
+ [whereObservedRootSizeMatches implemented]
1603
+ [whereElementIntersectsWith implemented]
1604
+ [whereConnectionHas implemented]
1605
+ [whereLocalNameMatches implemented as [RegularExpressionNameMatching](requirements/Done/RegularExpressionNameMatching.md)]
256
1606
 
257
- If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "select" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support, maybe even after the browser vendors provide a selector-observer (if ever).
1607
+ [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
258
1608
 
259
- So the developer could use:
1609
+ ## Waiting for Custom Element Definitions
260
1610
 
261
- ## Polyfill Supported Mount Observer
1611
+ The `whenDefined` property allows you to wait for custom elements to be defined before mounting elements. This ensures that elements are only processed after their custom element definitions are available, preventing issues with undefined custom elements.
262
1612
 
263
- ```JavaScript
1613
+ ```javascript
264
1614
  const observer = new MountObserver({
265
- //supported by this polyfill
266
- matching:'my-element',
267
- import: './my-element.js',
268
- do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
269
- if(!customElements.get(localName)) {
270
- customElements.define(localName, modules[0].MyElement);
271
- }
272
- observer.disconnectedSignal.abort();
273
- }
274
-
275
- }, {disconnectedSignal: new AbortController().signal});
1615
+ matching: 'my-element',
1616
+ whenDefined: 'my-element', // Wait for my-element to be defined
1617
+ do: (element) => {
1618
+ console.log('my-element is now defined and mounted');
1619
+ }
1620
+ });
276
1621
  observer.observe(document);
277
- ```
278
-
279
- and could perhaps expect faster binding as a result of the more limited supported expressions. Since "select" is not specified, it is assumed to be "*".
280
-
281
- This polyfill in fact only supports this latter option ("matching"), and leaves "select" for such a time as when a selector observer is available in the platform.
282
-
283
- [Implemented as Requirement 1](requirements/Done/Requirement1.md).
284
1622
 
1623
+ // Later, when the custom element is defined
1624
+ customElements.define('my-element', class extends HTMLElement {
1625
+ // ...
1626
+ });
1627
+ ```
285
1628
 
286
- ## The import key
1629
+ **Waiting for multiple custom elements:**
287
1630
 
288
- This proposal has been amended to support multiple imports, including of different types:
1631
+ You can specify an array of tag names to wait for all of them to be defined:
289
1632
 
290
- ```JavaScript
1633
+ ```javascript
291
1634
  const observer = new MountObserver({
292
- matching:'my-element',
293
- import: [
294
- ['./my-element-small.css', {type: 'css'}],
295
- './my-element.js',
296
- ],
297
- do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
298
- ...
299
- }
1635
+ matching: 'my-element, another-element',
1636
+ whenDefined: ['my-element', 'another-element'], // Wait for both
1637
+ do: (element) => {
1638
+ console.log('Both elements are defined, mounting:', element.localName);
1639
+ }
300
1640
  });
301
- observer.observe(document);
302
1641
  ```
303
1642
 
304
- Once again, the key can accept either a single import, but alternatively it can also support multiple imports (via an array).
1643
+ **How it works:**
305
1644
 
306
- The do function won't be invoked until all the imports have been successfully completed and inserted into the modules array.
1645
+ 1. The check happens first, before any other `where*` conditions
1646
+ 2. Uses `customElements.whenDefined()` for each specified tag name
1647
+ 3. Uses the `customElementRegistry` of the observed root node
1648
+ 4. Only checks once per observer instance (doesn't re-check on subsequent mounts)
1649
+ 5. The `observe()` method waits for all definitions before processing elements
307
1650
 
308
- 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.
1651
+ **Common use cases:**
309
1652
 
310
- This proposal would also include support for JSON and HTML module imports (really, all types).
1653
+ ```javascript
1654
+ // Lazy load and wait for custom element definition
1655
+ const observer = new MountObserver({
1656
+ matching: 'my-button',
1657
+ whenDefined: 'my-button',
1658
+ import: './my-button.js',
1659
+ do: 'builtIns.defineCustomElement'
1660
+ });
311
1661
 
312
- [Implemented as Requirement 1](requirements/Done/Requirement1.md).
1662
+ // Wait for dependencies before enhancing
1663
+ const observer = new MountObserver({
1664
+ matching: '.needs-custom-elements',
1665
+ whenDefined: ['base-element', 'helper-element'],
1666
+ do: (element) => {
1667
+ // Safe to interact with custom elements now
1668
+ const base = element.querySelector('base-element');
1669
+ const helper = element.querySelector('helper-element');
1670
+ }
1671
+ });
1672
+ ```
313
1673
 
314
- ## Preemptive downloading
1674
+ **AND condition logic:**
315
1675
 
316
- There are two significant steps to imports, each of which imposes a cost:
1676
+ Like all `where*` properties, `whenDefined` forms an AND condition with other filters. However, it's checked first as a prerequisite before evaluating other conditions:
317
1677
 
318
- 1. Downloading the resource.
319
- 2. Loading the resource into memory.
1678
+ ```javascript
1679
+ const observer = new MountObserver({
1680
+ matching: 'my-element',
1681
+ whenDefined: 'my-element', // Checked FIRST
1682
+ whereInstanceOf: HTMLElement, // Then checked
1683
+ whereLocalNameMatches: /^my-/, // Then checked
1684
+ do: (element) => { /* ... */ }
1685
+ });
1686
+ ```
320
1687
 
321
- What if we want to *download* the resource ahead of time, but only load into memory when needed?
1688
+ [whenDefined implemented as [SupportForWhenDefined](requirements/SupportForWhenDefined.md)]
322
1689
 
323
- 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.
1690
+ ## LocalName Pattern Matching
324
1691
 
325
- So for this we add loadingEagerness:
1692
+ 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.
326
1693
 
327
- ```JavaScript
1694
+ ```javascript
328
1695
  const observer = new MountObserver({
329
- select: 'my-element', //not supported by this polyfill
330
- loadingEagerness: 'eager', //partially supported by this polyfill
331
- import: './my-element.js',
332
- do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
1696
+ matching: '*', // Match all elements
1697
+ whereLocalNameMatches: /^my-/, // Only mount elements starting with 'my-'
1698
+ do: (element) => {
1699
+ console.log('Mounted:', element.localName);
1700
+ }
333
1701
  });
1702
+ observer.observe(document);
334
1703
  ```
335
1704
 
336
- 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.
1705
+ **String patterns are automatically converted to RegExp:**
337
1706
 
338
- The polyfill just loads the module into memory right away.
1707
+ ```javascript
1708
+ // These are equivalent
1709
+ whereLocalNameMatches: 'button|input'
1710
+ whereLocalNameMatches: /button|input/
1711
+ ```
339
1712
 
340
- > [!NOTE]
341
- > 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.
1713
+ **Common use cases:**
342
1714
 
343
- ## Separating JS imperative code from JSON serializable config
1715
+ ```javascript
1716
+ // Match custom elements with a specific prefix
1717
+ whereLocalNameMatches: /^app-/
344
1718
 
1719
+ // Match elements ending with a suffix
1720
+ whereLocalNameMatches: /-widget$/
345
1721
 
346
- In order to support pure 100% declarative syntax in the passed in MountConfig argument, we need to be able to import the do function. This is done as follows:
1722
+ // Match multiple element types
1723
+ whereLocalNameMatches: /^(button|input|select)$/
347
1724
 
348
- ```JavaScript
349
- //module myActions.js
350
- const doFunction = function({localName}, {modules, observer, MountConfig, rootNode}){
351
- if(!customElements.get(localName)) {
352
- // Find the first exported class constructor from the module
353
- const ElementClass = Object.values(modules[0]).find(exp =>
354
- typeof exp === 'function' && exp.prototype && exp.prototype.constructor === exp
355
- );
356
- if(ElementClass) {
357
- customElements.define(localName, ElementClass);
358
- }
359
- }
360
- observer.disconnectedSignal.abort();
361
- }
362
- export {doFunction as do}
1725
+ // Match elements containing a pattern
1726
+ whereLocalNameMatches: /dialog/
1727
+ ```
1728
+
1729
+ **AND condition logic:**
363
1730
 
364
- // observer setup
1731
+ Like all `where*` properties, `whereLocalNameMatches` forms an AND condition with other filters:
365
1732
 
1733
+ ```javascript
366
1734
  const observer = new MountObserver({
367
- matching:'my-element',
368
- import: [
369
- './my-element.js',
370
- ['./my-element-small.css', {type: 'css'}],
371
- './myActions.js'
372
- ],
373
- reference: 2
1735
+ matching: '[data-enhanced]', // Must have data-enhanced attribute
1736
+ whereLocalNameMatches: /^custom-/, // AND localName starts with 'custom-'
1737
+ whereInstanceOf: HTMLElement, // AND is an HTMLElement instance
1738
+ do: (element) => { /* ... */ }
374
1739
  });
375
- observer.observe(document);
376
-
377
1740
  ```
378
1741
 
379
- Here "2" refers to the imported module index ('./myActions.js' in this case).
1742
+ This will only mount elements that satisfy ALL three conditions.
380
1743
 
381
- ### How the reference property works
1744
+ ## Custom JavaScript Checks with shouldMount
382
1745
 
383
- The `reference` property allows us to call `do` functions from imported modules, enabling 100% JSON-serializable configuration. This is useful when you want to separate imperative code from declarative configuration.
1746
+ 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.
384
1747
 
385
- **Key behaviors:**
386
- - The `reference` property can be a single number or an array of numbers, each referring to an import index
387
- - Referenced modules must be JavaScript modules (not CSS, JSON, or HTML imports)
388
- - If a referenced module exports a `do` function, it will be called after the inline `do` callback (if present)
389
- - If a referenced module doesn't export a `do` function, it's silently skipped
390
- - The inline `do` callback runs first, then referenced `do` functions run in the order specified
1748
+ 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.
391
1749
 
392
- **Important:** Since `do` is a reserved keyword in JavaScript, you must export it using the syntax:
393
1750
  ```javascript
394
- const doFunction = function(element, context) { /* ... */ };
395
- export { doFunction as do };
1751
+ const observer = new MountObserver({
1752
+ matching: '.protected-feature',
1753
+ shouldMount: (el, ctx) => {
1754
+ // Check user permission
1755
+ const requiredRole = el.dataset.requiredRole;
1756
+ return currentUser.hasRole(requiredRole);
1757
+ },
1758
+ do: (el) => {
1759
+ // Only called if shouldMount returned true
1760
+ enhanceProtectedFeature(el);
1761
+ }
1762
+ });
396
1763
  ```
397
1764
 
398
- **Validation:** The `reference` property is validated in the constructor:
399
- - Throws an error if `import` is not defined
400
- - Throws an error if any index is out of bounds
401
- - Throws an error if any index points to a non-JS module (e.g., CSS or JSON import)
402
-
403
- Multiple references can also be made.
404
-
405
- So for example:
1765
+ **Behavior:**
1766
+ - `shouldMount` is called after ALL `where*` conditions pass
1767
+ - If it returns `true`, the element mounts (do callback + mount event)
1768
+ - If it returns `false`, the element does NOT mount (no do callback, no mount event)
1769
+ - If it throws an error, it's treated as `false` and the error is logged
1770
+ - The element can be re-evaluated if removed and re-added to the DOM
406
1771
 
407
- ```JavaScript
1772
+ **Use Cases:**
408
1773
 
409
- import: [
410
- ['./my-element-small.css', {type: 'css'}],
411
- './component.js',
412
- './actions1.js',
413
- './actions2.js'
414
- ],
415
- reference: [2, 3] // Both actions1 and actions2 will have their 'do' called if present
1774
+ Authorization checks:
1775
+ ```javascript
1776
+ shouldMount: (el) => currentUser.hasPermission(el.dataset.permission)
416
1777
  ```
417
1778
 
418
- [Implemented as [Requirement11](requirements/Done/Requirement11.md)]
419
-
420
- ## Media / container queries / instanceOf / custom checks [TODO] out of date
1779
+ Feature flags:
1780
+ ```javascript
1781
+ shouldMount: (el) => featureFlags.isEnabled(el.dataset.feature)
1782
+ ```
421
1783
 
422
- 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?):
1784
+ Data validation:
1785
+ ```javascript
1786
+ shouldMount: (el) => {
1787
+ return el.dataset.apiKey &&
1788
+ el.dataset.apiEndpoint &&
1789
+ el.dataset.apiKey.length > 0;
1790
+ }
1791
+ ```
423
1792
 
424
- ```JavaScript
425
- const observer = new MountObserver({
426
- select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
427
- withMediaMatching: '(max-width: 1250px)',
428
- whereObservedRootSizeMatches: '(min-width: 700px)',
429
- whereElementIntersectsWith:{
430
- rootMargin: "0px",
431
- threshold: 1.0,
432
- },
433
- whereInstanceOf: [HTMLMarqueeElement], //or 'HTMLMarqueeElement'
434
- whereLangIn: ['en-GB'], // Cannot be implemented - see https://github.com/whatwg/html/issues/7039
435
- whereConnectionHas:{
436
- effectiveTypeIn: ["slow-2g"],
437
- },
438
- import: ['./my-element-small.css', {type: 'css'}],
439
- do: ...
440
- });
1793
+ Complex conditional logic:
1794
+ ```javascript
1795
+ shouldMount: (el, ctx) => {
1796
+ const parent = el.closest('[data-context]');
1797
+ if (!parent) return false;
1798
+
1799
+ const isActive = parent.dataset.context === 'active';
1800
+ const widgetType = el.dataset.widgetType;
1801
+ const enabledWidgets = parent.dataset.enabledWidgets?.split(',') || [];
1802
+
1803
+ return isActive && enabledWidgets.includes(widgetType);
1804
+ }
441
1805
  ```
442
1806
 
443
- [whereInstanceOf implemented as [Requirement5](requirements/Done/Requirement5.md)]
444
- [whereObservedRootSizeMatches implemented]
445
- [whereElementIntersectsWith implemented]
446
- [whereConnectionHas implemented]
1807
+ **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.
447
1808
 
448
- [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
1809
+ [Implemented as SupportForShouldMount requirement](requirements/Done/SupportForShouldMount.md)
449
1810
 
450
1811
  ## InstanceOf checks in detail
451
1812
 
452
- 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 "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.
1813
+ 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.
453
1814
 
454
1815
  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.
455
1816
 
@@ -462,52 +1823,69 @@ However, where this support for "whereInstanceOf" would be *most* helpful is whe
462
1823
 
463
1824
  -->
464
1825
 
465
- ### Referenced whereInstanceOf
1826
+ ## Custom Element Registry Matching
466
1827
 
467
- Similar to the `do` function, the `whereInstanceOf` check can also be moved to imported modules for 100% JSON-serializable configuration:
1828
+ 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.
1829
+
1830
+ Learn more about [scoped custom element registries in Chrome's blog post](https://developer.chrome.com/blog/scoped-registries).
1831
+
1832
+ **How it works:**
468
1833
 
469
1834
  ```javascript
470
- // module mySettings.js
471
- const doFunction = function({localName}, {modules, observer, MountConfig, rootNode}) {
472
- if(!customElements.get(localName)) {
473
- customElements.define(localName, modules[1].MyElement);
474
- }
475
- observer.disconnectedSignal.abort();
476
- };
1835
+ // Observe document - only mounts elements in the global registry
1836
+ const observer1 = new MountObserver({
1837
+ matching: '.my-element',
1838
+ do: (el) => { /* ... */ }
1839
+ });
1840
+ observer1.observe(document);
1841
+
1842
+ // Observe shadow root - only mounts elements in that shadow root's registry
1843
+ const shadowRoot = host.attachShadow({ mode: 'open' });
1844
+ const observer2 = new MountObserver({
1845
+ matching: '.my-element',
1846
+ do: (el) => { /* ... */ }
1847
+ });
1848
+ observer2.observe(shadowRoot);
1849
+ ```
1850
+
1851
+ **Registry matching logic:**
1852
+
1853
+ The implementation is straightforward - it compares the `customElementRegistry` property of the root node with the `customElementRegistry` property of each candidate element:
1854
+
1855
+ ```javascript
1856
+ const registriesMatch = rootNode.customElementRegistry === element.customElementRegistry;
1857
+ ```
477
1858
 
478
- const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
1859
+ **Default behavior** (same registry):
1860
+ - Elements with matching registries → mount ✓
1861
+ - Elements with different registries → don't mount ✓
479
1862
 
480
- export { doFunction as do, whereInstanceOf };
1863
+ **Inverted behavior** with `whereDifferentCustomElementRegistry: true`:
1864
+ - Elements with matching registries → don't mount ✓
1865
+ - Elements with different registries → mount ✓
481
1866
 
482
- // my local module
1867
+ **Use case for inverted matching:**
1868
+ ```javascript
1869
+ // Mount elements from OTHER registries (cross-registry observation)
483
1870
  const observer = new MountObserver({
484
- matching: 'my-element',
485
- import: [
486
- ['./my-element-small.css', {type: 'css'}],
487
- './my-element.js',
488
- './mySettings.js'
489
- ],
490
- reference: 2
1871
+ matching: '.external-component',
1872
+ whereDifferentCustomElementRegistry: true,
1873
+ do: (el) => { /* Handle elements from different registries */ }
491
1874
  });
492
- observer.observe(document);
1875
+ observer.observe(shadowRoot);
493
1876
  ```
494
1877
 
495
- **Behavior:**
496
- - **Combining checks**: If both inline `whereInstanceOf` and referenced `whereInstanceOf` exist, they are AND'd together (element must match both)
497
- - **Multiple references**: If multiple referenced modules export `whereInstanceOf`, the element must match ALL of them (AND logic)
498
- - **Validation**: Referenced `whereInstanceOf` is validated after imports load. Throws an error if not a Constructor or array of Constructors
499
- - **Optional export**: If a referenced module doesn't export `whereInstanceOf`, it's silently ignored
500
- - **Timing**:
501
- - With lazy loading (default): Inline `whereInstanceOf` is checked first (before imports), then referenced checks happen after imports load
502
- - With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
1878
+ **Behavior across browser versions:**
1879
+ - **Pre-Chrome 146**: Both `customElementRegistry` properties are `undefined`, so `undefined === undefined` is `true` and elements match (backward compatible)
1880
+ - **Chrome 146+ with scoped registries**: Elements are filtered by registry reference equality
503
1881
 
504
- This optimization ensures that with lazy loading, elements that don't match the inline `whereInstanceOf` won't trigger unnecessary imports.
1882
+ 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.
505
1883
 
506
- [Implemented as [Requirement12](requirements/Done/Requirement12.md)]
1884
+ [Implemented as [ExcludeMatchingElementsWhereCustomElementRegistriesDon'tMatch](requirements/ExcludeMatchingElementsWhereCustomElementRegistriesDon'tMatch.md)]
507
1885
 
508
1886
  ## Element Mount Extension
509
1887
 
510
- For even more convenience, you 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).
1888
+ 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](https://developer.chrome.com/blog/scoped-registries) (Chrome 146+, latest WebKit/Safari).
511
1889
 
512
1890
  ```JavaScript
513
1891
  import 'mount-observer/ElementMountExtension.js';
@@ -532,7 +1910,7 @@ Scope options (via `options.scope`):
532
1910
  - `'self'`: Observes only this element
533
1911
  - `'root'`: Observes the root node (document or shadow root)
534
1912
  - `'shadow'`: Observes the element's shadowRoot (throws error if none exists)
535
- - `Element`: Observes a custom element you specify
1913
+ - `Element`: Observes a custom element we specify
536
1914
 
537
1915
  This is especially useful for web components that want to observe their own shadow DOM or scoped registry:
538
1916
 
@@ -564,17 +1942,186 @@ class MyComponent extends HTMLElement {
564
1942
  }
565
1943
  ```
566
1944
 
567
- Browser support: Works in all browsers, but scoped registry features require Chrome 146+ or latest WebKit/Safari.
1945
+ Browser support: Works in all browsers, but [scoped registry features](https://developer.chrome.com/blog/scoped-registries) require Chrome 146+ or latest WebKit/Safari.
568
1946
 
569
1947
  [Implemented as CustomElementRegistryMounting requirement](requirements/Done/CustomElementRegistryMounting.md).
570
-
571
1948
 
1949
+ ### Global Propagation with `mountGlobally()`
1950
+
1951
+ The `mountGlobally()` method extends `mount()` to automatically propagate mount observers across [custom element registry](https://developer.chrome.com/blog/scoped-registries) 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.
1952
+
1953
+ ```JavaScript
1954
+ import 'mount-observer/ElementMountExtension.js';
1955
+
1956
+ // Mount globally - propagates to all child registries and shadow roots
1957
+ await document.mountGlobally({
1958
+ matching: '.target',
1959
+ do: (el) => {
1960
+ el.setAttribute('data-mounted', 'true');
1961
+ }
1962
+ });
1963
+ ```
1964
+
1965
+ The `mountGlobally()` method:
1966
+ - Mounts the config in the current registry first (using `mount()`)
1967
+ - Creates two propagators that automatically mount in:
1968
+ - Elements with different custom element registries (`whereDifferentCustomElementRegistry: true`)
1969
+ - Shadow roots within the same registry (custom elements with shadow DOM)
1970
+ - Waits for custom elements to be defined before mounting (ensures shadow roots exist)
1971
+ - Recursively propagates through nested shadow roots
1972
+
1973
+ This enables "viral" propagation of mount observers, perfect for bootstrapping core handlers like `builtIns.mountObserverScript`:
1974
+
1975
+ ```JavaScript
1976
+ // Bootstrap mount observer script support globally
1977
+ await document.mountGlobally({
1978
+ do: 'builtIns.mountObserverScript'
1979
+ });
1980
+
1981
+ // Now MOSE scripts work everywhere, even in scoped registries
1982
+ ```
1983
+
1984
+ Both `Element.prototype.mountGlobally()` and `ShadowRoot.prototype.mountGlobally()` are available.
1985
+
1986
+ [Implemented as goViral requirement](requirements/Done/goViral.md).
1987
+
1988
+ ## Hierarchical Observer Composition with the `with` Property
1989
+
1990
+ 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.
1991
+
1992
+ ### Basic Usage
1993
+
1994
+ ```JavaScript
1995
+ const observer = new MountObserver({
1996
+ matching: '.container',
1997
+ with: {
1998
+ // Sub-observer for custom elements
1999
+ registry: {
2000
+ matching: 'my-element',
2001
+ import: './my-element.js',
2002
+ do: 'builtIns.defineCustomElement'
2003
+ },
2004
+ // Sub-observer for styles
2005
+ styles: {
2006
+ matching: '.styled',
2007
+ import: './styles.css'
2008
+ }
2009
+ }
2010
+ });
2011
+
2012
+ await observer.observe(document);
2013
+ ```
2014
+
2015
+ ### How It Works
2016
+
2017
+ 1. **Automatic Creation**: When the parent observer's `observe()` method is called, it automatically creates sub-observers for each entry in the `with` property.
2018
+
2019
+ 2. **Same Root Node**: All sub-observers observe the same root node as the parent.
2020
+
2021
+ 3. **Independent Configuration**: Each sub-observer operates independently with its own configuration. Sub-observers do NOT inherit properties from the parent.
2022
+
2023
+ 4. **Automatic Lifecycle**: Sub-observers are automatically disconnected when the parent disconnects.
2024
+
2025
+ 5. **Unlimited Nesting**: Sub-observers can have their own `with` property for unlimited nesting depth.
2026
+
2027
+ ### Accessing Sub-Observers in Handlers
2028
+
2029
+ Sub-observers are accessible in mount handlers via the `context.withObservers` property:
2030
+
2031
+ ```JavaScript
2032
+ const observer = new MountObserver({
2033
+ matching: '.parent',
2034
+ with: {
2035
+ registry: { matching: 'custom-element' },
2036
+ styles: { matching: '.styled' }
2037
+ },
2038
+ do: (el, ctx) => {
2039
+ // Access sub-observers with type safety
2040
+ const registryObserver = ctx.withObservers?.registry;
2041
+ const stylesObserver = ctx.withObservers?.styles;
2042
+
2043
+ if (registryObserver) {
2044
+ console.log('Registry observer:', registryObserver);
2045
+ console.log('Mounted elements:', registryObserver.mountedElements);
2046
+ }
2047
+ }
2048
+ });
2049
+ ```
2050
+
2051
+ ### Nested Sub-Observers
2052
+
2053
+ Sub-observers can have their own sub-observers, creating a tree structure:
2054
+
2055
+ ```JavaScript
2056
+ const observer = new MountObserver({
2057
+ matching: '.root',
2058
+ with: {
2059
+ level1: {
2060
+ matching: '.level1',
2061
+ with: {
2062
+ level2: {
2063
+ matching: '.level2',
2064
+ do: (el) => console.log('Level 2 mounted:', el)
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2069
+ });
2070
+ ```
2071
+
2072
+ ### Use Case: Cross-Scope Registry Management
2073
+
2074
+ A practical use case is managing custom elements across different scoped registries:
2075
+
2076
+ ```JavaScript
2077
+ const observer = new MountObserver({
2078
+ matching: 'div[shadowroot]',
2079
+ with: {
2080
+ // Observe elements in the main registry
2081
+ mainRegistry: {
2082
+ matching: 'my-element',
2083
+ whereDifferentCustomElementRegistry: false,
2084
+ do: 'builtIns.defineCustomElement'
2085
+ },
2086
+ // Observe elements in shadow DOM registries
2087
+ shadowRegistry: {
2088
+ matching: 'shadow-element',
2089
+ whereDifferentCustomElementRegistry: true,
2090
+ do: 'builtIns.defineScopedCustomElement'
2091
+ }
2092
+ }
2093
+ });
2094
+ ```
2095
+
2096
+ ### Type Safety
572
2097
 
2098
+ When using TypeScript, the keys in the `with` property are inferred and provide autocomplete:
573
2099
 
2100
+ ```TypeScript
2101
+ const observer = new MountObserver({
2102
+ matching: '.parent',
2103
+ with: {
2104
+ registry: { matching: 'my-element' },
2105
+ styles: { import: './styles.css' }
2106
+ },
2107
+ do: (el, ctx) => {
2108
+ ctx.withObservers?.registry // ✓ TypeScript knows this exists
2109
+ ctx.withObservers?.unknown // ✗ TypeScript error
2110
+ }
2111
+ });
2112
+ ```
574
2113
 
2114
+ ### Key Benefits
575
2115
 
2116
+ 1. **Declarative Composition**: Define complex observer hierarchies in a single configuration
2117
+ 2. **Automatic Lifecycle**: Sub-observers are created and cleaned up automatically
2118
+ 3. **Independent Operation**: Each sub-observer has its own configuration and state
2119
+ 4. **Type Safety**: Full TypeScript support with key inference
2120
+ 5. **Unlimited Nesting**: Create arbitrarily deep observer hierarchies
576
2121
 
2122
+ ### Known Limitations
577
2123
 
2124
+ - **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.
578
2125
 
579
2126
  ## Mount Observer Script Elements (MOSEs)
580
2127
 
@@ -582,14 +2129,15 @@ Following an approach similar to the [speculation api](https://developer.chrome.
582
2129
 
583
2130
  ```JavaScript
584
2131
  // myPackage/myDefiner.js
585
- //my all powerful custom element definer
586
- const doFunction = function({localNme}, {modules, observer}){
587
- if(!customElements.get(localName)) {
588
- customElements.define(localName, modules[1].MyElement);
2132
+ // My all powerful custom element definer
2133
+ export const mountConfig = {
2134
+ do: function({localName}, {modules, observer}) {
2135
+ if(!customElements.get(localName)) {
2136
+ customElements.define(localName, modules[1].MyElement);
2137
+ }
2138
+ observer.disconnectedSignal.abort();
589
2139
  }
590
- observer.disconnectedSignal.abort();
591
- }
592
- export { doFunction as do };
2140
+ };
593
2141
  ```
594
2142
 
595
2143
  ```html
@@ -597,18 +2145,14 @@ export { doFunction as do };
597
2145
  {
598
2146
  "select":"my-element",
599
2147
  "import": [
600
- ["./my-element-small.css", {type: "css"}],
601
- "./my-element.js",
602
- "myPackage/myDefiner.js
2148
+ ["./my-element-small.css", {"type": "css"}],
2149
+ "./my-element.js"
603
2150
  ],
604
- "reference": 2
2151
+ "configFrom": "myPackage/myDefiner.js"
605
2152
  }
606
2153
  </script>
607
2154
  ```
608
2155
 
609
- 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, including support for inheritance across containing scoped custom element registries.
610
-
611
- 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.
612
2156
 
613
2157
 
614
2158
  ## Binding from a distance
@@ -648,7 +2192,7 @@ This allows developers to create "stylesheet" like capabilities.
648
2192
 
649
2193
  ## Registering reusable handlers with MountObserver.define
650
2194
 
651
- To make MountConfig configurations more JSON-serializable and encourage code reuse, you can register handler classes with string names and reference them by name:
2195
+ To make MountConfig configurations more JSON-serializable and encourage code reuse, we can register handler classes with string names and reference them by name:
652
2196
 
653
2197
  ```JavaScript
654
2198
  import {EvtRt} from 'mount-observer/EvtRt.js';
@@ -701,25 +2245,6 @@ const observer = new MountObserver({
701
2245
 
702
2246
  Handlers execute in the order specified. If a handler constructor throws an error, execution stops and subsequent handlers won't run.
703
2247
 
704
- ### Interaction with the reference property
705
-
706
- When both `do` (with string/array) and `reference` are specified, the execution order is:
707
-
708
- 1. Inline `do` functions and registered handlers (from `do` strings), in whatever order they appear
709
- 2. Referenced `do` functions (from `reference` property)
710
-
711
- ```JavaScript
712
- MountObserver.define('setup', SetupHandler);
713
-
714
- const observer = new MountObserver({
715
- matching: 'button',
716
- import: './button-actions.js',
717
- reference: 0,
718
- do: ['setup', (el) => { el.dataset.ready = 'true'; }]
719
- });
720
- // Execution order: setup handler, inline function, then imported do function
721
- ```
722
-
723
2248
  ### Handler requirements
724
2249
 
725
2250
  Registered handlers must be classes (constructors) that accept `(mountedElement: Element, ctx: MountContext)` as constructor parameters. They can be:
@@ -758,13 +2283,13 @@ MountObserver.define('myHandler', Handler2); // Error: myHandler already in use
758
2283
 
759
2284
  ### Global registry
760
2285
 
761
- 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.
2286
+ 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.
762
2287
 
763
2288
  [Implemented as [Requirement14](requirements/Done/Requirement14.md)]
764
2289
 
765
2290
  ### Handler defaults with static properties
766
2291
 
767
- Registered handler classes can specify default MountConfig properties using static class properties. When you reference a handler by name, its static properties are automatically merged with your inline configuration, with inline config always taking precedence:
2292
+ 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:
768
2293
 
769
2294
  ```JavaScript
770
2295
  import {EvtRt} from 'mount-observer/EvtRt.js';
@@ -869,7 +2394,7 @@ export default class MyElement extends HTMLElement {
869
2394
  }
870
2395
 
871
2396
  // main.js
872
- import { MountObserver } from 'mount-observer';
2397
+ import { MountObserver } from 'mount-observer/MountObserver.js';
873
2398
 
874
2399
  const observer = new MountObserver({
875
2400
  matching: 'my-element',
@@ -1019,7 +2544,7 @@ Using `assignOn*` provides several benefits:
1019
2544
 
1020
2545
  ### Dynamically updating assignGingerly configuration
1021
2546
 
1022
- The `MountObserver` class provides a public `assignGingerly()` method that allows you to merge new updates into the observer. This is useful for responding to user actions or application state changes:
2547
+ 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:
1023
2548
 
1024
2549
  ```JavaScript
1025
2550
  const observer = new MountObserver({
@@ -1046,7 +2571,7 @@ await observer.assignGingerly({
1046
2571
 
1047
2572
  3. **Applies to future elements**: Future elements that mount will receive the merged configuration.
1048
2573
 
1049
- 4. **Starting without initial config**: You can call the method even if no `assignGingerly` was specified in the constructor:
2574
+ 4. **Starting without initial config**: We can call the method even if no `assignGingerly` was specified in the constructor:
1050
2575
 
1051
2576
  ```JavaScript
1052
2577
  const observer = new MountObserver({
@@ -1080,7 +2605,7 @@ The method is async because the assign-gingerly library is loaded dynamically wh
1080
2605
 
1081
2606
  ## Reversible property assignment with stageOnMount
1082
2607
 
1083
- While `assignOnMount` and `assignOnDismount` provide permanent property assignments, sometimes you need temporary changes that automatically reverse when elements dismount. The `stageOnMount` property provides this capability using the `assignTentatively` function from assign-gingerly:
2608
+ 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:
1084
2609
 
1085
2610
  ```JavaScript
1086
2611
  const observer = new MountObserver({
@@ -1104,7 +2629,7 @@ When a matching button mounts, these properties are applied. When it dismounts (
1104
2629
  2. **Applies the new properties** when elements mount
1105
2630
  3. **Automatically reverses** to original values when elements dismount
1106
2631
 
1107
- This is different from `assignOnMount`/`assignOnDismount`, where you must explicitly specify both the mount and dismount values.
2632
+ This is different from `assignOnMount`/`assignOnDismount`, where we must explicitly specify both the mount and dismount values.
1108
2633
 
1109
2634
  ### When to use stageOnMount vs assignOnMount
1110
2635
 
@@ -1228,7 +2753,7 @@ const observer = new MountObserver({
1228
2753
  observer.observe(document);
1229
2754
  ```
1230
2755
 
1231
- This dispatches a `custom-ready` event from each matching button element when it mounts. Events bubble by default, so you can listen at the document level:
2756
+ 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:
1232
2757
 
1233
2758
  ```JavaScript
1234
2759
  document.addEventListener('custom-ready', (e) => {
@@ -1396,7 +2921,7 @@ observer.observe(document);
1396
2921
 
1397
2922
  ## Element-specific lifecycle notifications with getNotifier
1398
2923
 
1399
- While the MountObserver dispatches lifecycle events (mount, dismount, disconnect) at the observer level, sometimes you need to listen for events specific to a single element. The `getNotifier()` method returns an EventTarget that dispatches filtered events for only that element.
2924
+ 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.
1400
2925
 
1401
2926
  ### Basic usage
1402
2927
 
@@ -1494,7 +3019,7 @@ getNotifier(element: Element): EventTarget
1494
3019
  [Implemented as [Requirement13](requirements/Done/Requirement13.md)]
1495
3020
 
1496
3021
 
1497
- ## Extra lazy loading
3022
+ <!-- ## Extra lazy loading
1498
3023
 
1499
3024
  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.
1500
3025
 
@@ -1509,7 +3034,7 @@ const observer = new MountObserver({
1509
3034
  },
1510
3035
  import: './my-element.js'
1511
3036
  });
1512
- ```
3037
+ ``` -->
1513
3038
 
1514
3039
 
1515
3040
 
@@ -1518,7 +3043,8 @@ const observer = new MountObserver({
1518
3043
  Subscribing can be done via:
1519
3044
 
1520
3045
  ```JavaScript
1521
- observer.addEventListener('confirm', e => {
3046
+ //[TODO] not implemented yet
3047
+ observer.addEventListener('shouldMount', e => {
1522
3048
  e.isSatisfied = true; //or false to prevent the mount event below
1523
3049
  });
1524
3050
  observer.addEventListener('mount', e => {
@@ -1533,18 +3059,23 @@ observer.addEventListener('dismount', e => {
1533
3059
  observer.addEventListener('disconnect', e => {
1534
3060
  ...
1535
3061
  });
3062
+ //[TODO]
1536
3063
  observer.addEventListener('move', e => {
1537
3064
  ...
1538
3065
  });
3066
+ //[TODO]
1539
3067
  observer.addEventListener('reconnect', e => {
1540
3068
  ...
1541
3069
  });
3070
+ //[TODO]
1542
3071
  observer.addEventListener('reconfirm', e => {
1543
3072
  ...
1544
3073
  });
3074
+ //[TODO]
1545
3075
  observer.addEventListener('exit', e => {
1546
3076
  ...
1547
3077
  });
3078
+ //[TODO]
1548
3079
  observer.addEventListener('forget', e => {
1549
3080
  ...
1550
3081
  });