mount-observer 0.1.1 → 0.1.3

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
@@ -5,6 +5,45 @@
5
5
 
6
6
  Note that much of what is described below has not yet been polyfilled.
7
7
 
8
+ ## Implementation Status
9
+
10
+ The following features have been implemented and tested:
11
+
12
+ ### Core Functionality
13
+ - ✅ **whereElementMatches**: CSS selector-based element matching
14
+ - ✅ **whereAttr**: Complex attribute-based matching with:
15
+ - Built-in vs custom element distinction
16
+ - Attribute prefix variations (data-, enh-, data-enh-)
17
+ - Hierarchical attribute branches with customizable delimiters
18
+ - Coordinate system for attribute mapping
19
+ - ✅ **whereInstanceOf**: Constructor-based element filtering (single or array)
20
+ - ✅ **whereMediaMatches**: Media query-based conditional mounting (string or MediaQueryList)
21
+ - ✅ **whereOutside**: Donut hole scoping (exclude elements inside matching ancestors)
22
+
23
+ ### Lifecycle & Events
24
+ - ✅ **mount/dismount/disconnect events**: Element lifecycle tracking
25
+ - ✅ **attrchange event**: Attribute change notifications with batching
26
+ - ✅ **mediamatch/mediaunmatch events**: Media query state change notifications (with `getPlayByPlay` option)
27
+ - ✅ **load event**: Import completion notification
28
+
29
+ ### Advanced Features
30
+ - ✅ **Dynamic imports**: Lazy loading of JavaScript modules
31
+ - ✅ **assignOnMount**: Property assignment when elements mount
32
+ - ✅ **assignOnDismount**: Property assignment when elements dismount
33
+ - ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
34
+ - ✅ **map configuration**: Metadata mapping for attribute coordinates
35
+ - ✅ **once option**: Fire attrchange event only once per attribute
36
+ - ✅ **Shared MutationObserver**: Efficient observer sharing across instances
37
+ - ✅ **Code splitting**: Conditional features loaded on-demand
38
+ - ✅ **Memory management**: WeakRef usage for DOM node references
39
+
40
+ ### Not Yet Implemented
41
+ - ❌ Intersection observer integration
42
+ - ❌ Container query support
43
+ - ❌ Shadow DOM traversal utilities
44
+ - ❌ Reconnect event handling
45
+ - ❌ Multiple import types (CSS, JSON, HTML)
46
+
8
47
  # The MountObserver api.
9
48
 
10
49
  Author: Bruce B. Anderson (with valuable feedback from @doeixd )
@@ -70,9 +109,9 @@ To specify the equivalent of what the alternative proposal linked to above would
70
109
 
71
110
  ```JavaScript
72
111
  const observer = new MountObserver({
73
- select:'my-element',
112
+ select:'my-element', //not supported by this polyfill
74
113
  import: './my-element.js',
75
- do: ({localName}, {modules, observer, observeInfo}) => {
114
+ do: ({localName}, {modules, observer, mountInit, rootNode}) => {
76
115
  if(!customElements.get(localName)) {
77
116
  customElements.define(localName, modules[0].MyElement);
78
117
  }
@@ -83,7 +122,7 @@ const observer = new MountObserver({
83
122
  observer.observe(document);
84
123
  ```
85
124
 
86
- 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, as well as more structured inline functions also as discussed below, will continue to be called repeatedly.
125
+ 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.
87
126
 
88
127
  The constructor argument can also be an array of objects that fit the pattern shown above.
89
128
 
@@ -100,19 +139,20 @@ The "observer" constant above is a class instance that inherits from EventTarget
100
139
  > [!Note]
101
140
  > 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.
102
141
 
103
- 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 has to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable.
142
+ 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 has to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable in some circumstances.
104
143
 
105
- 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 "on" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support.
144
+ 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).
106
145
 
107
146
  So the developer could use:
108
147
 
109
- ## Polyfill Supported Scenario I
148
+ ## Polyfill Supported Mount Observer
110
149
 
111
150
  ```JavaScript
112
151
  const observer = new MountObserver({
113
- import: './my-element.js',
152
+ //supported by this polyfill
114
153
  whereElementMatches:'my-element',
115
- do: ({localName}, {modules, observer, observeInfo}) => {
154
+ import: './my-element.js',
155
+ do: ({localName}, {modules, observer, mountInit, rootNode}) => {
116
156
  if(!customElements.get(localName)) {
117
157
  customElements.define(localName, modules[0].MyElement);
118
158
  }
@@ -123,9 +163,11 @@ const observer = new MountObserver({
123
163
  observer.observe(document);
124
164
  ```
125
165
 
126
- 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 "*"
166
+ 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 "*".
167
+
168
+ This polyfill in fact only supports this latter option ("whereElementMatches"), and leaves "select" for such a time as when a selector observer is available in the platform.
127
169
 
128
- This polyfill in fact only supports this latter option ("whreElementMatches"), and leaves "select" for such a time as when a selector observer is available in the platform.
170
+ [Implemented as Requirement 1](requirements/Requirement1.md).
129
171
 
130
172
  ## The import key
131
173
 
@@ -133,12 +175,12 @@ This proposal has been amended to support multiple imports, including of differe
133
175
 
134
176
  ```JavaScript
135
177
  const observer = new MountObserver({
136
- select:'my-element',
178
+ whereElementMatches:'my-element',
137
179
  import: [
138
180
  ['./my-element-small.css', {type: 'css'}],
139
181
  './my-element.js',
140
182
  ],
141
- do: ({localName}, {modules, observer}) => {
183
+ do: ({localName}, {modules, observer, mountInit, rootNode}) => {
142
184
  if(!customElements.get(localName)) {
143
185
  customElements.define(localName, modules[1].MyElement);
144
186
  }
@@ -154,7 +196,9 @@ The do function won't be invoked until all the imports have been successfully co
154
196
 
155
197
  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.
156
198
 
157
- This proposal would also include support for JSON and HTML module imports (really, all types).
199
+ This proposal would also include support for JSON and HTML module imports (really, all types).
200
+
201
+ [Implemented as Requirement 1](requirements/Requirement1.md).
158
202
 
159
203
  ## Preemptive downloading
160
204
 
@@ -171,7 +215,7 @@ So for this we add loadingEagerness:
171
215
 
172
216
  ```JavaScript
