mount-observer 0.1.2 → 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
@@ -28,7 +28,8 @@ The following features have been implemented and tested:
28
28
 
29
29
  ### Advanced Features
30
30
  - ✅ **Dynamic imports**: Lazy loading of JavaScript modules
31
- - ✅ **assignGingerly**: Property assignment on mount
31
+ - ✅ **assignOnMount**: Property assignment when elements mount
32
+ - ✅ **assignOnDismount**: Property assignment when elements dismount
32
33
  - ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
33
34
  - ✅ **map configuration**: Metadata mapping for attribute coordinates
34
35
  - ✅ **once option**: Fire attrchange event only once per attribute
@@ -108,9 +109,9 @@ To specify the equivalent of what the alternative proposal linked to above would
108
109
 
109
110
  ```JavaScript
110
111
  const observer = new MountObserver({
111
- select:'my-element',
112
+ select:'my-element', //not supported by this polyfill
112
113
  import: './my-element.js',
113
- do: ({localName}, {modules, observer, observeInfo}) => {
114
+ do: ({localName}, {modules, observer, mountInit, rootNode}) => {
114
115
  if(!customElements.get(localName)) {
115
116
  customElements.define(localName, modules[0].MyElement);
116
117
  }
@@ -121,7 +122,7 @@ const observer = new MountObserver({
121
122
  observer.observe(document);
122
123
  ```
123
124
 
124
- 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.
125
126
 
126
127
  The constructor argument can also be an array of objects that fit the pattern shown above.
127
128
 
@@ -138,19 +139,20 @@ The "observer" constant above is a class instance that inherits from EventTarget
138
139
  > [!Note]
139
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.
140
141
 
141
- 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.
142
143
 
143
- 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 every provide a selector-observer (if ever).
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).
144
145
 
145
146
  So the developer could use:
146
147
 
147
- ## Polyfill Supported Scenario I
148
+ ## Polyfill Supported Mount Observer
148
149
 
149
150
  ```JavaScript
150
151
  const observer = new MountObserver({
152
+ //supported by this polyfill
151
153
  whereElementMatches:'my-element',
152
154
  import: './my-element.js',
153
- do: ({localName}, {modules, observer, observeInfo}) => {
155
+ do: ({localName}, {modules, observer, mountInit, rootNode}) => {
154
156
  if(!customElements.get(localName)) {
155
157
  customElements.define(localName, modules[0].MyElement);
156
158
  }
@@ -163,7 +165,7 @@ observer.observe(document);
163
165
 
164
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 "*".
165
167
 
166
- 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.
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.
167
169
 
168
170
  [Implemented as Requirement 1](requirements/Requirement1.md).
169
171
 
@@ -173,12 +175,12 @@ This proposal has been amended to support multiple imports, including of differe
173
175
 
174
176
  ```JavaScript
