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