173
217
  const observer = new MountObserver({
174
- select: 'my-element',
218
+ select: 'my-element', //not supported by this polyfill
175
219
  loadingEagerness: 'eager',
176
220
  import: './my-element.js',
177
221
  do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
@@ -183,130 +227,368 @@ So what this does is only check for the presence of an element with tag name "my
183
227
  > [!NOTE]
184
228
  > 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.
185
229
 
230
+ ## Separating JS imperative code from JSON serializable config
231
+
232
+
233
+
234
+ In order to support pure 100% declarative syntax in the passed in mountInit argument, we need to be able to import the do function. This is done as follows:
235
+
236
+ ```JavaScript
237
+ //module myActions.js
238
+ const doFunction = function({localName}, {modules, observer, mountInit, rootNode}){
239
+ if(!customElements.get(localName)) {
240
+ // Find the first exported class constructor from the module
241
+ const ElementClass = Object.values(modules[0]).find(exp =>
242
+ typeof exp === 'function' && exp.prototype && exp.prototype.constructor === exp
243
+ );
244
+ if(ElementClass) {
245
+ customElements.define(localName, ElementClass);
246
+ }
247
+ }
248
+ observer.disconnectedSignal.abort();
249
+ }
250
+ export {doFunction as do}
251
+
252
+ // observer setup
253
+
254
+ const observer = new MountObserver({
255
+ whereElementMatches:'my-element',
256
+ import: [
257
+ './my-element.js',
258
+ ['./my-element-small.css', {type: 'css'}],
259
+ './myActions.js'
260
+ ],
261
+ reference: 2
262
+ });
263
+ observer.observe(document);
264
+
265
+ ```
266
+
267
+ Here "2" refers to the imported module index ('./myActions.js' in this case).
268
+
269
+ ### How the reference property works
270
+
271
+ The `reference` property allows you 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.
272
+
273
+ **Key behaviors:**
274
+ - The `reference` property can be a single number or an array of numbers, each referring to an import index
275
+ - Referenced modules must be JavaScript modules (not CSS, JSON, or HTML imports)
276
+ - If a referenced module exports a `do` function, it will be called after the inline `do` callback (if present)
277
+ - If a referenced module doesn't export a `do` function, it's silently skipped
278
+ - The inline `do` callback runs first, then referenced `do` functions run in the order specified
279
+
280
+ **Important:** Since `do` is a reserved keyword in JavaScript, you must export it using the syntax:
281
+ ```javascript
282
+ const doFunction = function(element, context) { /* ... */ };
283
+ export { doFunction as do };
284
+ ```
285
+
286
+ **Validation:** The `reference` property is validated in the constructor:
287
+ - Throws an error if `import` is not defined
288
+ - Throws an error if any index is out of bounds
289
+ - Throws an error if any index points to a non-JS module (e.g., CSS or JSON import)
290
+
291
+ Multiple references can also be made.
292
+
293
+ So for example:
294
+
295
+ ```JavaScript
296
+
297
+ import: [
298
+ ['./my-element-small.css', {type: 'css'}],
299
+ './component.js',
300
+ './actions1.js',
301
+ './actions2.js'
302
+ ],
303
+ reference: [2, 3] // Both actions1 and actions2 will have their 'do' called if present
304
+ ```
305
+
306
+ [Implemented as [Requirement11](requirements/Requirement11.md)]
307
+
308
+ ### Referenced whereInstanceOf
309
+
310
+ Similar to the `do` function, the `whereInstanceOf` check can also be moved to imported modules for 100% JSON-serializable configuration:
311
+
312
+ ```javascript
313
+ // module mySettings.js
314
+ const doFunction = function({localName}, {modules, observer, mountInit, rootNode}) {
315
+ if(!customElements.get(localName)) {
316
+ customElements.define(localName, modules[1].MyElement);
317
+ }
318
+ observer.disconnectedSignal.abort();
319
+ };
320
+
321
+ const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
322
+
323
+ export { doFunction as do, whereInstanceOf };
324
+
325
+ // my local module
326
+ const observer = new MountObserver({
327
+ whereElementMatches: 'my-element',
328
+ import: [
329
+ ['./my-element-small.css', {type: 'css'}],
330
+ './my-element.js',
331
+ './mySettings.js'
332
+ ],
333
+ reference: 2
334
+ });
335
+ observer.observe(document);
336
+ ```
337
+
338
+ **Behavior:**
339
+ - **Combining checks**: If both inline `whereInstanceOf` and referenced `whereInstanceOf` exist, they are AND'd together (element must match both)
340
+ - **Multiple references**: If multiple referenced modules export `whereInstanceOf`, the element must match ALL of them (AND logic)
341
+ - **Validation**: Referenced `whereInstanceOf` is validated after imports load. Throws an error if not a Constructor or array of Constructors
342
+ - **Optional export**: If a referenced module doesn't export `whereInstanceOf`, it's silently ignored
343
+ - **Timing**:
344
+ - With lazy loading (default): Inline `whereInstanceOf` is checked first (before imports), then referenced checks happen after imports load
345
+ - With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
346
+
347
+ This optimization ensures that with lazy loading, elements that don't match the inline `whereInstanceOf` won't trigger unnecessary imports.
348
+
349
+ [Implemented as [Requirement12](requirements/Requirement12.md)]
350
+
186
351
 
187
352
 
188
353
  ## Mount Observer Script Elements (MOSEs)
189
354
 
190
355
  Following an approach similar to the [speculation api](https://developer.chrome.com/blog/speculation-rules-improvements), we can add a script element anywhere in the DOM:
191
356
 
192
- ```html
193
- <script type="mountobserver" onload="{...}" onmount="{
194
- const {matchingElement} = event;
195
- const {localName} = matchingElement;
357
+ ```JavaScript
358
+ // myPackage/myDefiner.js
359
+ //my all powerful custom element definer
360
+ const doFunction = function({localNme}, {modules, observer}){
196
361
  if(!customElements.get(localName)) {
197
362
  customElements.define(localName, modules[1].MyElement);
198
363
  }
199
364
  observer.disconnectedSignal.abort();
200
- }">
365
+ }
366
+ export { doFunction as do };
367
+ ```
368
+
369
+ ```html
370
+ <script type="mountobserver" >
201
371
  {
202
372
  "select":"my-element",
203
373
  "import": [
204
374
  ["./my-element-small.css", {type: "css"}],
205
375
  "./my-element.js",
206
- ]
376
+ "myPackage/myDefiner.js
377
+ ],
378
+ "reference": 2
207
379
  }
208
380
  </script>
209
381
  ```
210
382
 
211
- The things that make this API work together, namely the "modules", "observer", and "mountedElements" (an array of an array of weak refs to elements that match all the criteria for the i<sup>th</sup> "on" selector) would be accessible as properties of the script element:
383
+ To keep this proposal / polyfill of reasonable size, mount observer script elements has its own [repo / sub-proposal](https://github.com/bahrus/mount-observer-script-element). There's much more to it, but it is awaiting implementation of scoped custom element registry before finalizing the requirements and (re)-implementing.
384
+
385
+ 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 (mountInit) is as JSON serializable as possible.
386
+
387
+
388
+ ## Binding from a distance
389
+
390
+ It is important to note that "whereElementMatches" is a css query with no restrictions. So something like:
212
391
 
213
392
  ```JavaScript
214
- const {modules, observer, mountedElements, mountInit} = myMountObserver;
393
+ import {EvtRt} from 'mount-observer/EvtRt.js';
394
+
395
+ class MyHandler extends EvtRt {
396
+ mount(mountedElement, mountInit, context){
397
+ mountedElement.textContent = 'hello';
398
+ }
399
+ dismount(mountedElement, mountInit){
400
+ mountedElement.textContent = 'goodbye';
401
+ }
402
+ }
403
+
404
+ const observer = new MountObserver({
405
+ // not supported by polyfill
406
+ //select: 'div > p + p ~ span[class$="name"]'
407
+ // is supported:
408
+ whereElementMatches: 'div > p + p ~ span[class$="name"]',
409
+ do: (mountedElement, ctx) => {
410
+ new MyHandler(mountedElement, ctx);
411
+ },
412
+ });
413
+ observer.observe(document);
215
414
  ```
216
415
 
217
- The "scope" of the observer would be the ShadowRoot containing the script element (or the document outside Shadow if placed outside any shadow DOM, like in the head element).
218
416
 
219
- Once again, arrays of settings could be supported, which, in practice, would greatly increase the ratio between declarative, JSON-parsable instructions that could be performed in low-level c++/rust threads, vs custom JavaScript in the example above. The events / callbacks would need to provide the index of which set of criteria was just fulfilled.
417
+ ... would work.
220
418
 
221
- > [!Note]
222
- > To support the event handlers above, I believe it would require that CSP solutions factor in both the inner content of the script element as well as all the event handlers via the string concatenation operator. I actually think such support is quite critical due to lack of support of import.meta.[some reference to the script element] not being available, as it was pre-ES Modules.
419
+ EvtRt is a convenience class provided with the polyfill package, and is considered part of this proposal (see how it is used below by built in handlers).
223
420
 
224
- ## Specific solution for lazy loading custom element definitions
421
+ This allows developers to create "stylesheet" like capabilities.
225
422
 
226
- Since the example we've been dwelling on so far (lazy custom element definition) seems like such a pressing, common requirement, and was in fact the originating impetus for this proposal, we can go a step further and make the example above 100% declarative, thus resulting in a less clunky interplay between JSON and custom script. This is meant as a way of illustrating how the platform could continue to extend this proposal going forward.
423
+ ## Registering reusable handlers with MountObserver.define
227
424
 
228
- The syntax below is just one, "spit-balling" way this could be done, as an example, and would require absorbing final heuristics from other custom element initiatives (such as declarative custom elements) when they get added to the platform.
425
+ To make MountInit configurations more JSON-serializable and encourage code reuse, you can register handler classes with string names and reference them by name:
229
426
 
230
- ```html
231
- <script type="mountobserver">
232
- {
233
- "select":"my-element",
234
- "import": [
235
- ["./my-element-small.css", {type: "css"}],
236
- "./my-element.js",
237
- ],
238
- "define": {
239
- "targetRegistry": "CustomElements",
240
- "targetScope": "global",
241
- "styleModules": [0],
242
- "classDefinition": {
243
- "module": 1,
244
- "exportSymbol": 'MyElement'
245
- }
427
+ ```JavaScript
428
+ import {EvtRt} from 'mount-observer/EvtRt.js';
429
+
430
+ class MyHandler extends EvtRt {
431
+ mount(mountedElement, mountInit, context){
432
+ mountedElement.textContent = 'hello';
433
+ }
434
+ dismount(mountedElement, mountInit){
435
+ mountedElement.textContent = 'bye';
246
436
  }
247
437
  }
248
- </script>
438
+
439
+ // Register the handler with a string name
440
+ MountObserver.define('myHandler', MyHandler);
441
+
442
+ // Reference it by name in the configuration
443
+ const observer = new MountObserver({
444
+ whereElementMatches: 'div > p + p ~ span[class$="name"]',
445
+ do: 'myHandler' // String reference instead of inline function
446
+ });
447
+ observer.observe(document);
249
448
  ```
250
449
 
450
+ ### Benefits of registered handlers
251
451
 
252
- ## Shadow Root inheritance
452
+ 1. **JSON serialization**: Configurations using string references can be serialized to JSON
453
+ 2. **Code reuse**: Define handlers once, use them in multiple observers
454
+ 3. **Separation of concerns**: Keep handler logic separate from configuration
253
455
 
254
- Inside a shadow root, we can plop a script element, also with type "mountobserver", optionally giving it the same id as above:
456
+ ### Using arrays with mixed types
255
457
 
256
- ```html
257
- #shadowRoot
258
- <script id=myMountObserver type=mountobserver>
259
- {
260
- "select":"your-element"
458
+ The `do` property can be a string, a function, or an array mixing both:
459
+
460
+ ```JavaScript
461
+ MountObserver.define('logger', LoggerHandler);
462
+ MountObserver.define('validator', ValidatorHandler);
463
+
464
+ const observer = new MountObserver({
465
+ whereElementMatches: 'input',
466
+ do: [
467
+ 'logger', // Registered handler
468
+ (element, ctx) => { // Inline function
469
+ element.dataset.processed = 'true';
470
+ },
471
+ 'validator' // Another registered handler
472
+ ]
473
+ });
474
+ ```
475
+
476
+ Handlers execute in the order specified. If a handler constructor throws an error, execution stops and subsequent handlers won't run.
477
+
478
+ ### Interaction with the reference property
479
+
480
+ When both `do` (with string/array) and `reference` are specified, the execution order is:
481
+
482
+ 1. Inline `do` functions and registered handlers (from `do` strings), in whatever order they appear
483
+ 2. Referenced `do` functions (from `reference` property)
484
+
485
+ ```JavaScript
486
+ MountObserver.define('setup', SetupHandler);
487
+
488
+ const observer = new MountObserver({
489
+ whereElementMatches: 'button',
490
+ import: './button-actions.js',
491
+ reference: 0,
492
+ do: ['setup', (el) => { el.dataset.ready = 'true'; }]
493
+ });
494
+ // Execution order: setup handler, inline function, then imported do function
495
+ ```
496
+
497
+ ### Handler requirements
498
+
499
+ Registered handlers must be classes (constructors) that accept `(mountedElement: Element, ctx: MountContext)` as constructor parameters. They can be:
500
+
501
+ - ES6 classes extending `EvtRt` (recommended)
502
+ - ES6 classes with custom logic
503
+ - ES5-style constructor functions
504
+
505
+ ```JavaScript
506
+ // ES5-style constructor function
507
+ function SimpleHandler(element, ctx) {
508
+ element.textContent = 'Handled!';
261
509
  }
262
- </script>
510
+
511
+ MountObserver.define('simple', SimpleHandler);
263
512
  ```
264
513
 
265
- If no id is found in the parent ShadowRoot (or in the parent window if the shadow root is at the top level), then this becomes a new set of rules to observe.
514
+ ### Error handling
266
515
 
267
- But if a matching id is found, then the values from the parent script element get merged in with the one in the child, with the child settings, including the event handling attributes.
516
+ **Validation at construction time**: If you reference an unregistered handler name, an error is thrown when creating the MountObserver:
268
517
 
269
- > [!Note]
270
- > The onload event is critical for a number of reasons, among them:
271
- > 1. We need a way to inject non JSON serializable settings (described below) when necessary, and
272
- > 2. We need a way to override settings in child Shadow DOM's programmatically in some cases.
518
+ ```JavaScript
519
+ const observer = new MountObserver({
520
+ do: 'nonexistent' // Error: No handler defined for nonexistent
521
+ });
522
+ ```
523
+
524
+ **Duplicate registration**: Attempting to register the same name twice throws an error:
525
+
526
+ ```JavaScript
527
+ MountObserver.define('myHandler', Handler1);
528
+ MountObserver.define('myHandler', Handler2); // Error: myHandler already in use
529
+ ```
273
530
 
274
- We will come back to some important [additional features](#creating-frameworks-that-revolve-around-moses) of using these script elements later, but first we want to cover the highlights of this proposal, in order to give more context as to what kinds of functionality these MOSEs can provide.
275
531
 
276
532
 
277
- ## Binding from a distance
533
+ ### Global registry
534
+
535
+ 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.
536
+
537
+ [Implemented as [Requirement14](requirements/Requirement14.md)]
538
+
539
+ ### Built in handlers
540
+
541
+ This proposal advocates having the platform provide some built in handlers, that extend EvtRt, that is included with this Polyfill.
278
542
 
279
- It is important to note that "select" is a css query with no restrictions. So something like:
543
+ #### Log to console handler
280
544
 
281
545
  ```JavaScript
282
546
  const observer = new MountObserver({
283
- select:'div > p + p ~ span[class$="name"]',
284
- do:{
285
- mount: (matchingElement) => {
286
- //attach some behavior or set some property value or add an event listener, etc.
287
- matchingElement.textContent = 'hello';
288
- },
289
- dismount: (matchingElement) => {
290
- matchingElement.textContent = 'bye';
291
- }
292
- }
293
- })
547
+ // not supported by polyfill
548
+ //select: 'div > p + p ~ span[class$="name"]'
549
+ // is supported:
550
+ whereElementMatches: 'div > p + p ~ span[class$="name"]',
551
+ do: 'builtIns.logToConsole'
552
+ });
553
+ observer.observe(document);
294
554
  ```
295
555
 
296
- ... would work.
556
+ This logs to console all the events (mount, dismount, disconnect)
557
+
558
+ ### Lazy custom element handler
559
+
560
+ ```JavaScript
561
+ // MyElement.js
562
+ export default class MyElement extends HTMLElement {
563
+ connectedCallback() {
564
+ this.textContent = 'Hello!';
565
+ }
566
+ }
567
+
568
+ // main.js
569
+ import { MountObserver } from 'mount-observer';
570
+
571
+ const observer = new MountObserver({
572
+ whereElementMatches: 'my-element',
573
+ import: './MyElement.js',
574
+ do: 'builtIns.defineCustomElement'
575
+ });
576
+ observer.observe(document);
297
577
 
298
- Note that in this example, "do" no longer points to a function. When it did (above), we mentioned this would only be called once per element. **Now it will be called every the conditions flip from not all satisfied to satisfied"**.
578
+ // HTML - elements will be upgraded when discovered
579
+ // by the mount observer
580
+ <my-element></my-element>
299
581
 
300
- This would allow developers to create "stylesheet" like capabilities.
582
+ ```
301
583
 
302
- ## Applying properties with assignGingerly
584
+ ## Applying properties on mount and dismount
303
585
 
304
- For the common use case of setting properties on matching elements, MountObserver provides built-in support for the [assignGingerly](https://github.com/bahrus/assign-gingerly) library. This allows you to declaratively specify properties to apply to elements without writing custom mount callbacks:
586
+ For the common use case of setting properties on matching elements, MountObserver provides built-in support for the [assignGingerly](https://github.com/bahrus/assign-gingerly) library. This allows us to declaratively specify properties to apply to elements during their lifecycle without writing custom mount callbacks:
305
587
 
306
588
  ```JavaScript
307
589
  const observer = new MountObserver({
308
590
  whereElementMatches: 'input',
309
- assignGingerly: {
591
+ assignOnMount: {
310
592
  disabled: true,
311
593
  value: 'Default value',
312
594
  title: 'This is a tooltip'
@@ -317,35 +599,100 @@ observer.observe(document);
317
599
 
318
600
  This will automatically apply the specified properties to all matching input elements, both existing ones and those added dynamically.
319
601
 
602
+ [Implemented as [Requirement2](requirements/Requirement2.md) and [Requirement16](requirements/Requirement16.md)]
603
+
604
+ ### Assigning properties on dismount
605
+
606
+ You can also specify properties to apply when elements are removed from the DOM using `assignOnDismount`:
607
+
608
+ ```JavaScript
609
+ const observer = new MountObserver({
610
+ whereElementMatches: '.status-indicator',
611
+ assignOnMount: {
612
+ '?.style?.color': 'green',
613
+ '?.dataset?.status': 'active'
614
+ },
615
+ assignOnDismount: {
616
+ '?.style?.color': 'red',
617
+ '?.dataset?.status': 'inactive'
618
+ }
619
+ });
620
+ observer.observe(document);
621
+ ```
622
+
623
+ This is useful for cleanup operations, visual feedback, or maintaining state on elements that may be temporarily removed from the DOM but still referenced elsewhere in your code.
624
+
625
+ **Note:** The `assignOnDismount` properties are applied before the element is removed from the mounted elements tracking, so the element still has access to its DOM context.
626
+
627
+ #### Practical use case: Form validation feedback
628
+
629
+ A common use case is providing visual feedback for form validation:
630
+
631
+ ```JavaScript
632
+ const observer = new MountObserver({
633
+ whereElementMatches: 'input.validated',
634
+ assignOnMount: {
635
+ '?.style?.borderColor': 'green',
636
+ '?.style?.backgroundColor': '#f0fff0',
637
+ '?.setAttribute': ['aria-invalid', 'false']
638
+ },
639
+ assignOnDismount: {
640
+ '?.style?.borderColor': '',
641
+ '?.style?.backgroundColor': '',
642
+ '?.removeAttribute': 'aria-invalid'
643
+ }
644
+ });
645
+ observer.observe(document);
646
+ ```
647
+
648
+ When an input gains the `validated` class, it gets green styling. When the class is removed (dismount), the styling is cleaned up.
649
+
650
+ #### Remounting behavior
651
+
652
+ If an element is removed and then re-added to the DOM, the `assignOnMount` properties will be reapplied:
653
+
654
+ ```JavaScript
655
+ const input = document.querySelector('input');
656
+ input.classList.add('validated'); // assignOnMount applied
657
+ input.classList.remove('validated'); // assignOnDismount applied
658
+ input.classList.add('validated'); // assignOnMount applied again
659
+ ```
660
+
661
+ This ensures consistent behavior across the element's lifecycle.
662
+
320
663
  ### Nested properties with dataset
321
664
 
322
- The `assignGingerly` library supports nested property assignment using the `?.` notation. This is particularly useful for setting data attributes:
665
+ The `assignGingerly` library supports nested property assignment using the `?.` notation. This is particularly useful for setting data attributes and style:
323
666
 
324
667
  ```JavaScript
325
668
  const observer = new MountObserver({
326
669
  whereElementMatches: 'button',
327
- assignGingerly: {
670
+ assignOnMount: {
328
671
  disabled: false,
329
- '?.dataset.action': 'submit',
330
- '?.dataset.trackingId': '12345'
672
+ '?.dataset?.action': 'submit',
673
+ '?.dataset?.trackingId': '12345',
674
+ '?.style': {
675
+ color: 'white',
676
+ height: '25px',
677
+ }
331
678
  }
332
679
  });
333
680
  observer.observe(document);
334
681
  ```
335
682
 
336
- The `?.` prefix tells assignGingerly to create nested properties if they don't exist. In this example, `?.dataset.action` will set the `data-action` attribute on the button elements.
683
+ The `?.` prefix tells assignGingerly to create nested properties if they don't exist. In this example, `?.dataset?.action` will set the `data-action` attribute on the button elements.
337
684
 
338
685
  ### Combining with imports
339
686
 
340
- You can combine `assignGingerly` with lazy loading to both import resources and set properties:
687
+ You can combine `assignOn*` with lazy loading to both import resources and set properties:
341
688
 
342
689
  ```JavaScript
343
690
  const observer = new MountObserver({
344
691
  whereElementMatches: 'my-element',
345
692
  import: './my-element.js',
346
- assignGingerly: {
693
+ assignOnMount: {
347
694
  theme: 'dark',
348
- '?.dataset.initialized': 'true'
695
+ '?.dataset?.initialized': 'true'
349
696
  },
350
697
  do: ({localName}, {modules}) => {
351
698
  if(!customElements.get(localName)) {
@@ -360,13 +707,386 @@ The `assignGingerly` properties are applied after imports are loaded but before
360
707
 
361
708
  ### Performance benefits
362
709
 
363
- Using `assignGingerly` provides several benefits:
710
+ Using `assignOn*` provides several benefits:
364
711
 
365
712
  1. **Lazy loading**: The assign-gingerly library is only loaded when needed (when the `assignGingerly` property is specified)
366
713
  2. **Bulk operations**: Properties are applied efficiently to all matching elements
367
714
  3. **Declarative**: No need to write custom mount callbacks for simple property assignments
368
715
  4. **Consistent**: The same property values are applied uniformly across all matching elements
369
716
 
717
+ ### Dynamically updating assignGingerly configuration
718
+
719
+ 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:
720
+
721
+ ```JavaScript
722
+ const observer = new MountObserver({
723
+ whereElementMatches: 'input',
724
+ assignOnMount: {
725
+ disabled: true,
726
+ value: 'Initial value'
727
+ }
728
+ });
729
+ observer.observe(document);
730
+
731
+ // Later, update the configuration
732
+ await observer.assignGingerly({
733
+ title: 'Updated tooltip',
734
+ placeholder: 'New placeholder'
735
+ });
736
+ ```
737
+
738
+ **Key behaviors:**
739
+
740
+ 1. **Merging**: New properties are merged with existing configuration. In the example above, future elements will receive all properties: `disabled`, `value`, `title`, and `placeholder`.
741
+
742
+ 2. **Applies to existing elements**: The new properties are immediately applied to all currently mounted elements.
743
+
744
+ 3. **Applies to future elements**: Future elements that mount will receive the merged configuration.
745
+
746
+ 4. **Starting without initial config**: You can call the method even if no `assignGingerly` was specified in the constructor:
747
+
748
+ ```JavaScript
749
+ const observer = new MountObserver({
750
+ whereElementMatches: 'input'
751
+ });
752
+ observer.observe(document);
753
+
754
+ // Set configuration later
755
+ await observer.assignGingerly({
756
+ disabled: true,
757
+ value: 'Set via method'
758
+ });
759
+ ```
760
+
761
+ 5. **Clearing configuration**: Pass `undefined` to clear the configuration for future elements (already-mounted elements keep their properties):
762
+
763
+ ```JavaScript
764
+ await observer.assignGingerly(undefined);
765
+ // Future elements will not have properties applied
766
+ // Existing elements retain their current properties
767
+ ```
768
+
769
+ **Method signature:**
770
+ ```TypeScript
771
+ async assignGingerly(config: Record<string, any> | undefined): Promise<void>
772
+ ```
773
+
774
+ The method is async because the assign-gingerly library is loaded dynamically when needed.
775
+
776
+ [Implemented as [Requirement9](requirements/Requirement9.md)]
777
+
778
+ ## Emitting events from mounted elements
779
+
780
+ MountObserver can automatically dispatch custom events from elements when they mount. This is useful for:
781
+
782
+ 1. **Signaling readiness**: Notify parent components or listeners that an element is ready
783
+ 2. **Initialization events**: Trigger workflows when elements appear in the DOM
784
+ 3. **Decoupled communication**: Allow elements to announce their presence without tight coupling
785
+
786
+ ### Basic event emission
787
+
788
+ ```JavaScript
789
+ const observer = new MountObserver({
790
+ whereElementMatches: 'button[data-action]',
791
+ mountedElemEmits: {
792
+ event: 'Event',
793
+ args: 'custom-ready'
794
+ }
795
+ });
796
+ observer.observe(document);
797
+ ```
798
+
799
+ 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:
800
+
801
+ ```JavaScript
802
+ document.addEventListener('custom-ready', (e) => {
803
+ console.log('Button ready:', e.target);
804
+ });
805
+ ```
806
+
807
+ ### Event constructors
808
+
809
+ You can specify any event constructor available in `globalThis`:
810
+
811
+ ```JavaScript
812
+ mountedElemEmits: {
813
+ event: 'CustomEvent',
814
+ args: ['element-ready', { detail: { timestamp: Date.now() } }]
815
+ }
816
+ ```
817
+
818
+ Or pass a constructor directly:
819
+
820
+ ```JavaScript
821
+ mountedElemEmits: {
822
+ event: CustomEvent,
823
+ args: ['element-ready', { detail: { timestamp: Date.now() } }]
824
+ }
825
+ ```
826
+
827
+ ### Magic string substitution
828
+
829
+ Use magic strings to inject dynamic values into event data:
830
+
831
+ - `{{mountedElement}}` - The element that just mounted
832
+ - `{{mountInit}}` - The MountInit configuration object
833
+
834
+ ```JavaScript
835
+ const observer = new MountObserver({
836
+ whereElementMatches: 'button[data-test]',
837
+ mountedElemEmits: {
838
+ event: 'CustomEvent',
839
+ args: ['element-mounted', {
840
+ detail: {
841
+ element: '{{mountedElement}}',
842
+ config: '{{mountInit}}'
843
+ }
844
+ }]
845
+ }
846
+ });
847
+ ```
848
+
849
+ Magic strings work at any depth in nested objects and arrays:
850
+
851
+ ```JavaScript
852
+ mountedElemEmits: {
853
+ event: 'CustomEvent',
854
+ args: ['data-ready', {
855
+ detail: {
856
+ nested: {
857
+ deep: {
858
+ element: '{{mountedElement}}'
859
+ }
860
+ }
861
+ }
862
+ }]
863
+ }
864
+ ```
865
+
866
+ ### Multiple events
867
+
868
+ Emit multiple events in sequence by providing an array:
869
+
870
+ ```JavaScript
871
+ const observer = new MountObserver({
872
+ whereElementMatches: 'my-component',
873
+ mountedElemEmits: [
874
+ { event: 'Event', args: 'component-loading' },
875
+ { event: 'Event', args: 'component-ready' },
876
+ { event: 'CustomEvent', args: ['component-initialized', { detail: { version: '1.0' } }] }
877
+ ]
878
+ });
879
+ ```
880
+
881
+ Events are dispatched in the order specified.
882
+
883
+ ### Event properties with eventProps
884
+
885
+ Apply additional properties to the event object using `eventProps`:
886
+
887
+ ```JavaScript
888
+ mountedElemEmits: {
889
+ event: 'CustomEvent',
890
+ args: ['ready', { detail: {} }],
891
+ eventProps: {
892
+ timestamp: Date.now(), //TODO: magic string?
893
+ source: 'mount-observer',
894
+ element: '{{mountedElement}}'
895
+ }
896
+ }
897
+ ```
898
+
899
+ Properties are applied using the [assignGingerly](https://github.com/bahrus/assign-gingerly) library, which supports nested property assignment with the `?.` notation.
900
+
901
+ ### Fire once per element
902
+
903
+ Use `oncePerMountedElement` to ensure an event only fires the first time an element mounts:
904
+
905
+ ```JavaScript
906
+ const observer = new MountObserver({
907
+ whereElementMatches: 'button[data-once]',
908
+ mountedElemEmits: {
909
+ event: 'Event',
910
+ args: 'initialized',
911
+ oncePerMountedElement: true
912
+ }
913
+ });
914
+ ```
915
+
916
+ If the element is removed and re-added to the DOM, the event will not fire again. This is useful for initialization events that should only happen once per element instance.
917
+
918
+ ### Performance considerations
919
+
920
+ The event emission logic is code-split into a separate module (`emitEvents.js`) that is only loaded when `mountedElemEmits` is configured. This keeps the core MountObserver lean for users who don't need this feature.
921
+
922
+ ### Complete example
923
+
924
+ ```JavaScript
925
+ const observer = new MountObserver({
926
+ whereElementMatches: 'my-widget',
927
+ import: './my-widget.js',
928
+ mountedElemEmits: [
929
+ {
930
+ event: 'CustomEvent',
931
+ args: ['widget-loading', {
932
+ detail: {
933
+ element: '{{mountedElement}}',
934
+ timestamp: Date.now()
935
+ }
936
+ }],
937
+ oncePerMountedElement: true
938
+ },
939
+ {
940
+ event: 'Event',
941
+ args: 'widget-ready'
942
+ }
943
+ ],
944
+ do: ({localName}, {modules}) => {
945
+ if(!customElements.get(localName)) {
946
+ customElements.define(localName, modules[0].MyWidget);
947
+ }
948
+ }
949
+ });
950
+
951
+ // Listen for events
952
+ document.addEventListener('widget-loading', (e) => {
953
+ console.log('Widget loading:', e.detail.element);
954
+ });
955
+
956
+ document.addEventListener('widget-ready', (e) => {
957
+ console.log('Widget ready:', e.target);
958
+ });
959
+
960
+ observer.observe(document);
961
+ ```
962
+
963
+ [Implemented as [Requirement10](requirements/Requirement10.md)]
964
+
965
+ ## Element-specific lifecycle notifications with getNotifier
966
+
967
+ While the MountObserver dispatches lifecycle events (mount, dismount, disconnect, attrchange) 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.
968
+
969
+ ### Basic usage
970
+
971
+ ```JavaScript
972
+ const observer = new MountObserver({
973
+ whereElementMatches: 'button',
974
+ do: (mountedElement, {observer}) => {
975
+ const notifier = observer.getNotifier(mountedElement);
976
+
977
+ notifier.addEventListener('mount', (e) => {
978
+ console.log('This specific button mounted', e.mountedElement);
979
+ });
980
+
981
+ notifier.addEventListener('dismount', (e) => {
982
+ console.log('This specific button dismounted', e.mountedElement, e.reason);
983
+ });
984
+
985
+ notifier.addEventListener('disconnect', (e) => {
986
+ console.log('This specific button disconnected', e.mountedElement);
987
+ });
988
+ }
989
+ });
990
+ observer.observe(document);
991
+ ```
992
+
993
+ ### When mount events fire on notifiers
994
+
995
+ The notifier follows a specific rule for mount events:
996
+
997
+ - **First mount**: If `getNotifier()` is called during the `do` callback (when the element is mounting), the mount event does NOT fire on the notifier
998
+ - **Subsequent mounts**: After the element dismounts and mounts again, the mount event WILL fire on the notifier
999
+
1000
+ This prevents duplicate mount notifications when setting up listeners during the initial mount.
1001
+
1002
+ ```JavaScript
1003
+ const observer = new MountObserver({
1004
+ whereElementMatches: '#my-button',
1005
+ do: (element, {observer}) => {
1006
+ const notifier = observer.getNotifier(element);
1007
+
1008
+ // This listener won't fire for the current mount
1009
+ // (since we're inside the do callback)
1010
+ notifier.addEventListener('mount', () => {
1011
+ console.log('Element re-mounted after being removed');
1012
+ });
1013
+ }
1014
+ });
1015
+ ```
1016
+
1017
+ ### Creating notifiers before mounting
1018
+
1019
+ You can call `getNotifier()` at any time, even before an element mounts:
1020
+
1021
+ ```JavaScript
1022
+ const observer = new MountObserver({
1023
+ whereElementMatches: '#future-button'
1024
+ });
1025
+ observer.observe(document);
1026
+
1027
+ // Get notifier before element exists
1028
+ const button = document.createElement('button');
1029
+ button.id = 'future-button';
1030
+
1031
+ const notifier = observer.getNotifier(button);
1032
+ notifier.addEventListener('mount', () => {
1033
+ console.log('Button mounted!'); // This WILL fire
1034
+ });
1035
+
1036
+ // Add to DOM later
1037
+ document.body.appendChild(button);
1038
+ ```
1039
+
1040
+ When the notifier is created before the element mounts, the mount event fires normally.
1041
+
1042
+ ### Filtered attrchange events
1043
+
1044
+ For `attrchange` events, the notifier receives a filtered version containing only changes for that specific element:
1045
+
1046
+ ```JavaScript
1047
+ const observer = new MountObserver({
1048
+ whereElementMatches: 'input',
1049
+ whereAttr: {
1050
+ hasBuiltInRootIn: ['data'],
1051
+ hasCERootIn: ['data'],
1052
+ hasBase: 'value',
1053
+ hasBranchIn: ['']
1054
+ },
1055
+ do: (element, {observer}) => {
1056
+ const notifier = observer.getNotifier(element);
1057
+
1058
+ notifier.addEventListener('attrchange', (e) => {
1059
+ // e.changes only contains changes for this specific input
1060
+ console.log('Attribute changed on this input:', e.changes);
1061
+ });
1062
+ }
1063
+ });
1064
+ ```
1065
+
1066
+ Even if multiple elements have attribute changes in the same mutation batch, each notifier only receives the changes relevant to its element.
1067
+
1068
+ ### Use cases
1069
+
1070
+ Element-specific notifiers are useful for:
1071
+
1072
+ 1. **Progressive enhancement**: Attach/detach behaviors when elements mount/dismount
1073
+ 2. **Cleanup on disconnect**: Remove event listeners or cancel timers when elements are removed
1074
+ 3. **Peer element coordination**: React to changes in related elements
1075
+ 4. **Lifecycle-aware components**: Build components that respond to their own mounting state
1076
+
1077
+ ### Performance notes
1078
+
1079
+ - Notifiers are cached in a WeakMap, so calling `getNotifier()` multiple times for the same element returns the same EventTarget
1080
+ - No explicit cleanup is needed - notifiers are garbage collected when their elements are
1081
+ - The notifier continues to exist even after the element disconnects, allowing it to receive mount events if the element is re-added
1082
+
1083
+ **Method signature:**
1084
+ ```TypeScript
1085
+ getNotifier(element: Element): EventTarget
1086
+ ```
1087
+
1088
+ [Implemented as [Requirement13](requirements/Requirement13.md)]
1089
+
370
1090
 
371
1091
  ## Extra lazy loading
372
1092
 
@@ -376,7 +1096,7 @@ However, we could make the loading even more lazy by specifying intersection opt
376
1096
 
377
1097
  ```JavaScript
378
1098
  const observer = new MountObserver({
379
- select: 'my-element',
1099
+ select: 'my-element', //not supported by polyfill
380
1100
  whereElementIntersectsWith:{
381
1101
  rootMargin: "0px",
382
1102
  threshold: 1.0,
@@ -385,45 +1105,33 @@ const observer = new MountObserver({
385
1105
  });
386
1106
  ```
387
1107
 
388
- ## Media / container queries / instanceOf / custom checks
1108
+ ## Media / container queries / instanceOf / custom checks [TODO] out of date
389
1109
 
390
1110
  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?):
391
1111
 
392
1112
  ```JavaScript
393
1113
  const observer = new MountObserver({
394
- select: 'div > p + p ~ span[class$="name"]',
1114
+ select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
395
1115
  whereMediaMatches: '(max-width: 1250px)',
396
1116
  whereSizeOfContainerMatches: '(min-width: 700px)',
397
1117
  whereContainerHas: '[itemprop=isActive][value="true"]',
398
1118
  whereInstanceOf: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
399
1119
  whereLangIn: ['en-GB'],
400
- whereConnectiselect:{
1120
+ whereConnectionHas:{
401
1121
  effectiveTypeIn: ["slow-2g"],
402
1122
  },
403
1123
  import: ['./my-element-small.css', {type: 'css'}],
404
- do: {
405
- confirm: (matchingElement, (e: MountObserverConfirmEvent) => {
406
- e.isSatisfied = true;
407
- e.preventDefault();
408
- }),
409
- mount: ({localName}, {modules}) => {
410
- ...
411
- },
412
- dismount: ...,
413
- disconnect: ...,
414
- move: ...,
415
- reconnect: ...,
416
- confirm: ...,
417
- reconfirm: ...,
418
- exit: ...,
419
- forget: ...,
420
- }
1124
+ do: ...
421
1125
  });
422
1126
  ```
423
1127
 
1128
+ [whereInstanceOf implemented as [Requirement5](requirements/Requirement5.md)]
1129
+
1130
+ [whereMediaMatches implemented as [Requirement6](requirements/Requirement6.md)]
1131
+
424
1132
  ## InstanceOf checks in detail
425
1133
 
426
- 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. 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.
1134
+ 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.
427
1135
 
428
1136
  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.
429
1137
 
@@ -446,7 +1154,7 @@ observer.addEventListener('confirm', e => {
446
1154
  });
447
1155
  observer.addEventListener('mount', e => {
448
1156
  console.log({
449
- matchingElement: e.matchingElement,
1157
+ mountedElement: e.mountedElement,
450
1158
  module: e.module
451
1159
  });
452
1160
  });
@@ -473,6 +1181,8 @@ observer.addEventListener('forget', e => {
473
1181
  });
474
1182
  ```
475
1183
 
1184
+ [mount, dismount, disconnect] events implemented
1185
+
476
1186
  ## Explanation of all states / events
477
1187
 
478
1188
  Normally, an element stays in its place in the DOM tree, but the conditions that the MountObserver instance is monitoring for can change for the element, based on modifications to the attributes of the element itself, or its custom state, or to other peer elements within the shadowRoot, if any, or window resizing, etc. As the element meets or doesn't meet all the conditions, the mountObserver will first call the corresponding mount/dismount callback, and then dispatch event "mount" or "dismount" according to whether the criteria are all met or not.
@@ -506,6 +1216,8 @@ I'm on the fence on that one. I think the benefits either way to DX are so sma
506
1216
 
507
1217
  ## Dismounting
508
1218
 
1219
+ [TODO] This section is out of date
1220
+
509
1221
  In many cases, it will be critical to inform the developer **why** the element no longer satisfies all the criteria. For example, we may be using an intersection observer, and when we've scrolled away from view, we can "shut down" until the element is (nearly) scrolled back into view. We may also be displaying things differently depending on the network speed. How we should respond when one of the original conditions, but not the other, no longer applies, is of paramount importance.
510
1222
 
511
1223
  So the dismount event should provide a "checklist" of all the conditions, and their current value:
@@ -538,6 +1250,8 @@ So I believe the prudent thing to do is wait for all the conditions to be satisf
538
1250
 
539
1251
  The alternative to providing this feature, which I'm leaning towards, is to just ask the developer to create "specialized" mountObserver construction arguments, that turn on and off precisely when the developer needs to know.
540
1252
 
1253
+ [Implemented with [Requirement6](requirements/Requirement6.md)]
1254
+
541
1255
 
542
1256
  ## Support for "donut hole scoping"
543
1257
 
@@ -550,36 +1264,48 @@ For the polyfill, we need to support it as follows:
550
1264
  ```html
551
1265
  <div id=myTest itemscope>
552
1266
  <span itemprop=name>
1267
+ <div itemscope>
1268
+ <data itemprop=ssn>
1269
+ </div>
553
1270
  </div>
554
1271
  ```
555
1272
 
1273
+ We want to find all elements with attribute itemprop outside any itemscope, so the span and not the data element.
1274
+
556
1275
  ```JavaScript
557
- const oElement = document.getElementById('myTest');
1276
+ const oContainerNode = document.getElementById('myTest');
558
1277
  const observer = new MountObserver({
559
- select:'[itemprop]',
560
- outside: '[itemscope]'
561
- do: {
562
- mount: ({localName}, {modules, observer}) => {
563
- ...
564
- },
1278
+ whereElementMatches:'[itemprop]',
1279
+ whereOutside: '[itemscope]'
1280
+ do: ({localName}, {modules, observer}) => {
1281
+ ...
565
1282
  },
566
1283
  disconnectedSignal: new AbortController().signal
567
1284
  });
568
- observer.observe(oElement);
1285
+ observer.observe(oContainerNode);
569
1286
  ```
570
1287
 
571
- The check for "outside" is done via script:
1288
+ The check for "whereOutside" is done via script:
572
1289
 
573
1290
  ```JavaScript
574
- outsideCheck(oElement: Element, matchCandidate: Element, outside: string){
575
- const elementsToExclude = Array.from(oElement.querySelectorAll(outside));
576
- for(const elementToExclude of elementsToExclude){
577
- if(elementToExclude === matchCandidate || elementToExclude.contains(matchCandidate)) return false;
578
- }
579
- return true;
1291
+ import {whereOutside} from 'mount-observer/whereOutside.js';
1292
+ whereOutside(oContainerNode: Node, matchCandidate: Element, outside: string){
1293
+ let current = matchCandidate.parentElement;
1294
+
1295
+ while (current && current !== oContainerNode) {
1296
+ if (current.matches(outside)) {
1297
+ return false; // Found an excluding ancestor
1298
+ }
1299
+ current = current.parentElement;
1300
+ }
1301
+
1302
+ return true; // No excluding ancestors found
580
1303
  }
1304
+
581
1305
  ```
582
1306
 
1307
+ [Implemented as [Requirement7](requirements/Requirement7.md)]
1308
+
583
1309
  ## A tribute to attributes
584
1310
 
585
1311
  Attributes of DOM elements are tricky. They've been around since the get-go of the Web, and they've survived multiple eras of web development, where different philosophies have prevailed, so prepare yourself for some esoteric discussions in what follows.
@@ -632,10 +1358,10 @@ const mo = new MountObserver({
632
1358
  observedAttrsWhenMounted: ['lang', 'contenteditable']
633
1359
  });
634
1360
 
635
- mo.addEventListener('attrChange', e => {
1361
+ mo.addEventListener('attrchange', e => {
636
1362
  console.log(e);
637
1363
  // {
638
- // matchingElement,
1364
+ // mountedElement,
639
1365
  // attrChangeInfo:[{
640
1366
  // idx: 0,
641
1367
  // name: 'lang'
@@ -779,7 +1505,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
779
1505
  mo.addEventListener('attrChange', e => {
780
1506
  console.log(e);
781
1507
  // {
782
- // matchingElement,
1508
+ // mountedElement,
783
1509
  // attrChangeInfo:[{
784
1510
  // idx: 0,
785
1511
  // oldValue: null,
@@ -1144,78 +1870,6 @@ Just as it is useful to be able lazy load external imports when needed, it would
1144
1870
  </compose>
1145
1871
  ```
1146
1872
 
1147
-
1148
- ## Creating "frameworks" that revolve around MOSEs.
1149
-
1150
- Often, we will want to define a large number of "mount observer script elements (MOSEs)" programmatically, and we need it to be done in a generic way, that can be published and easily referenced.
1151
-
1152
- This is a problem space that [be-hive](https://github.com/bahrus/be-hive) is grappling with, and is used as an example for this section, to simply make things more concrete. But we can certainly envision other "frameworks" that could leverage this feature for a variety of purposes, including other families of behaviors/enhancements, or "binding from a distance" syntaxes.
1153
-
1154
- In particular, *be-hive* supports publishing [enhancements](https://github.com/bahrus/be-enhanced) that take advantage of the DOM filtering ability that the MountObserver provides, that "ties the knot" based on CSS matches in the DOM to behaviors/enhancements that we want to attach directly onto the matching elements. *be-hive* seeks to take advantage of the inheritable infrastructure that MOSEs provide, but we don't want to burden the developer with having to manually list all these configurations, we want it to happen automatically, only expecting manual intervention when we need some special customizations within a specific ShadowDOM realm.
1155
-
1156
- To support this, we propose these highlights:
1157
-
1158
- 1. Adding a static "synthesize" method to the MountObserver api. This would provide a kind of passage-way from the imperative api to the declarative one.
1159
- 2. As the *synthesize* method is called repeatedly from different packages that work within that framework, it creates a cluster of MOSEs wrapped inside the "synthesizing" custom element ("be-hive") that the framework developer authors. It appends script elements with type="mountobserver" to the custom element instance sitting in the DOM, that dispatches events from the synthesizing custom element it gets appended to, so subscribers in child Shadow DOM's don't need to add a general mutation observer in order to know when parent shadow roots had a MOSE inserted that it needs to act on. This allows the child Shadow DOM's to inherit (in this case) behaviors/enhancements from the parent Shadow DOM.
1160
-
1161
- So framework developers can develop a bespoke custom element that inherits from the "abstract" class "*Synthesizer*" that is part of this package / proposal, that is used to group families of MountObserver's together.
1162
-
1163
- Some attributes that the base "Synthesizer" supports are listed below. They are all related to allowing individual ShadowDOM realms to be able to easily opt in or opt out, depending on the level of control/trust that is exerted by a web component / Shadow Root, as far as the HTML it imports in.
1164
-
1165
- 1. passthrough. Allows for the inheritance of behaviors to flow through from above (or from the root document), while not actually activating any of them within the Shadow DOM realm itself.
1166
- 2. exclude. List of specific MOSE id's to block. Allows them to flow through to child Shadow Roots.
1167
- 3. include. List of specific MOSE id's to allow.
1168
-
1169
- What functionality do these "synthesizing" custom elements provide, what value-add proposition do they fulfill over what is built into the MountObserver polyfill / package?
1170
-
1171
- The sky is the limit, but focusing on the first example, be-hive, they are:
1172
-
1173
- 1. Managing, interpreting and parsing the attributes that add semantic enhancement vocabularies onto exiting elements.
1174
- 2. Establishing the "handshake" that imports the enhancement package, instantiates the enhancement, and passes properties that were previously assigned to the pre-enhanced element to the attached enhancement/behavior.
1175
- 3. Providing an inheritable "registry" of reusable scriptlets that can be leveraged in a declarative way.
1176
-
1177
- If one inspects the DOM, one will see grouped (already "parsed") MOSEs, like so:
1178
-
1179
- ```html
1180
- <be-hive>
1181
- <script type=mountobserver id=be-hive.be-searching></script>
1182
- <script type=mountobserver id=be-hive.be-counted></script>
1183
- </be-hive>
1184
- ```
1185
-
1186
- Without the help of the synthesize method / Synthesizer base class, the developer would need to set these up manually, so this lifts a significant burden from the shoulders of people who want to leverage these behaviors/enhancements in a seamless way.
1187
-
1188
- The developer of each package defines their MOSE "template", and then syndicates it via the synthesize method:
1189
-
1190
- ```JavaScript
1191
- MountObserver.synthesize(root: document | shadowRootNode, ctr: ({new() => Synthesizer}), mose: MOSE)
1192
- ```
1193
-
1194
- What this method does is it:
1195
-
1196
- 1. Uses [customElements.getName](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/getName) to get the name of the custom element (say it is 'be-hive') from the provided constructor.
1197
- 2. Searches for a be-hive tag inside the root node (with special logic for the "head" element). If not found, creates it.
1198
- 3. Places the MOSE inside.
1199
-
1200
-
1201
- Then in our shadowroot, rather than adding a script type=mountobserver for every single mount observer we want to inherit, we could reference the group via simply:
1202
-
1203
- ```html
1204
- <be-hive></be-hive>
1205
- ```
1206
-
1207
- And we can give each inheriting ShadowRoot a personality of its own by customizing the settings within that shadow scope, by manually adding a MOSE with matching id that overrides the inheriting settings with custom settings:
1208
-
1209
- ```html
1210
- <be-hive>
1211
- <script type=mountobserver id=be-hive.be-searching>
1212
- {
1213
- ...my custom settings
1214
- }
1215
- </script>
1216
- </be-hive>
1217
- ```
1218
-
1219
1873
  ## Creating an Element-To-RefID DOM traversal API
1220
1874
 
1221
1875
  The platform provides some nice help with managing forms, including IDREF dependency support:
@@ -1332,4 +1986,4 @@ To keep the api uniform, we hide this discrepancy by pretending the form element
1332
1986
  // includes both field1 and field2
1333
1987
 
1334
1988
  </script>
1335
- ```
1989
+ ```