mount-observer 0.1.4 → 0.1.6

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
@@ -10,19 +10,13 @@ Note that much of what is described below has not yet been polyfilled.
10
10
  The following features have been implemented and tested:
11
11
 
12
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)
13
+ - ✅ **matching**: CSS selector-based element matching
14
+ - ✅ **withInstance**: Constructor-based element filtering (single or array)
15
+ - ✅ **withMediaMatching**: Media query-based conditional mounting (string or MediaQueryList)
16
+ - **withScopePerimeter**: Donut hole scoping (exclude elements inside matching ancestors)
22
17
 
23
18
  ### Lifecycle & Events
24
19
  - ✅ **mount/dismount/disconnect events**: Element lifecycle tracking
25
- - ✅ **attrchange event**: Attribute change notifications with batching
26
20
  - ✅ **mediamatch/mediaunmatch events**: Media query state change notifications (with `getPlayByPlay` option)
27
21
  - ✅ **load event**: Import completion notification
28
22
 
@@ -30,9 +24,11 @@ The following features have been implemented and tested:
30
24
  - ✅ **Dynamic imports**: Lazy loading of JavaScript modules
31
25
  - ✅ **assignOnMount**: Property assignment when elements mount
32
26
  - ✅ **assignOnDismount**: Property assignment when elements dismount
27
+ - ✅ **stageOnMount**: Reversible property assignment (auto-restores on dismount)
28
+ - ✅ **spawn**: Automatic enhancement spawning via assign-gingerly integration
33
29
  - ✅ **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
30
+ - ✅ **Array argument shorthand**: Pass EnhancementConfig[] directly to constructor
31
+ - ✅ **Element mount extension**: element.mount() method for scoped registry observation
36
32
  - ✅ **Shared MutationObserver**: Efficient observer sharing across instances
37
33
  - ✅ **Code splitting**: Conditional features loaded on-demand
38
34
  - ✅ **Memory management**: WeakRef usage for DOM node references
@@ -44,21 +40,21 @@ The following features have been implemented and tested:
44
40
  - ❌ Reconnect event handling
45
41
  - ❌ Multiple import types (CSS, JSON, HTML)
46
42
 
47
- # The MountObserver api.
43
+ # The MountObserver API
48
44
 
49
- Author: Bruce B. Anderson (with valuable feedback from @doeixd )
45
+ Author: Bruce B. Anderson (with valuable feedback from @doeixd)
50
46
 
51
- Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
47
+ Issues / PRs / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
52
48
 
53
49
  Last Update: Aug 7, 2025
54
50
 
55
51
  ## Benefits of this API
56
52
 
57
- What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver api are more encompassing, and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one api. It is basically mapping common filtering conditions in the DOM, to mounting a "campaign" of some sort, like importing a resource, and/or progressively enhancing an element, and/or "binding from a distance".
53
+ What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver API are more encompassing and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one API. It basically maps common filtering conditions in the DOM to mounting a "campaign" of some sort, like importing a resource, and/or progressively enhancing an element, and/or "binding from a distance".
58
54
 
