mount-observer 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DefineCustomElementHandler.js +37 -3
- package/DefineCustomElementHandler.ts +43 -4
- package/ElementMountExtension.js +45 -0
- package/ElementMountExtension.ts +61 -0
- package/Events.js +18 -29
- package/Events.ts +7 -16
- package/EvtRt.js +14 -14
- package/EvtRt.ts +15 -15
- package/MountObserver.js +240 -111
- package/MountObserver.ts +283 -131
- package/README.md +667 -447
- package/emitEvents.js +13 -13
- package/emitEvents.ts +14 -14
- package/getRootRegistryContainer.js +27 -0
- package/getRootRegistryContainer.ts +32 -0
- package/index.js +2 -3
- package/index.ts +4 -5
- package/loadImports.js +1 -1
- package/loadImports.ts +1 -1
- package/mediaQuery.js +5 -5
- package/mediaQuery.ts +7 -7
- package/package.json +5 -9
- package/playwright.config.ts +8 -8
- package/types.d.ts +22 -46
- package/{whereOutside.js → withScopePerimeter.js} +1 -1
- package/{whereOutside.ts → withScopePerimeter.ts} +1 -1
- package/attrChanges.js +0 -70
- package/attrChanges.ts +0 -90
- package/attrCoordinates.js +0 -93
- package/attrCoordinates.ts +0 -122
- package/whereAttr.js +0 -174
- package/whereAttr.ts +0 -221
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
|
-
- ✅ **
|
|
14
|
-
- ✅ **
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
- ✅ **
|
|
35
|
-
- ✅ **
|
|
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
|
|
43
|
+
# The MountObserver API
|
|
48
44
|
|
|
49
|
-
Author:
|
|
45
|
+
Author: Bruce B. Anderson (with valuable feedback from @doeixd)
|
|
50
46
|
|
|
51
|
-
Issues /
|
|
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).
|
|
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
|
|
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
|
|
76
|
+
### Does this API make the impossible possible?
|
|
81
77
|
|
|
82
|
-
There is quite a bit of functionality this proposal would open up
|
|
78
|
+
There is quite a bit of functionality this proposal would open up that is exceedingly difficult to polyfill reliably:
|
|
83
79
|
|
|
84
|
-
1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
97
|
-
1.
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
100
|
-
2.
|
|
101
|
-
3.
|
|
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
|
-
##
|
|
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,
|
|
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
|
|
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
|
-
|
|
220
|
+
matching:'my-element',
|
|
154
221
|
import: './my-element.js',
|
|
155
|
-
do: ({localName}, {modules, observer,
|
|
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 ("
|
|
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
|
-
|
|
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,
|
|
184
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
374
|
+
### Referenced withInstance
|
|
309
375
|
|
|
310
|
-
Similar to the `do` function, the `
|
|
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,
|
|
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
|
|
387
|
+
const withInstance = [HTMLMarqueeElement, SVGElement];
|
|
322
388
|
|
|
323
|
-
export { doFunction as do,
|
|
389
|
+
export { doFunction as do, withInstance };
|
|
324
390
|
|
|
325
391
|
// my local module
|
|
326
392
|
const observer = new MountObserver({
|
|
327
|
-
|
|
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 `
|
|
340
|
-
- **Multiple references**: If multiple referenced modules export `
|
|
341
|
-
- **Validation**: Referenced `
|
|
342
|
-
- **Optional export**: If a referenced module doesn't export `
|
|
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 `
|
|
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 `
|
|
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 (
|
|
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 "
|
|
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,
|
|
589
|
+
mount(mountedElement, MountConfig, context){
|
|
397
590
|
mountedElement.textContent = 'hello';
|
|
398
591
|
}
|
|
399
|
-
dismount(mountedElement,
|
|
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
|
-
|
|
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
|
|
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,
|
|
624
|
+
mount(mountedElement, MountConfig, context){
|
|
432
625
|
mountedElement.textContent = 'hello';
|
|
433
626
|
}
|
|
434
|
-
dismount(mountedElement,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `{{
|
|
1379
|
+
- `{{MountConfig}}` - The MountConfig configuration object
|
|
833
1380
|
|
|
834
1381
|
```JavaScript
|
|
835
1382
|
const observer = new MountObserver({
|
|
836
|
-
|
|
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: '{{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1636
|
+
withMediaMatching: '(max-width: 1250px)',
|
|
1116
1637
|
whereSizeOfContainerMatches: '(min-width: 700px)',
|
|
1117
1638
|
whereContainerHas: '[itemprop=isActive][value="true"]',
|
|
1118
|
-
|
|
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
|
-
[
|
|
1649
|
+
[withInstance implemented as [Requirement5](requirements/Done/Requirement5.md)]
|
|
1129
1650
|
|
|
1130
|
-
[
|
|
1651
|
+
[withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
|
|
1131
1652
|
|
|
1132
1653
|
## InstanceOf checks in detail
|
|
1133
1654
|
|
|
1134
|
-
Carving out the special "
|
|
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 "
|
|
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
|
-
|
|
1279
|
-
|
|
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 "
|
|
1809
|
+
The check for "withScopePerimeter" is done via script:
|
|
1289
1810
|
|
|
1290
1811
|
```JavaScript
|
|
1291
|
-
import {
|
|
1292
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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>
|