175
177
  const observer = new MountObserver({
176
- select:'my-element',
178
+ whereElementMatches:'my-element',
177
179
  import: [
178
180
  ['./my-element-small.css', {type: 'css'}],
179
181
  './my-element.js',
180
182
  ],
181
- do: ({localName}, {modules, observer}) => {
183
+ do: ({localName}, {modules, observer, mountInit, rootNode}) => {
182
184
  if(!customElements.get(localName)) {
183
185
  customElements.define(localName, modules[1].MyElement);
184
186
  }
@@ -213,7 +215,7 @@ So for this we add loadingEagerness:
213
215
 
214
216
  ```JavaScript
215
217
  const observer = new MountObserver({
216
- select: 'my-element',
218
+ select: 'my-element', //not supported by this polyfill
217
219
  loadingEagerness: 'eager',
218
220
  import: './my-element.js',
219
221
  do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
@@ -225,130 +227,368 @@ So what this does is only check for the presence of an element with tag name "my
225
227
  > [!NOTE]
226
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.
227
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
+
228
351
 
229
352
 
230
353
  ## Mount Observer Script Elements (MOSEs)
231
354
 
232
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:
233
356
 
234
- ```html
235
- <script type="mountobserver" onload="{...}" onmount="{
236
- const {matchingElement} = event;
237
- const {localName} = matchingElement;
357
+ ```JavaScript
358
+ // myPackage/myDefiner.js
359
+ //my all powerful custom element definer
360
+ const doFunction = function({localNme}, {modules, observer}){
238
361
  if(!customElements.get(localName)) {
239
362
  customElements.define(localName, modules[1].MyElement);
240
363
  }
241
364
  observer.disconnectedSignal.abort();
242
- }">
365
+ }
366
+ export { doFunction as do };
367
+ ```
368
+
369
+ ```html
370
+ <script type="mountobserver" >
243
371
  {
244
372
  "select":"my-element",
245
373
  "import": [
246
374
  ["./my-element-small.css", {type: "css"}],
247
375
  "./my-element.js",
248
- ]
376
+ "myPackage/myDefiner.js
377
+ ],
378
+ "reference": 2
249
379
  }
250
380
  </script>
251
381
  ```
252
382
 
253
- 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:
254
391
 
255
392
  ```JavaScript
256
- 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);
257
414
  ```
258
415
 
259
- 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).
260
416
 
261
- 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.
262
418
 
263
- > [!Note]
264
- > 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).
265
420
 
266
- ## Specific solution for lazy loading custom element definitions
421
+ This allows developers to create "stylesheet" like capabilities.
267
422
 
268
- 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
269
424
 
270
- 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:
271
426
 
272
- ```html
273
- <script type="mountobserver">
274
- {
275
- "select":"my-element",
276
- "import": [
277
- ["./my-element-small.css", {type: "css"}],
278
- "./my-element.js",
279
- ],
280
- "define": {
281
- "targetRegistry": "CustomElements",
282
- "targetScope": "global",
283
- "styleModules": [0],
284
- "classDefinition": {
285
- "module": 1,
286
- "exportSymbol": 'MyElement'
287
- }
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';
288
436
  }
289
437
  }
290
- </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);
291
448
  ```
292
449
 
450
+ ### Benefits of registered handlers
293
451
 
294
- ## 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
295
455
 
296
- 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
297
457
 
298
- ```html
299
- #shadowRoot
300
- <script id=myMountObserver type=mountobserver>
301
- {
302
- "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!';
303
509
  }
304
- </script>
510
+
511
+ MountObserver.define('simple', SimpleHandler);
305
512
  ```
306
513
 
307
- 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
308
515
 
309
- 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:
310
517
 
311
- > [!Note]
312
- > The onload event is critical for a number of reasons, among them:
313
- > 1. We need a way to inject non JSON serializable settings (described below) when necessary, and
314
- > 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:
315
525
 
316
- 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.
526
+ ```JavaScript
527
+ MountObserver.define('myHandler', Handler1);
528
+ MountObserver.define('myHandler', Handler2); // Error: myHandler already in use
529
+ ```
317
530
 
318
531
 
319
- ## Binding from a distance
320
532
 
321
- It is important to note that "select" is a css query with no restrictions. So something like:
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.
542
+
543
+ #### Log to console handler
322
544
 
323
545
  ```JavaScript
324
546
  const observer = new MountObserver({
325
- select:'div > p + p ~ span[class$="name"]',
326
- do:{
327
- mount: (matchingElement) => {
328
- //attach some behavior or set some property value or add an event listener, etc.
329
- matchingElement.textContent = 'hello';
330
- },
331
- dismount: (matchingElement) => {
332
- matchingElement.textContent = 'bye';
333
- }
334
- }
335
- })
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);
336
554
  ```
337
555
 
338
- ... would work.
556
+ This logs to console all the events (mount, dismount, disconnect)
339
557
 
340
- 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"**.
558
+ ### Lazy custom element handler
341
559
 
342
- This would allow developers to create "stylesheet" like capabilities.
560
+ ```JavaScript
561
+ // MyElement.js
562
+ export default class MyElement extends HTMLElement {
563
+ connectedCallback() {
564
+ this.textContent = 'Hello!';
565
+ }
566
+ }
343
567
 
344
- ## Applying properties with assignGingerly
568
+ // main.js
569
+ import { MountObserver } from 'mount-observer';
345
570
 
346
- 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 without writing custom mount callbacks:
571
+ const observer = new MountObserver({
572
+ whereElementMatches: 'my-element',
573
+ import: './MyElement.js',
574
+ do: 'builtIns.defineCustomElement'
575
+ });
576
+ observer.observe(document);
577
+
578
+ // HTML - elements will be upgraded when discovered
579
+ // by the mount observer
580
+ <my-element></my-element>
581
+
582
+ ```
583
+
584
+ ## Applying properties on mount and dismount
585
+
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:
347
587
 
348
588
  ```JavaScript
349
589
  const observer = new MountObserver({
350
590
  whereElementMatches: 'input',
351
- assignGingerly: {
591
+ assignOnMount: {
352
592
  disabled: true,
353
593
  value: 'Default value',
354
594
  title: 'This is a tooltip'
@@ -359,7 +599,66 @@ observer.observe(document);
359
599
 
360
600
  This will automatically apply the specified properties to all matching input elements, both existing ones and those added dynamically.
361
601
 
362
- [Implemented as [Requirement2](requirements/Requirement2.md)]
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.
363
662
 
364
663
  ### Nested properties with dataset
365
664
 
@@ -368,7 +667,7 @@ The `assignGingerly` library supports nested property assignment using the `?.`
368
667
  ```JavaScript
369
668
  const observer = new MountObserver({
370
669
  whereElementMatches: 'button',
371
- assignGingerly: {
670
+ assignOnMount: {
372
671
  disabled: false,
373
672
  '?.dataset?.action': 'submit',
374
673
  '?.dataset?.trackingId': '12345',
@@ -385,13 +684,13 @@ The `?.` prefix tells assignGingerly to create nested properties if they don't e
385
684
 
386
685
  ### Combining with imports
387
686
 
388
- 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:
389
688
 
390
689
  ```JavaScript
391
690
  const observer = new MountObserver({
392
691
  whereElementMatches: 'my-element',
393
692
  import: './my-element.js',
394
- assignGingerly: {
693
+ assignOnMount: {
395
694
  theme: 'dark',
396
695
  '?.dataset?.initialized': 'true'
397
696
  },
@@ -408,7 +707,7 @@ The `assignGingerly` properties are applied after imports are loaded but before
408
707
 
409
708
  ### Performance benefits
410
709
 
411
- Using `assignGingerly` provides several benefits:
710
+ Using `assignOn*` provides several benefits:
412
711
 
413
712
  1. **Lazy loading**: The assign-gingerly library is only loaded when needed (when the `assignGingerly` property is specified)
414
713
  2. **Bulk operations**: Properties are applied efficiently to all matching elements
@@ -422,7 +721,7 @@ The `MountObserver` class provides a public `assignGingerly()` method that allow
422
721
  ```JavaScript
423
722
  const observer = new MountObserver({
424
723
  whereElementMatches: 'input',
425
- assignGingerly: {
724
+ assignOnMount: {
426
725
  disabled: true,
427
726
  value: 'Initial value'
428
727
  }
@@ -590,7 +889,7 @@ mountedElemEmits: {
590
889
  event: 'CustomEvent',
591
890
  args: ['ready', { detail: {} }],
592
891
  eventProps: {
593
- timestamp: Date.now(),
892
+ timestamp: Date.now(), //TODO: magic string?
594
893
  source: 'mount-observer',
595
894
  element: '{{mountedElement}}'
596
895
  }
@@ -663,6 +962,131 @@ observer.observe(document);
663
962
 
664
963
  [Implemented as [Requirement10](requirements/Requirement10.md)]
665
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
+
666
1090
 
667
1091
  ## Extra lazy loading
668
1092
 
@@ -681,13 +1105,13 @@ const observer = new MountObserver({
681
1105
  });
682
1106
  ```
683
1107
 
684
- ## Media / container queries / instanceOf / custom checks
1108
+ ## Media / container queries / instanceOf / custom checks [TODO] out of date
685
1109
 
686
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?):
687
1111
 
688
1112
  ```JavaScript
689
1113
  const observer = new MountObserver({
690
- select: 'div > p + p ~ span[class$="name"]',
1114
+ select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
691
1115
  whereMediaMatches: '(max-width: 1250px)',
692
1116
  whereSizeOfContainerMatches: '(min-width: 700px)',
693
1117
  whereContainerHas: '[itemprop=isActive][value="true"]',
@@ -697,23 +1121,7 @@ const observer = new MountObserver({
697
1121
  effectiveTypeIn: ["slow-2g"],
698
1122
  },
699
1123
  import: ['./my-element-small.css', {type: 'css'}],
700
- do: {
701
- confirm: (matchingElement, (e: MountObserverConfirmEvent) => {
702
- e.isSatisfied = true;
703
- e.preventDefault();
704
- }),
705
- mount: ({localName}, {modules}) => {
706
- ...
707
- },
708
- dismount: ...,
709
- disconnect: ...,
710
- move: ...,
711
- reconnect: ...,
712
- confirm: ...,
713
- reconfirm: ...,
714
- exit: ...,
715
- forget: ...,
716
- }
1124
+ do: ...
717
1125
  });
718
1126
  ```
719
1127
 
@@ -746,7 +1154,7 @@ observer.addEventListener('confirm', e => {
746
1154
  });
747
1155
  observer.addEventListener('mount', e => {
748
1156
  console.log({
749
- matchingElement: e.matchingElement,
1157
+ mountedElement: e.mountedElement,
750
1158
  module: e.module
751
1159
  });
752
1160
  });
@@ -869,10 +1277,8 @@ const oContainerNode = document.getElementById('myTest');
869
1277
  const observer = new MountObserver({
870
1278
  whereElementMatches:'[itemprop]',
871
1279
  whereOutside: '[itemscope]'
872
- do: {
873
- mount: ({localName}, {modules, observer}) => {
874
- ...
875
- },
1280
+ do: ({localName}, {modules, observer}) => {
1281
+ ...
876
1282
  },
877
1283
  disconnectedSignal: new AbortController().signal
878
1284
  });
@@ -952,10 +1358,10 @@ const mo = new MountObserver({
952
1358
  observedAttrsWhenMounted: ['lang', 'contenteditable']
953
1359
  });
954
1360
 
955
- mo.addEventListener('attrChange', e => {
1361
+ mo.addEventListener('attrchange', e => {
956
1362
  console.log(e);
957
1363
  // {
958
- // matchingElement,
1364
+ // mountedElement,
959
1365
  // attrChangeInfo:[{
960
1366
  // idx: 0,
961
1367
  // name: 'lang'
@@ -1099,7 +1505,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
1099
1505
  mo.addEventListener('attrChange', e => {
1100
1506
  console.log(e);
1101
1507
  // {
1102
- // matchingElement,
1508
+ // mountedElement,
1103
1509
  // attrChangeInfo:[{
1104
1510
  // idx: 0,
1105
1511
  // oldValue: null,
@@ -1464,78 +1870,6 @@ Just as it is useful to be able lazy load external imports when needed, it would
1464
1870
  </compose>
1465
1871
  ```
1466
1872
 
1467
-
1468
- ## Creating "frameworks" that revolve around MOSEs.
1469
-
1470
- 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.
1471
-
1472
- 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.
1473
-
1474
- 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.
1475
-
1476
- To support this, we propose these highlights:
1477
-
1478
- 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.
1479
- 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.
1480
-
1481
- 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.
1482
-
1483
- 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.
1484
-
1485
- 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.
1486
- 2. exclude. List of specific MOSE id's to block. Allows them to flow through to child Shadow Roots.
1487
- 3. include. List of specific MOSE id's to allow.
1488
-
1489
- What functionality do these "synthesizing" custom elements provide, what value-add proposition do they fulfill over what is built into the MountObserver polyfill / package?
1490
-
1491
- The sky is the limit, but focusing on the first example, be-hive, they are:
1492
-
1493
- 1. Managing, interpreting and parsing the attributes that add semantic enhancement vocabularies onto exiting elements.
1494
- 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.
1495
- 3. Providing an inheritable "registry" of reusable scriptlets that can be leveraged in a declarative way.
1496
-
1497
- If one inspects the DOM, one will see grouped (already "parsed") MOSEs, like so:
1498
-
1499
- ```html
1500
- <be-hive>
1501
- <script type=mountobserver id=be-hive.be-searching></script>
1502
- <script type=mountobserver id=be-hive.be-counted></script>
1503
- </be-hive>
1504
- ```
1505
-
1506
- 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.
1507
-
1508
- The developer of each package defines their MOSE "template", and then syndicates it via the synthesize method:
1509
-
1510
- ```JavaScript
1511
- MountObserver.synthesize(root: document | shadowRootNode, ctr: ({new() => Synthesizer}), mose: MOSE)
1512
- ```
1513
-
1514
- What this method does is it:
1515
-
1516
- 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.
1517
- 2. Searches for a be-hive tag inside the root node (with special logic for the "head" element). If not found, creates it.
1518
- 3. Places the MOSE inside.
1519
-
1520
-
1521
- 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:
1522
-
1523
- ```html
1524
- <be-hive></be-hive>
1525
- ```
1526
-
1527
- 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:
1528
-
1529
- ```html
1530
- <be-hive>
1531
- <script type=mountobserver id=be-hive.be-searching>
1532
- {
1533
- ...my custom settings
1534
- }
1535
- </script>
1536
- </be-hive>
1537
- ```
1538
-
1539
1873
  ## Creating an Element-To-RefID DOM traversal API
1540
1874
 
1541
1875
  The platform provides some nice help with managing forms, including IDREF dependency support:
@@ -1652,4 +1986,4 @@ To keep the api uniform, we hide this discrepancy by pretending the form element
1652
1986
  // includes both field1 and field2
1653
1987
 
1654
1988
  </script>
1655
- ```
1989
+ ```