59
55
  ["Binding from a distance"](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc, to the HTML as it streams in. Libraries that take this approach include [Corset](https://corset.dev/) and [trans-render](https://github.com/bahrus/trans-render), [selector-observer](https://github.com/josh/selector-observer), [pure](http://web.archive.org/web/20160313152905/https://beebole.com/pure/), [weld](https://github.com/tmpvar/weld), [bess](https://github.com/bkardell/bess). The concept has been promoted by a [number](https://bkardell.com/blog/CSSLike.html) [of](https://www.w3.org/TR/NOTE-AS) [prominent](https://www.xanthir.com/blog/b4K_0) voices in the community.
60
56
 
61
- The underlying theme is this api is meant to make it easy for the developer to do the right thing, by encouraging lazy loading and smaller footprints. It rolls up most all the other observer api's into one, including, potentially, [a selector observer](https://github.com/whatwg/dom/issues/1285), which may be a similar duplicate to [the match-media counterpart proposal](https://github.com/whatwg/dom/issues/1225).
57
+ The underlying theme is that this API is meant to make it easy for developers to do the right thing by encouraging lazy loading and smaller footprints. It rolls up most of the other observer APIs into one, including, potentially, [a selector observer](https://github.com/whatwg/dom/issues/1285), which may be a similar duplicate to [the match-media counterpart proposal](https://github.com/whatwg/dom/issues/1225).
62
58
 
63
59
  ### Finite Element Analysis
64
60
 
@@ -77,41 +73,112 @@ ES module based web components may or may not be the best fit for these applicat
77
73
  A significant pain point has to do with downloading all the third-party web components and/or (progressive) enhancements that these macro components / compositions require, and loading them into memory only when needed.
78
74
 
79
75
 
80
- ### Does this api make the impossible possible?
76
+ ### Does this API make the impossible possible?
81
77
 
82
- There is quite a bit of functionality this proposal would open up, that is exceedingly difficult to polyfill reliably:
78
+ There is quite a bit of functionality this proposal would open up that is exceedingly difficult to polyfill reliably:
83
79
 
84
- 1. It is unclear how to use mutation observers to observe changes to [custom state](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet). The closest thing might be a solution like [this](https://davidwalsh.name/detect-node-insertion), but that falls short for elements that aren't visible, or during template instantiation, and requires carefully constructed "negating" queries if needing to know when the css selector is no longer matching.
80
+ 1. It is unclear how to use mutation observers to observe changes to [custom state](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet). The closest thing might be a solution like [this](https://davidwalsh.name/detect-node-insertion), but that falls short for elements that aren't visible or during template instantiation, and requires carefully constructed "negating" queries if needing to know when the CSS selector is no longer matching.
85
81
 
86
- 2. For simple css matches, like "my-element", or "[name='hello']" it is enough to use a mutation observer, and only observe the elements within the specified DOM region (more on that below). But as CSS has evolved, it is quite easy to think of numerous css selectors that would require us to expand our mutation observer to need to scan the entire Shadow DOM realm, or the entire DOM tree outside any Shadow DOM, for any and all mutations (including attribute changes), and re-evaluate every single element within the specified DOM region for new matches or old matches that no longer match. Things like child selectors, :has, and so on. All this is done, miraculously, by the browser in a performant way. Reproducing this in userland using JavaScript alone, matching the same performance seems impossible.
82
+ 2. For simple CSS matches, like "my-element" or "[name='hello']", it is enough to use a mutation observer and only observe the elements within the specified DOM region (more on that below). But as CSS has evolved, it is quite easy to think of numerous CSS selectors that would require us to expand our mutation observer to scan the entire Shadow DOM realm, or the entire DOM tree outside any Shadow DOM, for any and all mutations (including attribute changes), and re-evaluate every single element within the specified DOM region for new matches or old matches that no longer match. Things like child selectors, :has, and so on. All this is done miraculously by the browser in a performant way. Reproducing this in userland using JavaScript alone while matching the same performance seems impossible.
87
83
 
88
- 3. Knowing when an element, previously being monitored for, passes totally "out-of-scope", so that no more hard references to the element remain. This would allow for cleanup of no longer needed weak references without requiring polling.
84
+ 3. Knowing when an element previously being monitored passes totally "out-of-scope" so that no more hard references to the element remain. This would allow for cleanup of no longer needed weak references without requiring polling.
89
85
 
90
- 4. Some css selectors, such as the [scope donut hole range](https://css-tricks.com/solved-by-css-donuts-scopes/#aa-donut-scoping-with-scope) aren't supported by oEl.querySelectorAll(...) or oEl.matches(...).
86
+ 4. Some CSS selectors, such as the [scope donut hole range](https://css-tricks.com/solved-by-css-donuts-scopes/#aa-donut-scoping-with-scope), aren't supported by oEl.querySelectorAll(...) or oEl.matches(...).
91
87
 
92
- ### Most significant use cases.
88
+ ### Most significant use cases
93
89
 
94
- The amount of code necessary to accomplish these common tasks designed to improve the user experience is significant. Building it into the platform would potentially:
90
+ The amount of code necessary to accomplish these common tasks designed to improve the user experience is significant. Building it into the platform would potentially:
95
91
 
96
- 1. Give the developer a strong signal to do the right thing, by
97
- 1. Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
98
- 2. Supporting "binding from a distance" that can set property values of elements in bulk as the HTML streams in. For example, say a web page is streaming in HTML with thousands of input elements (say a long tax form). We want to have some indication in the head tag of the HTML (for example) to make all the input elements read only as they stream through the page. With css, we could do similar things, for example set the background to red of all input elements. Why can't we do something similar with setting properties like readOnly, disabled, etc? With this api, giving developers the "keys" to css filtering, so they can "mount a campaign" to apply common settings on them all feels like something that almost every web developer has mentally screamed to themselves "why can't I do that?", doesn't it?
99
- 3. Supporting "progressive enhancement" more effectively.
100
- 2. Potentially by allowing the platform to do more work in the low-level (c/c++/rust?) code, without as much context switching into the JavaScript memory space, which may reduce cpu cycles as well. This is done by passing into the API substantial number of conditions, which can all be evaluated at a lower level, before the api needs to surface up to the developer "found one!".
101
- 3. As discussed earlier, to do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
92
+ 1. Give developers a strong signal to do the right thing by:
93
+ 1. Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
94
+ 2. Supporting "binding from a distance" that can set property values of elements in bulk as the HTML streams in. For example, say a web page is streaming in HTML with thousands of input elements (say a long tax form). We want to have some indication in the head tag of the HTML (for example) to make all the input elements read-only as they stream through the page. With CSS, we could do similar things, for example set the background to red of all input elements. Why can't we do something similar with setting properties like readOnly, disabled, etc? With this API, giving developers the "keys" to CSS filtering so they can "mount a campaign" to apply common settings on them all feels like something that almost every web developer has mentally screamed to themselves "why can't I do that?", doesn't it?
95
+ 3. Supporting "progressive enhancement" more effectively.
96
+ 2. Potentially allow the platform to do more work in low-level (C/C++/Rust?) code without as much context switching into the JavaScript memory space, which may reduce CPU cycles as well. This is done by passing a substantial number of conditions into the API, which can all be evaluated at a lower level before the API needs to surface up to the developer "found one!".
97
+ 3. As discussed earlier, to do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has an attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely the platform has found ways to do this more efficiently?
102
98
 
103
99
  The extra flexibility this new primitive would provide could be quite useful to things other than lazy loading of custom elements, such as implementing [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) as well as [binding from a distance](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) in userland.
104
-
105
100
 
106
- ## First use case -- lazy loading custom elements
101
+ ## Quick Examples of the Most Common Use Cases
102
+
103
+ Before getting into the weeds, let's demonstrate the two most prominent use cases:
104
+
105
+ ### Use Case 1: Custom Attribute Enhancement
106
+
107
+ ```html
108
+ <body>
109
+ <div log-to-console="clicked on a div">hello</div>
110
+
111
+ <script type=module>
112
+ import 'mount-observer/ElementMountExtension.js';
113
+ document.body.mount([{
114
+ withAttrs:{base: 'log-to-console'},
115
+ spawn: function(el){
116
+ el.addEventListener('click', e => {
117
+ console.log(e.target.getAttribute('log-to-console'));
118
+ });
119
+ },
120
+ }])
121
+ </script>
122
+ </body>
123
+ ```
124
+
125
+ See [this extending package](https://github.com/bahrus/mount-observer-script-element) that provides for a more declarative approach.
126
+
127
+ ### Use Case 2: Lazy Global Custom Element Definition
107
128
 
108
- To specify the equivalent of what the alternative proposal linked to above would do, we can do the following:
129
+ To specify the equivalent of what the [alternative proposal linked to above would do](https://github.com/WICG/webcomponents/issues/782), we can do the following:
130
+
131
+ ```JavaScript
132
+ // MyElement.js
133
+ export default class MyElement extends HTMLElement {
134
+ connectedCallback() {
135
+ this.textContent = 'Hello!';
136
+ }
137
+ }
138
+
139
+ // main.js
140
+ import 'mount-observer/ElementMountExtension.js';
141
+
142
+ document.mount({
143
+ matching: 'my-element',
144
+ import: './MyElement.js',
145
+ do: 'builtIns.defineCustomElement'
146
+ });
147
+
148
+ // HTML - elements will be upgraded when discovered
149
+ // by the mount observer
150
+ <my-element></my-element>
151
+
152
+ ```
153
+
154
+ This registers custom elements with the global customElements registry.
155
+
156
+ ### Scoped
157
+
158
+ To register the class in the same custom element registry as the element which calls the "mount" method (element in this case), use "builtIns.defineScopedCustomElement":
159
+
160
+ ```JavaScript
161
+ element.mount({
162
+ matching: 'my-element',
163
+ import: './MyElement.js',
164
+ do: 'builtIns.defineScopedCustomElement'
165
+ });
166
+ ```
167
+
168
+
169
+ # Thorough Exposition Begins Here
170
+
171
+ Okay, let's get into the weeds. First, we strongly recommend studying the core package that mount-observer extends, [assign-gingerly](https://www.npmjs.com/package/assign-gingerly).
172
+
173
+ ## First use case -- lazy loading custom elements without sugar coating
174
+
175
+ This registers the custom element in the global registry.
109
176
 
110
177
  ```JavaScript
111
178
  const observer = new MountObserver({
112
179
  select:'my-element', //not supported by this polyfill
113
180
  import: './my-element.js',
114
- do: ({localName}, {modules, observer, mountInit, rootNode}) => {
181
+ do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
115
182
  if(!customElements.get(localName)) {
116
183
  customElements.define(localName, modules[0].MyElement);
117
184
  }
@@ -139,7 +206,7 @@ The "observer" constant above is a class instance that inherits from EventTarget
139
206
  > [!Note]
140
207
  > 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.
141
208
 
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.
209
+ In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but have to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable in some circumstances.
143
210
 
144
211
  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).
145
212
 
@@ -150,9 +217,9 @@ So the developer could use:
150
217
  ```JavaScript
151
218
  const observer = new MountObserver({
152
219
  //supported by this polyfill
153
- whereElementMatches:'my-element',
220
+ matching:'my-element',
154
221
  import: './my-element.js',
155
- do: ({localName}, {modules, observer, mountInit, rootNode}) => {
222
+ do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
156
223
  if(!customElements.get(localName)) {
157
224
  customElements.define(localName, modules[0].MyElement);
158
225
  }
@@ -165,9 +232,10 @@ observer.observe(document);
165
232
 
166
233
  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
234
 
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.
235
+ This polyfill in fact only supports this latter option ("matching"), and leaves "select" for such a time as when a selector observer is available in the platform.
236
+
237
+ [Implemented as Requirement 1](requirements/Done/Requirement1.md).
169
238
 
170
- [Implemented as Requirement 1](requirements/Requirement1.md).
171
239
 
172
240
  ## The import key
173
241
 
@@ -175,16 +243,13 @@ This proposal has been amended to support multiple imports, including of differe
175
243
 
176
244
  ```JavaScript
177
245
  const observer = new MountObserver({
178
- whereElementMatches:'my-element',
246
+ matching:'my-element',
179
247
  import: [
180
248
  ['./my-element-small.css', {type: 'css'}],
181
249
  './my-element.js',
182
250
  ],
183
- do: ({localName}, {modules, observer, mountInit, rootNode}) => {
184
- if(!customElements.get(localName)) {
185
- customElements.define(localName, modules[1].MyElement);
186
- }
187
- observer.disconnectedSignal.abort();
251
+ do: ({localName}, {modules, observer, MountConfig, rootNode}) => {
252
+ ...
188
253
  }
189
254
  });
190
255
  observer.observe(document);
@@ -198,7 +263,7 @@ Previously, this proposal called for allowing arrow functions as well, thinking
198
263
 
199
264
  This proposal would also include support for JSON and HTML module imports (really, all types).
200
265
 
201
- [Implemented as Requirement 1](requirements/Requirement1.md).
266
+ [Implemented as Requirement 1](requirements/Done/Requirement1.md).
202
267
 
203
268
  ## Preemptive downloading
204
269
 
@@ -209,14 +274,14 @@ There are two significant steps to imports, each of which imposes a cost:
209
274
 
210
275
  What if we want to *download* the resource ahead of time, but only load into memory when needed?
211
276
 
212
- The link rel=modulepreload option provides an already existing platform support for this, but the browser complains when no use of the resource is used within a short time span of page load. That doesn't really fit the bill for lazy loading custom elements and other resources.
277
+ The link rel=modulepreload option (and maybe the new defer tc39 proposal) provides an already existing platform support for this, but the browser complains when no use of the resource is used within a short time span of page load. That doesn't really fit the bill for lazy loading custom elements and other resources.
213
278
 
214
279
  So for this we add loadingEagerness:
215
280
 
216
281
  ```JavaScript
217
282
  const observer = new MountObserver({
218
283
  select: 'my-element', //not supported by this polyfill
219
- loadingEagerness: 'eager',
284
+ loadingEagerness: 'eager', //partially supported by this polyfill
220
285
  import: './my-element.js',
221
286
  do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
222
287
  });
@@ -224,18 +289,19 @@ const observer = new MountObserver({
224
289
 
225
290
  So what this does is only check for the presence of an element with tag name "my-element", and it starts downloading the resource, even before the element has "mounted" based on other criteria.
226
291
 
292
+ The polyfill just loads the module into memory right away.
293
+
227
294
  > [!NOTE]
228
295
  > 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.
229
296
 
230
297
  ## Separating JS imperative code from JSON serializable config
231
298
 
232
299
 
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:
300
+ In order to support pure 100% declarative syntax in the passed in MountConfig argument, we need to be able to import the do function. This is done as follows:
235
301
 
236
302
  ```JavaScript
237
303
  //module myActions.js
238
- const doFunction = function({localName}, {modules, observer, mountInit, rootNode}){
304
+ const doFunction = function({localName}, {modules, observer, MountConfig, rootNode}){
239
305
  if(!customElements.get(localName)) {
240
306
  // Find the first exported class constructor from the module
241
307
  const ElementClass = Object.values(modules[0]).find(exp =>
@@ -252,7 +318,7 @@ export {doFunction as do}
252
318
  // observer setup
253
319
 
254
320
  const observer = new MountObserver({
255
- whereElementMatches:'my-element',
321
+ matching:'my-element',
256
322
  import: [
257
323
  './my-element.js',
258
324
  ['./my-element-small.css', {type: 'css'}],
@@ -268,7 +334,7 @@ Here "2" refers to the imported module index ('./myActions.js' in this case).
268
334
 
269
335
  ### How the reference property works
270
336
 
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.
337
+ The `reference` property allows us to call `do` functions from imported modules, enabling 100% JSON-serializable configuration. This is useful when you want to separate imperative code from declarative configuration.
272
338
 
273
339
  **Key behaviors:**
274
340
  - The `reference` property can be a single number or an array of numbers, each referring to an import index
@@ -303,28 +369,28 @@ import: [
303
369
  reference: [2, 3] // Both actions1 and actions2 will have their 'do' called if present
304
370
  ```
305
371
 
306
- [Implemented as [Requirement11](requirements/Requirement11.md)]
372
+ [Implemented as [Requirement11](requirements/Done/Requirement11.md)]
307
373
 
308
- ### Referenced whereInstanceOf
374
+ ### Referenced withInstance
309
375
 
310
- Similar to the `do` function, the `whereInstanceOf` check can also be moved to imported modules for 100% JSON-serializable configuration:
376
+ Similar to the `do` function, the `withInstance` check can also be moved to imported modules for 100% JSON-serializable configuration:
311
377
 
312
378
  ```javascript
313
379
  // module mySettings.js
314
- const doFunction = function({localName}, {modules, observer, mountInit, rootNode}) {
380
+ const doFunction = function({localName}, {modules, observer, MountConfig, rootNode}) {
315
381
  if(!customElements.get(localName)) {
316
382
  customElements.define(localName, modules[1].MyElement);
317
383
  }
318
384
  observer.disconnectedSignal.abort();
319
385
  };
320
386
 
321
- const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
387
+ const withInstance = [HTMLMarqueeElement, SVGElement];
322
388
 
323
- export { doFunction as do, whereInstanceOf };
389
+ export { doFunction as do, withInstance };
324
390
 
325
391
  // my local module
326
392
  const observer = new MountObserver({
327
- whereElementMatches: 'my-element',
393
+ matching: 'my-element',
328
394
  import: [
329
395
  ['./my-element-small.css', {type: 'css'}],
330
396
  './my-element.js',
@@ -336,17 +402,144 @@ observer.observe(document);
336
402
  ```
337
403
 
338
404
  **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
405
+ - **Combining checks**: If both inline `withInstance` and referenced `withInstance` exist, they are AND'd together (element must match both)
406
+ - **Multiple references**: If multiple referenced modules export `withInstance`, the element must match ALL of them (AND logic)
407
+ - **Validation**: Referenced `withInstance` is validated after imports load. Throws an error if not a Constructor or array of Constructors
408
+ - **Optional export**: If a referenced module doesn't export `withInstance`, it's silently ignored
343
409
  - **Timing**:
344
- - With lazy loading (default): Inline `whereInstanceOf` is checked first (before imports), then referenced checks happen after imports load
410
+ - With lazy loading (default): Inline `withInstance` is checked first (before imports), then referenced checks happen after imports load
345
411
  - With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
346
412
 
347
- This optimization ensures that with lazy loading, elements that don't match the inline `whereInstanceOf` won't trigger unnecessary imports.
413
+ This optimization ensures that with lazy loading, elements that don't match the inline `withInstance` won't trigger unnecessary imports.
414
+
415
+ [Implemented as [Requirement12](requirements/Done/Requirement12.md)]
416
+
417
+ ## Simplified API: Array Argument Shorthand
418
+
419
+ For simple use cases where you just want to enhance elements based on attributes without needing the full `MountConfig` object, you can pass an array of [EnhancementConfig` objects](https://github.com/bahrus/assign-gingerly) directly to the constructor:
420
+
421
+ ```JavaScript
422
+ import { MountObserver } from 'mount-observer';
423
+
424
+ // Instead of wrapping in MountConfig:
425
+ // const observer = new MountObserver({
426
+ // enhancementConfig: [config1, config2]
427
+ // });
428
+
429
+ // You can use the shorthand:
430
+ const observer = new MountObserver([
431
+ {
432
+ spawn: Enhancement1,
433
+ enhKey: 'enh1',
434
+ withAttrs: {
435
+ base: 'data-',
436
+ action: '${base}action'
437
+ }
438
+ },
439
+ {
440
+ spawn: Enhancement2,
441
+ enhKey: 'enh2',
442
+ withAttrs: {
443
+ base: 'data-',
444
+ theme: '${base}theme'
445
+ }
446
+ }
447
+ ]);
448
+
449
+ await observer.observe(document.body);
450
+ ```
451
+
452
+ When you pass an array directly:
453
+ - The array is automatically converted to `{ matching: '*', enhancementConfig: [...] }`
454
+ - All elements are considered (matching: '*'), with filtering done by `withAttrs` in each config
455
+ - This is perfect for attribute-based progressive enhancement scenarios
456
+ - You can still use all `EnhancementConfig` features like `spawn`, `withAttrs`, `canSpawn`, etc.
457
+
458
+ This "lite" API makes it easier to do the right thing by reducing boilerplate for common enhancement patterns.
459
+
460
+ [Implemented as ArrayArgument requirement](requirements/Done/ArrayArgument.md).
461
+
462
+ ## Element Mount Extension
463
+
464
+ For even more convenience, you can use the `element.mount()` method to observe elements within their scoped custom element registry context. This is particularly useful with scoped custom element registries (Chrome 146+, latest WebKit/Safari).
465
+
466
+ ```JavaScript
467
+ import 'mount-observer/ElementMountExtension.js';
468
+
469
+ // Mount with MountConfig
470
+ await document.body.mount({
471
+ matching: 'button',
472
+ do: (element) => {
473
+ element.classList.add('enhanced');
474
+ }
475
+ });
476
+
477
+ // Or use the array shorthand directly
478
+ await document.body.mount([
479
+ {
480
+ spawn: ButtonEnhancement,
481
+ enhKey: 'btn-enh',
482
+ withAttrs: {
483
+ base: 'data-',
484
+ action: '${base}action'
485
+ }
486
+ }
487
+ ]);
488
+ ```
489
+
490
+ The `mount()` method:
491
+ - Automatically finds the highest scoped container with the same `customElementRegistry` as the element (default behavior)
492
+ - Creates a `MountObserver` with the provided config
493
+ - Observes the determined scope
494
+ - Returns the element for chaining (as a Promise)
495
+ - Accepts both `MountConfig` objects and `EnhancementConfig[]` arrays
496
+
497
+ Scope options (via `options.scope`):
498
+ - `'registry'` (default): Observes the root registry container (highest element with same customElementRegistry)
499
+ - `'self'`: Observes only this element
500
+ - `'root'`: Observes the root node (document or shadow root)
501
+ - `'shadow'`: Observes the element's shadowRoot (throws error if none exists)
502
+ - `Element`: Observes a custom element you specify
503
+
504
+ This is especially useful for web components that want to observe their own shadow DOM or scoped registry:
505
+
506
+ ```JavaScript
507
+ class MyComponent extends HTMLElement {
508
+ async connectedCallback() {
509
+ const shadow = this.attachShadow({ mode: 'open', registry: new CustomElementRegistry() });
510
+ shadow.innerHTML = `<button data-action="click">Click me</button>`;
511
+
512
+ // Default: Observe within this component's scoped registry
513
+ await shadow.mount([{
514
+ spawn: ButtonHandler,
515
+ enhKey: 'handler',
516
+ withAttrs: { action: 'data-action' }
517
+ }]);
518
+
519
+ // Or observe just the shadow root itself
520
+ await this.mount([{
521
+ spawn: ShadowHandler,
522
+ enhKey: 'shadow'
523
+ }], { scope: 'shadow' });
524
+
525
+ // Or observe the entire document
526
+ await this.mount({
527
+ matching: '.global-button',
528
+ do: (el) => console.log('Global button found')
529
+ }, { scope: 'root' });
530
+ }
531
+ }
532
+ ```
533
+
534
+ Browser support: Works in all browsers, but scoped registry features require Chrome 146+ or latest WebKit/Safari.
535
+
536
+ [Implemented as CustomElementRegistryMounting requirement](requirements/Done/CustomElementRegistryMounting.md).
537
+
538
+
539
+
540
+
541
+
348
542
 
349
- [Implemented as [Requirement12](requirements/Requirement12.md)]
350
543
 
351
544
 
352
545
 
@@ -382,21 +575,21 @@ export { doFunction as do };
382
575
 
383
576
  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
577
 
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.
578
+ But I think it's important to think about this way of making the mount observer declarative, as it provides one significant reason why we place so much emphasis on making sure that the mount observer settings (MountConfig) is as JSON serializable as possible.
386
579
 
387
580
 
388
581
  ## Binding from a distance
389
582
 
390
- It is important to note that "whereElementMatches" is a css query with no restrictions. So something like:
583
+ It is important to note that "matching" (and especially the non polyfillable "select") is a css query with no restrictions. So something like:
391
584
 
392
585
  ```JavaScript
393
586
  import {EvtRt} from 'mount-observer/EvtRt.js';
394
587
 
395
588
  class MyHandler extends EvtRt {
396
- mount(mountedElement, mountInit, context){
589
+ mount(mountedElement, MountConfig, context){
397
590
  mountedElement.textContent = 'hello';
398
591
  }
399
- dismount(mountedElement, mountInit){
592
+ dismount(mountedElement, MountConfig){
400
593
  mountedElement.textContent = 'goodbye';
401
594
  }
402
595
  }
@@ -404,8 +597,8 @@ class MyHandler extends EvtRt {
404
597
  const observer = new MountObserver({
405
598
  // not supported by polyfill
406
599
  //select: 'div > p + p ~ span[class$="name"]'
407
- // is supported:
408
- whereElementMatches: 'div > p + p ~ span[class$="name"]',
600
+ // is supported by polyfill, and even after select is also supported:
601
+ matching: 'div > p + p ~ span[class$="name"]',
409
602
  do: (mountedElement, ctx) => {
410
603
  new MyHandler(mountedElement, ctx);
411
604
  },
@@ -422,16 +615,16 @@ This allows developers to create "stylesheet" like capabilities.
422
615
 
423
616
  ## Registering reusable handlers with MountObserver.define
424
617
 
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:
618
+ To make MountConfig configurations more JSON-serializable and encourage code reuse, you can register handler classes with string names and reference them by name:
426
619
 
427
620
  ```JavaScript
428
621
  import {EvtRt} from 'mount-observer/EvtRt.js';
429
622
 
430
623
  class MyHandler extends EvtRt {
431
- mount(mountedElement, mountInit, context){
624
+ mount(mountedElement, MountConfig, context){
432
625
  mountedElement.textContent = 'hello';
433
626
  }
434
- dismount(mountedElement, mountInit){
627
+ dismount(mountedElement, MountConfig){
435
628
  mountedElement.textContent = 'bye';
436
629
  }
437
630
  }
@@ -441,7 +634,7 @@ MountObserver.define('myHandler', MyHandler);
441
634
 
442
635
  // Reference it by name in the configuration
443
636
  const observer = new MountObserver({
444
- whereElementMatches: 'div > p + p ~ span[class$="name"]',
637
+ matching: 'div > p + p ~ span[class$="name"]',
445
638
  do: 'myHandler' // String reference instead of inline function
446
639
  });
447
640
  observer.observe(document);
@@ -462,7 +655,7 @@ MountObserver.define('logger', LoggerHandler);
462
655
  MountObserver.define('validator', ValidatorHandler);
463
656
 
464
657
  const observer = new MountObserver({
465
- whereElementMatches: 'input',
658
+ matching: 'input',
466
659
  do: [
467
660
  'logger', // Registered handler
468
661
  (element, ctx) => { // Inline function
@@ -486,7 +679,7 @@ When both `do` (with string/array) and `reference` are specified, the execution
486
679
  MountObserver.define('setup', SetupHandler);
487
680
 
488
681
  const observer = new MountObserver({
489
- whereElementMatches: 'button',
682
+ matching: 'button',
490
683
  import: './button-actions.js',
491
684
  reference: 0,
492
685
  do: ['setup', (el) => { el.dataset.ready = 'true'; }]
@@ -534,7 +727,7 @@ MountObserver.define('myHandler', Handler2); // Error: myHandler already in use
534
727
 
535
728
  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
729
 
537
- [Implemented as [Requirement14](requirements/Requirement14.md)]
730
+ [Implemented as [Requirement14](requirements/Done/Requirement14.md)]
538
731
 
539
732
  ### Built in handlers
540
733
 
@@ -547,7 +740,7 @@ const observer = new MountObserver({
547
740
  // not supported by polyfill
548
741
  //select: 'div > p + p ~ span[class$="name"]'
549
742
  // is supported:
550
- whereElementMatches: 'div > p + p ~ span[class$="name"]',
743
+ matching: 'div > p + p ~ span[class$="name"]',
551
744
  do: 'builtIns.logToConsole'
552
745
  });
553
746
  observer.observe(document);
@@ -569,7 +762,7 @@ export default class MyElement extends HTMLElement {
569
762
  import { MountObserver } from 'mount-observer';
570
763
 
571
764
  const observer = new MountObserver({
572
- whereElementMatches: 'my-element',
765
+ matching: 'my-element',
573
766
  import: './MyElement.js',
574
767
  do: 'builtIns.defineCustomElement'
575
768
  });
@@ -587,7 +780,7 @@ For the common use case of setting properties on matching elements, MountObserve
587
780
 
588
781
  ```JavaScript
589
782
  const observer = new MountObserver({
590
- whereElementMatches: 'input',
783
+ matching: 'input',
591
784
  assignOnMount: {
592
785
  disabled: true,
593
786
  value: 'Default value',
@@ -599,7 +792,7 @@ observer.observe(document);
599
792
 
600
793
  This will automatically apply the specified properties to all matching input elements, both existing ones and those added dynamically.
601
794
 
602
- [Implemented as [Requirement2](requirements/Requirement2.md) and [Requirement16](requirements/Requirement16.md)]
795
+ [Implemented as [Requirement2](requirements/Done/Requirement2.md) and [Requirement16](requirements/Done/Requirement16.md)]
603
796
 
604
797
  ### Assigning properties on dismount
605
798
 
@@ -607,7 +800,7 @@ You can also specify properties to apply when elements are removed from the DOM
607
800
 
608
801
  ```JavaScript
609
802
  const observer = new MountObserver({
610
- whereElementMatches: '.status-indicator',
803
+ matching: '.status-indicator',
611
804
  assignOnMount: {
612
805
  '?.style?.color': 'green',
613
806
  '?.dataset?.status': 'active'
@@ -630,7 +823,7 @@ A common use case is providing visual feedback for form validation:
630
823
 
631
824
  ```JavaScript
632
825
  const observer = new MountObserver({
633
- whereElementMatches: 'input.validated',
826
+ matching: 'input.validated',
634
827
  assignOnMount: {
635
828
  '?.style?.borderColor': 'green',
636
829
  '?.style?.backgroundColor': '#f0fff0',
@@ -666,7 +859,7 @@ The `assignGingerly` library supports nested property assignment using the `?.`
666
859
 
667
860
  ```JavaScript
668
861
  const observer = new MountObserver({
669
- whereElementMatches: 'button',
862
+ matching: 'button',
670
863
  assignOnMount: {
671
864
  disabled: false,
672
865
  '?.dataset?.action': 'submit',
@@ -688,7 +881,7 @@ You can combine `assignOn*` with lazy loading to both import resources and set p
688
881
 
689
882
  ```JavaScript
690
883
  const observer = new MountObserver({
691
- whereElementMatches: 'my-element',
884
+ matching: 'my-element',
692
885
  import: './my-element.js',
693
886
  assignOnMount: {
694
887
  theme: 'dark',
@@ -720,7 +913,7 @@ The `MountObserver` class provides a public `assignGingerly()` method that allow
720
913
 
721
914
  ```JavaScript
722
915
  const observer = new MountObserver({
723
- whereElementMatches: 'input',
916
+ matching: 'input',
724
917
  assignOnMount: {
725
918
  disabled: true,
726
919
  value: 'Initial value'
@@ -747,7 +940,7 @@ await observer.assignGingerly({
747
940
 
748
941
  ```JavaScript
749
942
  const observer = new MountObserver({
750
- whereElementMatches: 'input'
943
+ matching: 'input'
751
944
  });
752
945
  observer.observe(document);
753
946
 
@@ -773,7 +966,361 @@ async assignGingerly(config: Record<string, any> | undefined): Promise<void>
773
966
 
774
967
  The method is async because the assign-gingerly library is loaded dynamically when needed.
775
968
 
776
- [Implemented as [Requirement9](requirements/Requirement9.md)]
969
+ [Implemented as [Requirement9](requirements/Done/Requirement9.md)]
970
+
971
+ ## Reversible property assignment with stageOnMount
972
+
973
+ While `assignOnMount` and `assignOnDismount` provide permanent property assignments, sometimes you need temporary changes that automatically reverse when elements dismount. The `stageOnMount` property provides this capability using the `assignTentatively` function from assign-gingerly:
974
+
975
+ ```JavaScript
976
+ const observer = new MountObserver({
977
+ matching: 'button.async-action',
978
+ stageOnMount: {
979
+ disabled: true,
980
+ title: 'Processing...',
981
+ '?.dataset?.loading': 'true'
982
+ }
983
+ });
984
+ observer.observe(document);
985
+ ```
986
+
987
+ When a matching button mounts, these properties are applied. When it dismounts (e.g., loses the `async-action` class), the original values are automatically restored.
988
+
989
+ ### How it works
990
+
991
+ `stageOnMount` uses `assignTentatively` under the hood, which:
992
+
993
+ 1. **Captures original values** before making changes
994
+ 2. **Applies the new properties** when elements mount
995
+ 3. **Automatically reverses** to original values when elements dismount
996
+
997
+ This is different from `assignOnMount`/`assignOnDismount`, where you must explicitly specify both the mount and dismount values.
998
+
999
+ ### When to use stageOnMount vs assignOnMount
1000
+
1001
+ **Use `stageOnMount` when:**
1002
+ - You want temporary state changes that should automatically reverse
1003
+ - The original values matter and should be restored
1004
+ - You're toggling states (disabled/enabled, hidden/visible)
1005
+ - Setting temporary ARIA states or loading indicators
1006
+
1007
+ **Use `assignOnMount`/`assignOnDismount` when:**
1008
+ - You need different values on mount vs dismount (not just reversal)
1009
+ - You want permanent enhancements that shouldn't be reversed
1010
+ - You need explicit control over both mount and dismount behavior
1011
+ - The dismount value is not simply "restore original"
1012
+
1013
+ ### Comparison example
1014
+
1015
+ ```JavaScript
1016
+ // With assignOnMount/assignOnDismount - explicit control
1017
+ const observer1 = new MountObserver({
1018
+ matching: 'input.validated',
1019
+ assignOnMount: {
1020
+ '?.style?.borderColor': 'green'
1021
+ },
1022
+ assignOnDismount: {
1023
+ '?.style?.borderColor': 'red' // Different value, not restoration
1024
+ }
1025
+ });
1026
+
1027
+ // With stageOnMount - automatic reversal
1028
+ const observer2 = new MountObserver({
1029
+ matching: 'button.loading',
1030
+ stageOnMount: {
1031
+ disabled: true, // Automatically restores original disabled state on dismount
1032
+ '?.dataset?.loading': 'true' // Automatically removes on dismount
1033
+ }
1034
+ });
1035
+ ```
1036
+
1037
+ ### Combining with assignOnMount
1038
+
1039
+ You can use both `assignOnMount` and `stageOnMount` together. The order of operations is:
1040
+
1041
+ 1. **On mount**: `assignOnMount` applied first, then `stageOnMount`
1042
+ 2. **On dismount**: `stageOnMount` reversed first, then `assignOnDismount` applied
1043
+
1044
+ ```JavaScript
1045
+ const observer = new MountObserver({
1046
+ matching: 'form',
1047
+ assignOnMount: {
1048
+ noValidate: true // Permanent enhancement
1049
+ },
1050
+ stageOnMount: {
1051
+ '?.dataset?.submitting': 'true' // Temporary state
1052
+ }
1053
+ });
1054
+ ```
1055
+
1056
+ ### Nested properties
1057
+
1058
+ Like `assignOnMount`, `stageOnMount` supports nested property paths:
1059
+
1060
+ ```JavaScript
1061
+ const observer = new MountObserver({
1062
+ matching: '.modal',
1063
+ stageOnMount: {
1064
+ '?.style?.display': 'block',
1065
+ '?.style?.opacity': '1',
1066
+ '?.dataset?.visible': 'true',
1067
+ '?.setAttribute': ['aria-hidden', 'false']
1068
+ }
1069
+ });
1070
+ ```
1071
+
1072
+ ### Re-mounting behavior
1073
+
1074
+ If an element dismounts and then re-mounts, `stageOnMount` will:
1075
+
1076
+ 1. Capture the current values (which may have changed since last mount)
1077
+ 2. Apply the staged properties again
1078
+ 3. Store new reversal information for the next dismount
1079
+
1080
+ ```JavaScript
1081
+ const button = document.querySelector('button');
1082
+ button.disabled = false; // Original state
1083
+
1084
+ button.classList.add('loading'); // Mount: disabled becomes true
1085
+ button.classList.remove('loading'); // Dismount: disabled restored to false
1086
+
1087
+ button.disabled = true; // Manually changed
1088
+ button.classList.add('loading'); // Re-mount: disabled becomes true (staged value)
1089
+ button.classList.remove('loading'); // Dismount: disabled restored to true (the value before re-mount)
1090
+ ```
1091
+
1092
+ ### Performance and memory
1093
+
1094
+ - The assign-gingerly library is only loaded when `stageOnMount` is specified
1095
+ - Reversal objects are stored in a WeakMap, allowing garbage collection when elements are removed
1096
+ - Each element's reversal data is cleaned up when it dismounts
1097
+
1098
+ [Implemented as [Requirement13](requirements/Done/Requirement13.md)]
1099
+
1100
+ ## Spawning enhancements with assign-gingerly integration
1101
+
1102
+ MountObserver integrates with the [assign-gingerly](https://github.com/bahrus/assign-gingerly) enhancement system to automatically spawn enhancement instances when elements mount. This provides a powerful way to attach behaviors and functionality to elements using the enhancement registry pattern.
1103
+
1104
+ ### What is spawn?
1105
+
1106
+ In the assign-gingerly enhancement system, `spawn` is a class constructor (not a boolean) that defines what enhancement instance to create. The `enhancementConfig` object is a registry item that gets registered with the element's enhancement registry.
1107
+
1108
+ ### Basic spawn usage
1109
+
1110
+ ```JavaScript
1111
+ // Define an enhancement class
1112
+ class ButtonEnhancement {
1113
+ constructor(element, ctx, initVals) {
1114
+ this.element = element;
1115
+ this.onClick = this.onClick.bind(this);
1116
+ element.addEventListener('click', this.onClick);
1117
+ }
1118
+
1119
+ onClick(e) {
1120
+ console.log('Button clicked!', this.element);
1121
+ }
1122
+ }
1123
+
1124
+ const observer = new MountObserver({
1125
+ matching: 'button[data-enhance]',
1126
+ enhancementConfig: {
1127
+ spawn: ButtonEnhancement, // The class constructor
1128
+ enhKey: 'buttonEnh'
1129
+ }
1130
+ });
1131
+ observer.observe(document);
1132
+ ```
1133
+
1134
+ When an element mounts, if `enhancementConfig.spawn` is defined, MountObserver will:
1135
+ 1. Import the assign-gingerly object extension module
1136
+ 2. Call `element.enh.get(enhancementConfig, mountContext)` to spawn the enhancement
1137
+ 3. Pass the mount context as the second parameter, making it available to the enhancement constructor
1138
+
1139
+ ### How spawn works
1140
+
1141
+ The spawn feature leverages the `element.enh` property from assign-gingerly, which provides access to the enhancement registry. The `enhancementConfig` is a registry item with this structure:
1142
+
1143
+ ```TypeScript
1144
+ interface IBaseRegistryItem<T> {
1145
+ spawn: {new(element?: Element, ctx?: SpawnContext<T>, initVals?: Partial<T>): T};
1146
+ symlinks?: {[key: symbol]: keyof T};
1147
+ enhKey?: string;
1148
+ withAttrs?: AttrPatterns<T>;
1149
+ canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean;
1150
+ }
1151
+ ```
1152
+
1153
+ When you call `element.enh.get(enhancementConfig, mountContext)`:
1154
+ - If an enhancement matching the config already exists for this element, it returns the existing instance
1155
+ - If no enhancement exists, it creates a new one by calling `new enhancementConfig.spawn(element, ctx, initVals)`
1156
+ - The enhancement is registered in the element's custom element registry's enhancement registry
1157
+ - The mount context is passed to the enhancement constructor via `ctx.mountCtx`
1158
+
1159
+ ### Spawn happens once per element
1160
+
1161
+ The spawn operation only occurs the first time an element mounts. If the element is removed and re-added to the DOM:
1162
+ - The spawn code won't run again (element already in `#processedDoForElement`)
1163
+ - The existing enhancement instance persists with the element
1164
+ - This ensures enhancements are singletons per element instance
1165
+
1166
+ ### Mount context in enhancements
1167
+
1168
+ The mount context passed to spawned enhancements includes:
1169
+
1170
+ ```TypeScript
1171
+ interface MountContext {
1172
+ modules: any[]; // Imported modules (if import was specified)
1173
+ observer: MountObserver; // The MountObserver instance
1174
+ rootNode: Node; // The observed root node
1175
+ MountConfig: MountConfig; // The full configuration object
1176
+ }
1177
+ ```
1178
+
1179
+ This allows enhancements to access imported dependencies, communicate with the observer, and understand their mounting context.
1180
+
1181
+ ### Combining spawn with other features
1182
+
1183
+ Spawn works seamlessly with other MountObserver features:
1184
+
1185
+ ```JavaScript
1186
+ class WidgetEnhancement {
1187
+ constructor(element, ctx, initVals) {
1188
+ this.element = element;
1189
+ this.modules = ctx.mountCtx?.modules || [];
1190
+ console.log('Widget enhanced with', this.modules);
1191
+ }
1192
+
1193
+ theme = 'light';
1194
+ mode = 'default';
1195
+ }
1196
+
1197
+ const observer = new MountObserver({
1198
+ matching: 'my-widget',
1199
+ import: './widget-helpers.js',
1200
+ assignOnMount: {
1201
+ dataset: { initialized: 'true' }
1202
+ },
1203
+ stageOnMount: {
1204
+ disabled: true // Temporarily disable during setup
1205
+ },
1206
+ enhancementConfig: {
1207
+ spawn: WidgetEnhancement,
1208
+ enhKey: 'widget',
1209
+ withAttrs: {
1210
+ base: 'data-config',
1211
+ theme: '${base}-theme',
1212
+ mode: '${base}-mode'
1213
+ }
1214
+ },
1215
+ do: (element, ctx) => {
1216
+ console.log('Additional setup after spawn');
1217
+ }
1218
+ });
1219
+ ```
1220
+
1221
+ **Execution order on mount:**
1222
+ 1. `assignOnMount` properties applied
1223
+ 2. `stageOnMount` properties applied
1224
+ 3. **Spawn enhancement** (if configured)
1225
+ 4. `do` callbacks executed
1226
+ 5. Mount event dispatched
1227
+
1228
+ ### Attribute-based enhancement spawning
1229
+
1230
+ When combined with `withAttrs`, spawn only occurs for elements that have the specified attributes:
1231
+
1232
+ ```JavaScript
1233
+ class ActionEnhancement {
1234
+ constructor(element, ctx, initVals) {
1235
+ this.element = element;
1236
+ this.onClick = this.onClick.bind(this);
1237
+ element.addEventListener('click', this.onClick);
1238
+ }
1239
+
1240
+ onClick(e) {
1241
+ const action = this.element.dataset.action;
1242
+ console.log(`Action: ${action}`);
1243
+ }
1244
+ }
1245
+
1246
+ const observer = new MountObserver({
1247
+ matching: 'button',
1248
+ enhancementConfig: {
1249
+ spawn: ActionEnhancement,
1250
+ enhKey: 'action',
1251
+ withAttrs: {
1252
+ base: 'data-action'
1253
+ }
1254
+ }
1255
+ });
1256
+ ```
1257
+
1258
+ Only buttons with a `data-action` attribute (or `enh-data-action` for custom elements) will have the enhancement spawned.
1259
+
1260
+ ### Guard conditions with canSpawn
1261
+
1262
+ The `canSpawn` property in `enhancementConfig` provides conditional spawning:
1263
+
1264
+ ```JavaScript
1265
+ class InputEnhancement {
1266
+ constructor(element, ctx, initVals) {
1267
+ this.element = element;
1268
+ this.onInput = this.onInput.bind(this);
1269
+ element.addEventListener('input', this.onInput);
1270
+ }
1271
+
1272
+ onInput(e) {
1273
+ console.log('Input changed:', e.target.value);
1274
+ }
1275
+
1276
+ static canSpawn(element) {
1277
+ // Only spawn for inputs that aren't readonly
1278
+ return !element.readOnly;
1279
+ }
1280
+ }
1281
+
1282
+ const observer = new MountObserver({
1283
+ matching: 'input',
1284
+ enhancementConfig: {
1285
+ spawn: InputEnhancement,
1286
+ enhKey: 'inputEnh'
1287
+ }
1288
+ });
1289
+ ```
1290
+
1291
+ If `canSpawn` returns `false`, the enhancement won't be spawned for that element.
1292
+
1293
+ ### Browser compatibility
1294
+
1295
+ The spawn feature requires:
1296
+ - `Element.prototype.customElementRegistry` (Chrome 146+)
1297
+ - `customElementRegistry.enhancementRegistry` (Chrome 146+)
1298
+
1299
+ For older browsers, you'll need to polyfill these features or the spawn functionality won't work. The test suite includes a polyfill example:
1300
+
1301
+ ```JavaScript
1302
+ // Polyfill for browsers without customElementRegistry
1303
+ if (!Element.prototype.hasOwnProperty('customElementRegistry')) {
1304
+ Object.defineProperty(Element.prototype, 'customElementRegistry', {
1305
+ get() {
1306
+ if (!this._customElementRegistry) {
1307
+ this._customElementRegistry = {
1308
+ enhancementRegistry: new BaseRegistry()
1309
+ };
1310
+ }
1311
+ return this._customElementRegistry;
1312
+ }
1313
+ });
1314
+ }
1315
+ ```
1316
+
1317
+ ### Performance considerations
1318
+
1319
+ - The assign-gingerly object extension module is only loaded when `spawn` is configured
1320
+ - Enhancements are created once per element (singleton pattern)
1321
+ - The enhancement registry uses weak references to allow garbage collection
1322
+
1323
+ [Implemented as [SpawnOnMount](requirements/Done/SpawnOnMount.md)]
777
1324
 
778
1325
  ## Emitting events from mounted elements
779
1326
 
@@ -787,7 +1334,7 @@ MountObserver can automatically dispatch custom events from elements when they m
787
1334
 
788
1335
  ```JavaScript
789
1336
  const observer = new MountObserver({
790
- whereElementMatches: 'button[data-action]',
1337
+ matching: 'button[data-action]',
791
1338
  mountedElemEmits: {
792
1339
  event: 'Event',
793
1340
  args: 'custom-ready'
@@ -829,17 +1376,17 @@ mountedElemEmits: {
829
1376
  Use magic strings to inject dynamic values into event data:
830
1377
 
831
1378
  - `{{mountedElement}}` - The element that just mounted
832
- - `{{mountInit}}` - The MountInit configuration object
1379
+ - `{{MountConfig}}` - The MountConfig configuration object
833
1380
 
834
1381
  ```JavaScript
835
1382
  const observer = new MountObserver({
836
- whereElementMatches: 'button[data-test]',
1383
+ matching: 'button[data-test]',
837
1384
  mountedElemEmits: {
838
1385
  event: 'CustomEvent',
839
1386
  args: ['element-mounted', {
840
1387
  detail: {
841
1388
  element: '{{mountedElement}}',
842
- config: '{{mountInit}}'
1389
+ config: '{{MountConfig}}'
843
1390
  }
844
1391
  }]
845
1392
  }
@@ -869,7 +1416,7 @@ Emit multiple events in sequence by providing an array:
869
1416
 
870
1417
  ```JavaScript
871
1418
  const observer = new MountObserver({
872
- whereElementMatches: 'my-component',
1419
+ matching: 'my-component',
873
1420
  mountedElemEmits: [
874
1421
  { event: 'Event', args: 'component-loading' },
875
1422
  { event: 'Event', args: 'component-ready' },
@@ -904,7 +1451,7 @@ Use `oncePerMountedElement` to ensure an event only fires the first time an elem
904
1451
 
905
1452
  ```JavaScript
906
1453
  const observer = new MountObserver({
907
- whereElementMatches: 'button[data-once]',
1454
+ matching: 'button[data-once]',
908
1455
  mountedElemEmits: {
909
1456
  event: 'Event',
910
1457
  args: 'initialized',
@@ -923,7 +1470,7 @@ The event emission logic is code-split into a separate module (`emitEvents.js`)
923
1470
 
924
1471
  ```JavaScript
925
1472
  const observer = new MountObserver({
926
- whereElementMatches: 'my-widget',
1473
+ matching: 'my-widget',
927
1474
  import: './my-widget.js',
928
1475
  mountedElemEmits: [
929
1476
  {
@@ -960,17 +1507,17 @@ document.addEventListener('widget-ready', (e) => {
960
1507
  observer.observe(document);
961
1508
  ```
962
1509
 
963
- [Implemented as [Requirement10](requirements/Requirement10.md)]
1510
+ [Implemented as [Requirement10](requirements/Done/Requirement10.md)]
964
1511
 
965
1512
  ## Element-specific lifecycle notifications with getNotifier
966
1513
 
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.
1514
+ While the MountObserver dispatches lifecycle events (mount, dismount, disconnect) at the observer level, sometimes you need to listen for events specific to a single element. The `getNotifier()` method returns an EventTarget that dispatches filtered events for only that element.
968
1515
 
969
1516
  ### Basic usage
970
1517
 
971
1518
  ```JavaScript
972
1519
  const observer = new MountObserver({
973
- whereElementMatches: 'button',
1520
+ matching: 'button',
974
1521
  do: (mountedElement, {observer}) => {
975
1522
  const notifier = observer.getNotifier(mountedElement);
976
1523
 
@@ -1001,7 +1548,7 @@ This prevents duplicate mount notifications when setting up listeners during the
1001
1548
 
1002
1549
  ```JavaScript
1003
1550
  const observer = new MountObserver({
1004
- whereElementMatches: '#my-button',
1551
+ matching: '#my-button',
1005
1552
  do: (element, {observer}) => {
1006
1553
  const notifier = observer.getNotifier(element);
1007
1554
 
@@ -1020,7 +1567,7 @@ You can call `getNotifier()` at any time, even before an element mounts:
1020
1567
 
1021
1568
  ```JavaScript
1022
1569
  const observer = new MountObserver({
1023
- whereElementMatches: '#future-button'
1570
+ matching: '#future-button'
1024
1571
  });
1025
1572
  observer.observe(document);
1026
1573
 
@@ -1039,32 +1586,6 @@ document.body.appendChild(button);
1039
1586
 
1040
1587
  When the notifier is created before the element mounts, the mount event fires normally.
1041
1588
 
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
1589
  ### Use cases
1069
1590
 
1070
1591
  Element-specific notifiers are useful for:
@@ -1085,7 +1606,7 @@ Element-specific notifiers are useful for:
1085
1606
  getNotifier(element: Element): EventTarget
1086
1607
  ```
1087
1608
 
1088
- [Implemented as [Requirement13](requirements/Requirement13.md)]
1609
+ [Implemented as [Requirement13](requirements/Done/Requirement13.md)]
1089
1610
 
1090
1611
 
1091
1612
  ## Extra lazy loading
@@ -1112,10 +1633,10 @@ Unlike traditional CSS @import, CSS Modules don't support specifying different i
1112
1633
  ```JavaScript
1113
1634
  const observer = new MountObserver({
1114
1635
  select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
1115
- whereMediaMatches: '(max-width: 1250px)',
1636
+ withMediaMatching: '(max-width: 1250px)',
1116
1637
  whereSizeOfContainerMatches: '(min-width: 700px)',
1117
1638
  whereContainerHas: '[itemprop=isActive][value="true"]',
1118
- whereInstanceOf: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
1639
+ withInstance: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
1119
1640
  whereLangIn: ['en-GB'],
1120
1641
  whereConnectionHas:{
1121
1642
  effectiveTypeIn: ["slow-2g"],
@@ -1125,17 +1646,17 @@ const observer = new MountObserver({
1125
1646
  });
1126
1647
  ```
1127
1648
 
1128
- [whereInstanceOf implemented as [Requirement5](requirements/Requirement5.md)]
1649
+ [withInstance implemented as [Requirement5](requirements/Done/Requirement5.md)]
1129
1650
 
1130
- [whereMediaMatches implemented as [Requirement6](requirements/Requirement6.md)]
1651
+ [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
1131
1652
 
1132
1653
  ## InstanceOf checks in detail
1133
1654
 
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.
1655
+ Carving out the special "withInstance" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "confirm" callback logic (discussed later). For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
1135
1656
 
1136
1657
  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.
1137
1658
 
1138
- However, where this support for "whereInstanceOf" would be *most* helpful is when it comes to [*custom enhancements*](https://github.com/WICG/webcomponents/issues/1000) that only wish to lazily layer some heavy lifting functionality on top of certain families of already loaded and upgraded custom elements (possibly in addition to some (specified) built in elements). Here, the lazy loading of the *entire custom **enhancement***, based on the presence in the DOM of a member of the family of custom elements, would, if my calculations are correct, result in providing a significant benefit.
1659
+ However, where this support for "withInstance" would be *most* helpful is when it comes to [*custom enhancements*](https://github.com/WICG/webcomponents/issues/1000) that only wish to lazily layer some heavy lifting functionality on top of certain families of already loaded and upgraded custom elements (possibly in addition to some (specified) built in elements). Here, the lazy loading of the *entire custom **enhancement***, based on the presence in the DOM of a member of the family of custom elements, would, if my calculations are correct, result in providing a significant benefit.
1139
1660
 
1140
1661
 
1141
1662
  <!--
@@ -1250,7 +1771,7 @@ So I believe the prudent thing to do is wait for all the conditions to be satisf
1250
1771
 
1251
1772
  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.
1252
1773
 
1253
- [Implemented with [Requirement6](requirements/Requirement6.md)]
1774
+ [Implemented with [Requirement6](requirements/Done/Requirement6.md)]
1254
1775
 
1255
1776
 
1256
1777
  ## Support for "donut hole scoping"
@@ -1275,8 +1796,8 @@ We want to find all elements with attribute itemprop outside any itemscope, so t
1275
1796
  ```JavaScript
1276
1797
  const oContainerNode = document.getElementById('myTest');
1277
1798
  const observer = new MountObserver({
1278
- whereElementMatches:'[itemprop]',
1279
- whereOutside: '[itemscope]'
1799
+ matching:'[itemprop]',
1800
+ withScopePerimeter: '[itemscope]'
1280
1801
  do: ({localName}, {modules, observer}) => {
1281
1802
  ...
1282
1803
  },
@@ -1285,11 +1806,11 @@ const observer = new MountObserver({
1285
1806
  observer.observe(oContainerNode);
1286
1807
  ```
1287
1808
 
1288
- The check for "whereOutside" is done via script:
1809
+ The check for "withScopePerimeter" is done via script:
1289
1810
 
1290
1811
  ```JavaScript
1291
- import {whereOutside} from 'mount-observer/whereOutside.js';
1292
- whereOutside(oContainerNode: Node, matchCandidate: Element, outside: string){
1812
+ import {withScopePerimeter} from 'mount-observer/withScopePerimeter.js';
1813
+ withScopePerimeter(oContainerNode: Node, matchCandidate: Element, outside: string){
1293
1814
  let current = matchCandidate.parentElement;
1294
1815
 
1295
1816
  while (current && current !== oContainerNode) {
@@ -1304,308 +1825,7 @@ whereOutside(oContainerNode: Node, matchCandidate: Element, outside: string){
1304
1825
 
1305
1826
  ```
1306
1827
 
1307
- [Implemented as [Requirement7](requirements/Requirement7.md)]
1308
-
1309
- ## A tribute to attributes
1310
-
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.
1312
-
1313
- The MountObserver API provides explicit support for monitoring attributes. There are two primary reasons for why it is important to provide this as part of the API:
1314
-
1315
- Being that for both custom elements, as well as (hopefully) [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) we need to carefully work with sets of "owned" [observed](https://github.com/WICG/webcomponents/issues/1045) attributes, and in some cases we may need to manage combinations of prefixes and suffixes for better name-spacing management, creating the most effective css query becomes challenging.
1316
-
1317
- We want to be alerted by the discovery of elements adorned by these attributes, but then continue to be alerted to changes of their values, and we can't enumerate which values we are interested in, so we must subscribe to all values as they change.
1318
-
1319
- ## Attributes of attributes
1320
-
1321
- I think it is useful to divide [attributes](https://jakearchibald.com/2024/attributes-vs-properties/) that we would want to observe into two categories:
1322
-
1323
- 1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of truth" for key features of the DOM element itself. We will refer to these attributes as "Source of Truth" attributes. Please don't read too much into the name. Whether the platform or custom element author developer chooses to make properties reflect to attributes, or attributes reflect to the properties, or some hybrid of some sort, is immaterial here.
1324
-
1325
- By invariably named, I mean the name will be the same in all Shadow DOM realms.
1326
-
1327
- Examples are many built-in global attributes, like lang, or contenteditable, or more specialized examples such as "content" for the meta tag. It could also include attributes of third party custom elements we want to enhance in a cross-cutting way.
1328
-
1329
- I think in the vast majority of cases, setting the property values corresponding to these attributes results in directly reflecting those property values to the attributes (and vice versa). There are exceptions, especially for non-string attributes like the checked property of the input element / type=checkbox, and JSON based attributes for custom elements.
1330
-
1331
- Usually, there are no events we can subscribe to in order to know when the property changes. Hijacking the property setter in order to observe changes may not always work or feel very resilient. So monitoring the attribute value associated with the property is often the most effective way of observing when the property/attribute state for these elements change. And some attributes (like the microdata attributes such as itemprop) don't even have properties that they pair with!
1332
-
1333
-
1334
- 2. In contrast, there are scenarios where we want to support somewhat fluid, renamable attributes within different Shadow DOM realms, which add behavior/enhancement capabilities on top of built-in or third party custom elements. We'll refer to these attributes as "Enhancement Attributes."
1335
-
1336
- We want our api to be able to distinguish between these two, and to be able to combine both types in one mount observer instance's set of observed attributes.
1337
-
1338
- > [!NOTE]
1339
- > The most important reason for pointing out this distinction is this: "Source of Truth" attributes will only be *observed*, and will **not** trigger mount/unmount states unless they are part of the "on" selector string. And unlike all the other "where" conditions this proposal supports, the where clauses for the "Enhancement Attributes" are "one-way" -- they trigger a "mount" event / callback, followed by the ability to observe the stream of changes (including removal of those attributes), but they never trigger a "dismount".
1340
-
1341
- ### Counterpoint
1342
-
1343
- Does it make sense to even support "Source of Truth" attributes in a "MountObserver" api, if they have no impact on mounted state?
1344
-
1345
- We think it does, because some Enhancement Attributes will need to work in conjunction with Source of Truth attributes, in order to provide the observer a coherent picture of the full state of the element.
1346
-
1347
- This realization (hopefully correct) struck me while trying to implement a [userland implementation](https://github.com/bahrus/be-intl) of [this proposal](https://github.com/whatwg/html/issues/9294).
1348
-
1349
-
1350
- ### Source of Truth Attributes
1351
-
1352
- Let's focus on the first scenario. It doesn't make sense to use the word "where" for these, because we don't want these attributes to affect our mount/dismount state
1353
-
1354
- ```JavaScript
1355
- import {MountObserver} from 'mount-observer/MountObserver.js';
1356
- const mo = new MountObserver({
1357
- select: '*',
1358
- observedAttrsWhenMounted: ['lang', 'contenteditable']
1359
- });
1360
-
1361
- mo.addEventListener('attrchange', e => {
1362
- console.log(e);
1363
- // {
1364
- // mountedElement,
1365
- // attrChangeInfo:[{
1366
- // idx: 0,
1367
- // name: 'lang'
1368
- // isSOfTAttr: true,
1369
- // oldValue: null,
1370
- // newValue: 'en-GB',
1371
- // }]
1372
- // }
1373
- });
1374
- ```
1375
-
1376
- ### Help with parsing?
1377
-
1378
- This proposal is likely to evolve going forward, attempting to synthesize [separate ideas](https://github.com/WICG/webcomponents/issues/1045) for declaratively specifying how to interpret the attributes, parsing them so that they may be merged into properties of a class instance.
1379
-
1380
- But for now, such support is not part of this proposal (though we can see a glimpse of what that support might look like below).
1381
-
1382
- ### Custom Enhancements in userland
1383
-
1384
- [This proposal, support for (progressive) enhancement of built-in or third-party custom elements, could take quite a while to see the light of day, if ever](https://github.com/WICG/webcomponents/issues/1000).
1385
-
1386
- In the meantime, we want to provide the most help for providing for custom enhancements in userland, and for any other kind of (progressive) enhancement based on (server-rendered) attributes going forward.
1387
-
1388
- Suppose we have a (progressive) enhancement that we want to apply based on the presence of 1 or more attributes.
1389
-
1390
- To make this discussion concrete, let's suppose the "canonical" names of those attributes are:
1391
-
1392
- ```html
1393
- <div id=div>
1394
- <section
1395
- my-enhancement=greetings
1396
- my-enhancement-first-aspect=hello
1397
- my-enhancement-second-aspect=goodbye
1398
- my-enhancement-first-aspect-wow-this-is-deep
1399
- my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
1400
- ></section>
1401
- </div>
1402
- ```
1403
-
1404
- Now suppose we are worried about namespace clashes, plus we want to serve environments where HTML5 compliance is a must.
1405
-
1406
- So we also want to recognize additional attributes that should map to these canonical attributes:
1407
-
1408
- We want to also support:
1409
-
1410
- ```html
1411
- <div id=div>
1412
- <section class=hello
1413
- data-my-enhancement=greetings
1414
- data-my-enhancement-first-aspect=hello
1415
- data-my-enhancement-second-aspect=goodbye
1416
- data-my-enhancement-first-aspect-wow-this-is-deep
1417
- data-my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
1418
- ></section>
1419
- </div>
1420
- ```
1421
-
1422
- Based on the current unspoken rules, no one will raise an eyebrow with these attributes, because the platform has indicated it will generally avoid dashes in attributes (with an exception or two that will only happen in a blue moon, like aria-*).
1423
-
1424
- But now when we consider applying this enhancement to third party custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement?
1425
-
1426
- So let's say we want to insist that on custom elements, we must have the data- prefix?
1427
-
1428
- And we want to support an alternative, more semantic sounding prefix to data, say enh-*, endorsed by [this proposal](https://github.com/WICG/webcomponents/issues/1000).
1429
-
1430
- Here's what the api **doesn't** provide (as originally proposed):
1431
-
1432
- #### The carpal syndrome syntax
1433
-
1434
- Using the same expression structure as above, we would end up with this avalanche of settings:
1435
-
1436
- ```JavaScript
1437
- import {MountObserver} from '../MountObserver.js';
1438
- const mo = new MountObserver({
1439
- select: '*',
1440
- whereAttr:{
1441
- isIn: [
1442
- 'data-my-enhancement',
1443
- 'data-my-enhancement-first-aspect',
1444
- 'data-my-enhancement-second-aspect',
1445
- 'enh-my-enhancement',
1446
- 'enh-my-enhancement-first-aspect',
1447
- 'enh-my-enhancement-second-aspect',
1448
- //...some ten more combinations not listed
1449
- {
1450
- name: 'my-enhancement',
1451
- builtIn: true
1452
- },
1453
- {
1454
- name: 'my-enhancement-first-aspect',
1455
- builtIn: true
1456
- },
1457
- {
1458
- name: 'my-enhancement-second-aspect',
1459
- builtIn: true
1460
- },
1461
- ...
1462
- ]
1463
-
1464
- }
1465
- });
1466
- ```
1467
-
1468
- #### The DRY Way
1469
-
1470
- This seems like a much better approach, and is supported by this proposal:
1471
-
1472
- ```JavaScript
1473
- import {MountObserver} from '../MountObserver.js';
1474
- const mo = new MountObserver({
1475
- whereAttr:{
1476
- hasRootIn: ['data', 'enh', 'data-enh'],
1477
- hasBase: 'my-enhancement',
1478
- hasBranchIn: ['first-aspect', 'second-aspect', ''],
1479
- hasLeafIn: {
1480
- 'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
1481
- }
1482
- }
1483
- });
1484
- ```
1485
-
1486
- MountObserver provides a breakdown of the matching attribute when encountered:
1487
-
1488
- ```html
1489
- <div id=div>
1490
- <section class=hello my-enhancement-first-aspect-wow-this-is-deep="hello"></section>
1491
- </div>
1492
- <script type=module>
1493
- import {MountObserver} from '../MountObserver.js';
1494
- const mo = new MountObserver({
1495
- select: '*',
1496
- whereAttr:{
1497
- hasRootIn: ['data', 'enh', 'data-enh'],
1498
- hasBase: 'my-enhancement',
1499
- hasBranchIn: ['first-aspect', 'second-aspect', ''],
1500
- hasLeafIn: {
1501
- 'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
1502
- }
1503
- }
1504
- });
1505
- mo.addEventListener('attrChange', e => {
1506
- console.log(e);
1507
- // {
1508
- // mountedElement,
1509
- // attrChangeInfo:[{
1510
- // idx: 0,
1511
- // oldValue: null,
1512
- // newValue: 'good-bye',
1513
- // parts:{
1514
- // name: 'data-my-enhancement-first-aspect-wow-this-is-deep'
1515
- // root: 'data',
1516
- // base: 'my-enhancement',
1517
- // branch: 'first-aspect',
1518
- // leaf: 'wow-this-is-deep',
1519
- // }
1520
- // }]
1521
- // }
1522
- });
1523
- mo.observe(div);
1524
- setTimeout(() => {
1525
- const myCustomElement = document.querySelector('my-custom-element');
1526
- myCustomElement.setAttribute('data-my-enhancement-first-aspect-wow-this-is-deep', 'good-bye');
1527
- }, 1000);
1528
- </script>
1529
- ```
1530
-
1531
- Some libraries prefer to use the colon (:) rather than a dash to separate these levels of settings:
1532
-
1533
- Possibly some libraries may prefer to mix it up a bit:
1534
-
1535
-
1536
- ```html
1537
- <div id=div>
1538
- <section class=hello
1539
- data-my-enhancement=greetings
1540
- data-my-enhancement:first-aspect=hello
1541
- data-my-enhancement:second-aspect=goodbye
1542
- data-my-enhancement:first-aspect--wow-this-is-deep
1543
- data-my-enhancement:first-aspect--have-you-considered-using-json-for-this=just-saying
1544
- ></section>
1545
- </div>
1546
- ```
1547
-
1548
- An example of this in the real world can be found with [HTMX](https://htmx.org/docs/#hx-on):
1549
-
1550
- ```html
1551
- <button hx-post="/example"
1552
- hx-select:htmx:config-request="event.detail.parameters.example = 'Hello Scripting!'">
1553
- Post Me!
1554
- </button>
1555
- ```
1556
-
1557
- To support such syntax, specify the delimiters thusly:
1558
-
1559
- ```JavaScript
1560
- const mo = new MountObserver({
1561
- select: '*',
1562
- whereAttr:{
1563
- hasRootIn: ['data', 'enh', 'data-enh'],
1564
- hasBase: ['-', 'my-enhancement'],
1565
- hasBranchIn: [':', ['first-aspect', 'second-aspect', '']],
1566
- hasLeafIn: {
1567
- 'first-aspect': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
1568
- }
1569
- }
1570
- });
1571
- ```
1572
-
1573
- ## Supporting userland security protections
1574
-
1575
- As we saw with the HTMX example above, element enhancement libraries that (progressively) enhance server rendered HTML are finding it necessary to support inline event handling. Since the platform has provided no support for hashing built-in event handlers, there's no real advantage for these libraries to utilize the built-in event handlers, so might as well create bespoke event handlers, which unfortunately might not be detected by browser security mechanisms. Perhaps some of these libraries only enable that functionality after confirming no such CSP rules are in place, or provide console warnings, who knows? This reminds me of the plausible (but probably not universally held) belief that illegalizing relatively safe recreational drugs like hashish or beer pushes the illegal market to gravitate towards drugs/beverages which have more "bang for the buck", which are considerably less safe, leading to the conclusion that the "health and safety" laws end up causing more harm than good.
1576
-
1577
- I am personally pursuing a [userland implementation of CSP tailored for attributes](https://github.com/bahrus/be-hashing-out). What I'm finding necessary to support this is a way to quickly determine *the full list of* attributes a particular enhancement is monitoring for.
1578
-
1579
- Thus the mountObserver does provide that information to the consumer as well:
1580
-
1581
- ```JavaScript
1582
- const mo = new MountObserver({
1583
- select: '*',
1584
- whereAttr:{
1585
- hasRootIn: ['data', 'enh', 'data-enh'],
1586
- hasBase: ['-', 'my-enhancement'],
1587
- hasBranchIn: [':', ['first-aspect', 'second-aspect', '']],
1588
- hasLeafIn: {
1589
- 'first-aspect': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
1590
- }
1591
- }
1592
- });
1593
- const observedAttributes = await mo.observedAttrs();
1594
- ```
1595
-
1596
- ## Resolving ambiguity
1597
-
1598
- Because we want the multiple root values (enh-*, data-enh-*, *) to be treated as equivalent, from a developer point of view, we have a possible ambiguity -- what if more than one root is present for the same base, branch and leaf? Which value prevails over the others?
1599
-
1600
- Tentative rules:
1601
-
1602
- 1. Roots must differ in length.
1603
- 2. If one value is null (attribute not present) and the other a string, the one with the string value prevails.
1604
- 3. If two or more equivalent attributes have string values, the one with the longer root prevails.
1605
-
1606
- The thinking here is that longer roots indicate higher "specificity", so it is safer to use that one.
1607
-
1608
-
1828
+ [Implemented as [Requirement7](requirements/Done/Requirement7.md)]
1609
1829
 
1610
1830
  ## Intra document html imports
1611
1831
 
@@ -1839,7 +2059,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
1839
2059
  <template mount='{
1840
2060
  "select": ":not([defer-loading])",
1841
2061
  "loadingEagerness": "eager",
1842
- "whereMediaMatches": "(min-width: 700px)",
2062
+ "withMediaMatching": "(min-width: 700px)",
1843
2063
  "whereLangIn": ["en-GB"],
1844
2064
  }'>
1845
2065
  <div>I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot></div>
@@ -1848,7 +2068,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
1848
2068
  <template mount='{
1849
2069
  "select": ":not([defer-loading])",
1850
2070
  "loadingEagerness": "lazy",
1851
- "whereMediaMatches": "(max-width: 700px)",
2071
+ "withMediaMatching": "(max-width: 700px)",
1852
2072
  "whereLangIn": ["fr"],
1853
2073
  }'>
1854
2074
  <div>Je ne sais pas pourquoi tu dis <slot name=slot2></slot> je dis <slot name=slot1></slot></div>