mount-observer 0.1.2 → 0.1.4
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 +212 -42
- package/MountObserver.ts +254 -44
- package/README.md +519 -185
- package/SharedMutationObserver.js +9 -6
- package/SharedMutationObserver.ts +11 -8
- package/arr.js +13 -0
- package/arr.ts +13 -0
- package/emitEvents.js +1 -1
- package/emitEvents.ts +1 -1
- package/index.js +13 -1
- package/index.ts +10 -1
- package/loadImports.js +2 -1
- package/loadImports.ts +2 -1
- package/mediaQuery.js +2 -7
- package/mediaQuery.ts +2 -7
- package/package.json +15 -3
- package/types.d.ts +21 -15
package/README.md
CHANGED
|
@@ -28,7 +28,8 @@ The following features have been implemented and tested:
|
|
|
28
28
|
|
|
29
29
|
### Advanced Features
|
|
30
30
|
- ✅ **Dynamic imports**: Lazy loading of JavaScript modules
|
|
31
|
-
- ✅ **
|
|
31
|
+
- ✅ **assignOnMount**: Property assignment when elements mount
|
|
32
|
+
- ✅ **assignOnDismount**: Property assignment when elements dismount
|
|
32
33
|
- ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
|
|
33
34
|
- ✅ **map configuration**: Metadata mapping for attribute coordinates
|
|
34
35
|
- ✅ **once option**: Fire attrchange event only once per attribute
|
|
@@ -108,9 +109,9 @@ To specify the equivalent of what the alternative proposal linked to above would
|
|
|
108
109
|
|
|
109
110
|
```JavaScript
|
|
110
111
|
const observer = new MountObserver({
|
|
111
|
-
select:'my-element',
|
|
112
|
+
select:'my-element', //not supported by this polyfill
|
|
112
113
|
import: './my-element.js',
|
|
113
|
-
do: ({localName}, {modules, observer,
|
|
114
|
+
do: ({localName}, {modules, observer, mountInit, rootNode}) => {
|
|
114
115
|
if(!customElements.get(localName)) {
|
|
115
116
|
customElements.define(localName, modules[0].MyElement);
|
|
116
117
|
}
|
|
@@ -121,7 +122,7 @@ const observer = new MountObserver({
|
|
|
121
122
|
observer.observe(document);
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
The do function will *only be called once per matching element* -- i.e. if the element stops matching the "select" criteria, then matches again, the do function won't be called again. It will be called for all elements when they match within the scope passed in to the observe method. However, the events discussed below,
|
|
125
|
+
The do function will *only be called once per matching element* -- i.e. if the element stops matching the "select" criteria, then matches again, the do function won't be called again. It will be called for all elements when they match within the scope passed in to the observe method. However, the events discussed below, will continue to be called repeatedly.
|
|
125
126
|
|
|
126
127
|
The constructor argument can also be an array of objects that fit the pattern shown above.
|
|
127
128
|
|
|
@@ -138,19 +139,20 @@ The "observer" constant above is a class instance that inherits from EventTarget
|
|
|
138
139
|
> [!Note]
|
|
139
140
|
> Reading through the historical links tied to the selector-observer proposal this proposal helped spawn, I may have painted an overly optimistic picture of [what the platform is capable of](https://github.com/whatwg/dom/issues/398). It does leave me a little puzzled why this isn't an issue when it comes to styling, and also if some of the advances that were utilized to support :has could be applied to this problem space, so that maybe the arguments raised there have weakened. Even if the concerns raised are as relevant today, I think considering the use cases this proposal envisions, that the objections could be overcome, for the following reasons: 1. For scenarios where lazy loading is the primary objective, "bunching" multiple DOM mutations together and only reevaluating when things are quite idle is perfectly reasonable. Also, for binding from a distance, most of the mutations that need responding to quickly will be when the *state of the host* changes, so DOM mutations play a somewhat muted role in that regard. Again, bunching multiple DOM mutations together, even if adds a bit of a delay, also seems reasonable. I also think the platform could add an "analysis" step to look at the query and categorize it as "simple" queries vs complex. Selector queries that are driven by the characteristics of the element itself (localName, attributes, etc) could be handled in a more expedited fashion. Those that the platform does expect to require more babysitting could be monitored for less vigilantly. Maybe in the latter case, a console.warning could be emitted during initialization. The other use case, for lazy loading custom elements and custom enhancements based on attributes, I think most of the time this would fit the "simple" scenario, so again there wouldn't be much of an issue.
|
|
140
141
|
|
|
141
|
-
In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but has to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable.
|
|
142
|
+
In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but has to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable in some circumstances.
|
|
142
143
|
|
|
143
|
-
If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "select" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support, maybe even after the browser vendors
|
|
144
|
+
If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "select" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support, maybe even after the browser vendors provide a selector-observer (if ever).
|
|
144
145
|
|
|
145
146
|
So the developer could use:
|
|
146
147
|
|
|
147
|
-
## Polyfill Supported
|
|
148
|
+
## Polyfill Supported Mount Observer
|
|
148
149
|
|
|
149
150
|
```JavaScript
|
|
150
151
|
const observer = new MountObserver({
|
|
152
|
+
//supported by this polyfill
|
|
151
153
|
whereElementMatches:'my-element',
|
|
152
154
|
import: './my-element.js',
|
|
153
|
-
do: ({localName}, {modules, observer,
|
|
155
|
+
do: ({localName}, {modules, observer, mountInit, rootNode}) => {
|
|
154
156
|
if(!customElements.get(localName)) {
|
|
155
157
|
customElements.define(localName, modules[0].MyElement);
|
|
156
158
|
}
|
|
@@ -163,7 +165,7 @@ observer.observe(document);
|
|
|
163
165
|
|
|
164
166
|
and could perhaps expect faster binding as a result of the more limited supported expressions. Since "select" is not specified, it is assumed to be "*".
|
|
165
167
|
|
|
166
|
-
This polyfill in fact only supports this latter option ("
|
|
168
|
+
This polyfill in fact only supports this latter option ("whereElementMatches"), and leaves "select" for such a time as when a selector observer is available in the platform.
|
|
167
169
|
|
|
168
170
|
[Implemented as Requirement 1](requirements/Requirement1.md).
|
|
169
171
|
|
|
@@ -173,12 +175,12 @@ This proposal has been amended to support multiple imports, including of differe
|
|
|
173
175
|
|
|
174
176
|
```JavaScript
|
|
175
177
|
const observer = new MountObserver({
|
|
176
|
-
|
|
178
|
+
whereElementMatches:'my-element',
|
|
177
179
|
import: [
|
|
178
180
|
['./my-element-small.css', {type: 'css'}],
|
|
179
181
|
'./my-element.js',
|
|
180
182
|
],
|
|
181
|
-
do: ({localName}, {modules, observer}) => {
|
|
183
|
+
do: ({localName}, {modules, observer, mountInit, rootNode}) => {
|
|
182
184
|
if(!customElements.get(localName)) {
|
|
183
185
|
customElements.define(localName, modules[1].MyElement);
|
|
184
186
|
}
|
|
@@ -213,7 +215,7 @@ So for this we add loadingEagerness:
|
|
|
213
215
|
|
|
214
216
|
```JavaScript
|
|
215
217
|
const observer = new MountObserver({
|
|
216
|
-
select: 'my-element',
|
|
218
|
+
select: 'my-element', //not supported by this polyfill
|
|
217
219
|
loadingEagerness: 'eager',
|
|
218
220
|
import: './my-element.js',
|
|
219
221
|
do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
|
|
@@ -225,130 +227,368 @@ So what this does is only check for the presence of an element with tag name "my
|
|
|
225
227
|
> [!NOTE]
|
|
226
228
|
> As a result of the google IO 2024 talks, I became aware that there is some similarity between this proposal and the [speculation rules api](https://developer.chrome.com/blog/speculation-rules-improvements). This motivated the change to the property from "loading" to loadingEagerness above.
|
|
227
229
|
|
|
230
|
+
## Separating JS imperative code from JSON serializable config
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
In order to support pure 100% declarative syntax in the passed in mountInit argument, we need to be able to import the do function. This is done as follows:
|
|
235
|
+
|
|
236
|
+
```JavaScript
|
|
237
|
+
//module myActions.js
|
|
238
|
+
const doFunction = function({localName}, {modules, observer, mountInit, rootNode}){
|
|
239
|
+
if(!customElements.get(localName)) {
|
|
240
|
+
// Find the first exported class constructor from the module
|
|
241
|
+
const ElementClass = Object.values(modules[0]).find(exp =>
|
|
242
|
+
typeof exp === 'function' && exp.prototype && exp.prototype.constructor === exp
|
|
243
|
+
);
|
|
244
|
+
if(ElementClass) {
|
|
245
|
+
customElements.define(localName, ElementClass);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
observer.disconnectedSignal.abort();
|
|
249
|
+
}
|
|
250
|
+
export {doFunction as do}
|
|
251
|
+
|
|
252
|
+
// observer setup
|
|
253
|
+
|
|
254
|
+
const observer = new MountObserver({
|
|
255
|
+
whereElementMatches:'my-element',
|
|
256
|
+
import: [
|
|
257
|
+
'./my-element.js',
|
|
258
|
+
['./my-element-small.css', {type: 'css'}],
|
|
259
|
+
'./myActions.js'
|
|
260
|
+
],
|
|
261
|
+
reference: 2
|
|
262
|
+
});
|
|
263
|
+
observer.observe(document);
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Here "2" refers to the imported module index ('./myActions.js' in this case).
|
|
268
|
+
|
|
269
|
+
### How the reference property works
|
|
270
|
+
|
|
271
|
+
The `reference` property allows you to call `do` functions from imported modules, enabling 100% JSON-serializable configuration. This is useful when you want to separate imperative code from declarative configuration.
|
|
272
|
+
|
|
273
|
+
**Key behaviors:**
|
|
274
|
+
- The `reference` property can be a single number or an array of numbers, each referring to an import index
|
|
275
|
+
- Referenced modules must be JavaScript modules (not CSS, JSON, or HTML imports)
|
|
276
|
+
- If a referenced module exports a `do` function, it will be called after the inline `do` callback (if present)
|
|
277
|
+
- If a referenced module doesn't export a `do` function, it's silently skipped
|
|
278
|
+
- The inline `do` callback runs first, then referenced `do` functions run in the order specified
|
|
279
|
+
|
|
280
|
+
**Important:** Since `do` is a reserved keyword in JavaScript, you must export it using the syntax:
|
|
281
|
+
```javascript
|
|
282
|
+
const doFunction = function(element, context) { /* ... */ };
|
|
283
|
+
export { doFunction as do };
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Validation:** The `reference` property is validated in the constructor:
|
|
287
|
+
- Throws an error if `import` is not defined
|
|
288
|
+
- Throws an error if any index is out of bounds
|
|
289
|
+
- Throws an error if any index points to a non-JS module (e.g., CSS or JSON import)
|
|
290
|
+
|
|
291
|
+
Multiple references can also be made.
|
|
292
|
+
|
|
293
|
+
So for example:
|
|
294
|
+
|
|
295
|
+
```JavaScript
|
|
296
|
+
|
|
297
|
+
import: [
|
|
298
|
+
['./my-element-small.css', {type: 'css'}],
|
|
299
|
+
'./component.js',
|
|
300
|
+
'./actions1.js',
|
|
301
|
+
'./actions2.js'
|
|
302
|
+
],
|
|
303
|
+
reference: [2, 3] // Both actions1 and actions2 will have their 'do' called if present
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
[Implemented as [Requirement11](requirements/Requirement11.md)]
|
|
307
|
+
|
|
308
|
+
### Referenced whereInstanceOf
|
|
309
|
+
|
|
310
|
+
Similar to the `do` function, the `whereInstanceOf` check can also be moved to imported modules for 100% JSON-serializable configuration:
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
// module mySettings.js
|
|
314
|
+
const doFunction = function({localName}, {modules, observer, mountInit, rootNode}) {
|
|
315
|
+
if(!customElements.get(localName)) {
|
|
316
|
+
customElements.define(localName, modules[1].MyElement);
|
|
317
|
+
}
|
|
318
|
+
observer.disconnectedSignal.abort();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
|
|
322
|
+
|
|
323
|
+
export { doFunction as do, whereInstanceOf };
|
|
324
|
+
|
|
325
|
+
// my local module
|
|
326
|
+
const observer = new MountObserver({
|
|
327
|
+
whereElementMatches: 'my-element',
|
|
328
|
+
import: [
|
|
329
|
+
['./my-element-small.css', {type: 'css'}],
|
|
330
|
+
'./my-element.js',
|
|
331
|
+
'./mySettings.js'
|
|
332
|
+
],
|
|
333
|
+
reference: 2
|
|
334
|
+
});
|
|
335
|
+
observer.observe(document);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Behavior:**
|
|
339
|
+
- **Combining checks**: If both inline `whereInstanceOf` and referenced `whereInstanceOf` exist, they are AND'd together (element must match both)
|
|
340
|
+
- **Multiple references**: If multiple referenced modules export `whereInstanceOf`, the element must match ALL of them (AND logic)
|
|
341
|
+
- **Validation**: Referenced `whereInstanceOf` is validated after imports load. Throws an error if not a Constructor or array of Constructors
|
|
342
|
+
- **Optional export**: If a referenced module doesn't export `whereInstanceOf`, it's silently ignored
|
|
343
|
+
- **Timing**:
|
|
344
|
+
- With lazy loading (default): Inline `whereInstanceOf` is checked first (before imports), then referenced checks happen after imports load
|
|
345
|
+
- With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
|
|
346
|
+
|
|
347
|
+
This optimization ensures that with lazy loading, elements that don't match the inline `whereInstanceOf` won't trigger unnecessary imports.
|
|
348
|
+
|
|
349
|
+
[Implemented as [Requirement12](requirements/Requirement12.md)]
|
|
350
|
+
|
|
228
351
|
|
|
229
352
|
|
|
230
353
|
## Mount Observer Script Elements (MOSEs)
|
|
231
354
|
|
|
232
355
|
Following an approach similar to the [speculation api](https://developer.chrome.com/blog/speculation-rules-improvements), we can add a script element anywhere in the DOM:
|
|
233
356
|
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
357
|
+
```JavaScript
|
|
358
|
+
// myPackage/myDefiner.js
|
|
359
|
+
//my all powerful custom element definer
|
|
360
|
+
const doFunction = function({localNme}, {modules, observer}){
|
|
238
361
|
if(!customElements.get(localName)) {
|
|
239
362
|
customElements.define(localName, modules[1].MyElement);
|
|
240
363
|
}
|
|
241
364
|
observer.disconnectedSignal.abort();
|
|
242
|
-
}
|
|
365
|
+
}
|
|
366
|
+
export { doFunction as do };
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
```html
|
|
370
|
+
<script type="mountobserver" >
|
|
243
371
|
{
|
|
244
372
|
"select":"my-element",
|
|
245
373
|
"import": [
|
|
246
374
|
["./my-element-small.css", {type: "css"}],
|
|
247
375
|
"./my-element.js",
|
|
248
|
-
|
|
376
|
+
"myPackage/myDefiner.js
|
|
377
|
+
],
|
|
378
|
+
"reference": 2
|
|
249
379
|
}
|
|
250
380
|
</script>
|
|
251
381
|
```
|
|
252
382
|
|
|
253
|
-
|
|
383
|
+
To keep this proposal / polyfill of reasonable size, mount observer script elements has its own [repo / sub-proposal](https://github.com/bahrus/mount-observer-script-element). There's much more to it, but it is awaiting implementation of scoped custom element registry before finalizing the requirements and (re)-implementing.
|
|
384
|
+
|
|
385
|
+
But I think it's important to think about this way of making the mount observer declarative, as it provides one significant reason why we place so much emphasis on making sure that the mount observer settings (mountInit) is as JSON serializable as possible.
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
## Binding from a distance
|
|
389
|
+
|
|
390
|
+
It is important to note that "whereElementMatches" is a css query with no restrictions. So something like:
|
|
254
391
|
|
|
255
392
|
```JavaScript
|
|
256
|
-
|
|
393
|
+
import {EvtRt} from 'mount-observer/EvtRt.js';
|
|
394
|
+
|
|
395
|
+
class MyHandler extends EvtRt {
|
|
396
|
+
mount(mountedElement, mountInit, context){
|
|
397
|
+
mountedElement.textContent = 'hello';
|
|
398
|
+
}
|
|
399
|
+
dismount(mountedElement, mountInit){
|
|
400
|
+
mountedElement.textContent = 'goodbye';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const observer = new MountObserver({
|
|
405
|
+
// not supported by polyfill
|
|
406
|
+
//select: 'div > p + p ~ span[class$="name"]'
|
|
407
|
+
// is supported:
|
|
408
|
+
whereElementMatches: 'div > p + p ~ span[class$="name"]',
|
|
409
|
+
do: (mountedElement, ctx) => {
|
|
410
|
+
new MyHandler(mountedElement, ctx);
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
observer.observe(document);
|
|
257
414
|
```
|
|
258
415
|
|
|
259
|
-
The "scope" of the observer would be the ShadowRoot containing the script element (or the document outside Shadow if placed outside any shadow DOM, like in the head element).
|
|
260
416
|
|
|
261
|
-
|
|
417
|
+
... would work.
|
|
262
418
|
|
|
263
|
-
|
|
264
|
-
> To support the event handlers above, I believe it would require that CSP solutions factor in both the inner content of the script element as well as all the event handlers via the string concatenation operator. I actually think such support is quite critical due to lack of support of import.meta.[some reference to the script element] not being available, as it was pre-ES Modules.
|
|
419
|
+
EvtRt is a convenience class provided with the polyfill package, and is considered part of this proposal (see how it is used below by built in handlers).
|
|
265
420
|
|
|
266
|
-
|
|
421
|
+
This allows developers to create "stylesheet" like capabilities.
|
|
267
422
|
|
|
268
|
-
|
|
423
|
+
## Registering reusable handlers with MountObserver.define
|
|
269
424
|
|
|
270
|
-
|
|
425
|
+
To make MountInit configurations more JSON-serializable and encourage code reuse, you can register handler classes with string names and reference them by name:
|
|
271
426
|
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"targetRegistry": "CustomElements",
|
|
282
|
-
"targetScope": "global",
|
|
283
|
-
"styleModules": [0],
|
|
284
|
-
"classDefinition": {
|
|
285
|
-
"module": 1,
|
|
286
|
-
"exportSymbol": 'MyElement'
|
|
287
|
-
}
|
|
427
|
+
```JavaScript
|
|
428
|
+
import {EvtRt} from 'mount-observer/EvtRt.js';
|
|
429
|
+
|
|
430
|
+
class MyHandler extends EvtRt {
|
|
431
|
+
mount(mountedElement, mountInit, context){
|
|
432
|
+
mountedElement.textContent = 'hello';
|
|
433
|
+
}
|
|
434
|
+
dismount(mountedElement, mountInit){
|
|
435
|
+
mountedElement.textContent = 'bye';
|
|
288
436
|
}
|
|
289
437
|
}
|
|
290
|
-
|
|
438
|
+
|
|
439
|
+
// Register the handler with a string name
|
|
440
|
+
MountObserver.define('myHandler', MyHandler);
|
|
441
|
+
|
|
442
|
+
// Reference it by name in the configuration
|
|
443
|
+
const observer = new MountObserver({
|
|
444
|
+
whereElementMatches: 'div > p + p ~ span[class$="name"]',
|
|
445
|
+
do: 'myHandler' // String reference instead of inline function
|
|
446
|
+
});
|
|
447
|
+
observer.observe(document);
|
|
291
448
|
```
|
|
292
449
|
|
|
450
|
+
### Benefits of registered handlers
|
|
293
451
|
|
|
294
|
-
|
|
452
|
+
1. **JSON serialization**: Configurations using string references can be serialized to JSON
|
|
453
|
+
2. **Code reuse**: Define handlers once, use them in multiple observers
|
|
454
|
+
3. **Separation of concerns**: Keep handler logic separate from configuration
|
|
295
455
|
|
|
296
|
-
|
|
456
|
+
### Using arrays with mixed types
|
|
297
457
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
458
|
+
The `do` property can be a string, a function, or an array mixing both:
|
|
459
|
+
|
|
460
|
+
```JavaScript
|
|
461
|
+
MountObserver.define('logger', LoggerHandler);
|
|
462
|
+
MountObserver.define('validator', ValidatorHandler);
|
|
463
|
+
|
|
464
|
+
const observer = new MountObserver({
|
|
465
|
+
whereElementMatches: 'input',
|
|
466
|
+
do: [
|
|
467
|
+
'logger', // Registered handler
|
|
468
|
+
(element, ctx) => { // Inline function
|
|
469
|
+
element.dataset.processed = 'true';
|
|
470
|
+
},
|
|
471
|
+
'validator' // Another registered handler
|
|
472
|
+
]
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Handlers execute in the order specified. If a handler constructor throws an error, execution stops and subsequent handlers won't run.
|
|
477
|
+
|
|
478
|
+
### Interaction with the reference property
|
|
479
|
+
|
|
480
|
+
When both `do` (with string/array) and `reference` are specified, the execution order is:
|
|
481
|
+
|
|
482
|
+
1. Inline `do` functions and registered handlers (from `do` strings), in whatever order they appear
|
|
483
|
+
2. Referenced `do` functions (from `reference` property)
|
|
484
|
+
|
|
485
|
+
```JavaScript
|
|
486
|
+
MountObserver.define('setup', SetupHandler);
|
|
487
|
+
|
|
488
|
+
const observer = new MountObserver({
|
|
489
|
+
whereElementMatches: 'button',
|
|
490
|
+
import: './button-actions.js',
|
|
491
|
+
reference: 0,
|
|
492
|
+
do: ['setup', (el) => { el.dataset.ready = 'true'; }]
|
|
493
|
+
});
|
|
494
|
+
// Execution order: setup handler, inline function, then imported do function
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Handler requirements
|
|
498
|
+
|
|
499
|
+
Registered handlers must be classes (constructors) that accept `(mountedElement: Element, ctx: MountContext)` as constructor parameters. They can be:
|
|
500
|
+
|
|
501
|
+
- ES6 classes extending `EvtRt` (recommended)
|
|
502
|
+
- ES6 classes with custom logic
|
|
503
|
+
- ES5-style constructor functions
|
|
504
|
+
|
|
505
|
+
```JavaScript
|
|
506
|
+
// ES5-style constructor function
|
|
507
|
+
function SimpleHandler(element, ctx) {
|
|
508
|
+
element.textContent = 'Handled!';
|
|
303
509
|
}
|
|
304
|
-
|
|
510
|
+
|
|
511
|
+
MountObserver.define('simple', SimpleHandler);
|
|
305
512
|
```
|
|
306
513
|
|
|
307
|
-
|
|
514
|
+
### Error handling
|
|
308
515
|
|
|
309
|
-
|
|
516
|
+
**Validation at construction time**: If you reference an unregistered handler name, an error is thrown when creating the MountObserver:
|
|
310
517
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
518
|
+
```JavaScript
|
|
519
|
+
const observer = new MountObserver({
|
|
520
|
+
do: 'nonexistent' // Error: No handler defined for nonexistent
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Duplicate registration**: Attempting to register the same name twice throws an error:
|
|
315
525
|
|
|
316
|
-
|
|
526
|
+
```JavaScript
|
|
527
|
+
MountObserver.define('myHandler', Handler1);
|
|
528
|
+
MountObserver.define('myHandler', Handler2); // Error: myHandler already in use
|
|
529
|
+
```
|
|
317
530
|
|
|
318
531
|
|
|
319
|
-
## Binding from a distance
|
|
320
532
|
|
|
321
|
-
|
|
533
|
+
### Global registry
|
|
534
|
+
|
|
535
|
+
The handler registry is global and shared across all MountObserver instances, similar to the custom elements registry. Once a handler is registered, it can be used by any MountObserver instance in your application.
|
|
536
|
+
|
|
537
|
+
[Implemented as [Requirement14](requirements/Requirement14.md)]
|
|
538
|
+
|
|
539
|
+
### Built in handlers
|
|
540
|
+
|
|
541
|
+
This proposal advocates having the platform provide some built in handlers, that extend EvtRt, that is included with this Polyfill.
|
|
542
|
+
|
|
543
|
+
#### Log to console handler
|
|
322
544
|
|
|
323
545
|
```JavaScript
|
|
324
546
|
const observer = new MountObserver({
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
matchingElement.textContent = 'bye';
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
})
|
|
547
|
+
// not supported by polyfill
|
|
548
|
+
//select: 'div > p + p ~ span[class$="name"]'
|
|
549
|
+
// is supported:
|
|
550
|
+
whereElementMatches: 'div > p + p ~ span[class$="name"]',
|
|
551
|
+
do: 'builtIns.logToConsole'
|
|
552
|
+
});
|
|
553
|
+
observer.observe(document);
|
|
336
554
|
```
|
|
337
555
|
|
|
338
|
-
|
|
556
|
+
This logs to console all the events (mount, dismount, disconnect)
|
|
339
557
|
|
|
340
|
-
|
|
558
|
+
### Lazy custom element handler
|
|
341
559
|
|
|
342
|
-
|
|
560
|
+
```JavaScript
|
|
561
|
+
// MyElement.js
|
|
562
|
+
export default class MyElement extends HTMLElement {
|
|
563
|
+
connectedCallback() {
|
|
564
|
+
this.textContent = 'Hello!';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
343
567
|
|
|
344
|
-
|
|
568
|
+
// main.js
|
|
569
|
+
import { MountObserver } from 'mount-observer';
|
|
345
570
|
|
|
346
|
-
|
|
571
|
+
const observer = new MountObserver({
|
|
572
|
+
whereElementMatches: 'my-element',
|
|
573
|
+
import: './MyElement.js',
|
|
574
|
+
do: 'builtIns.defineCustomElement'
|
|
575
|
+
});
|
|
576
|
+
observer.observe(document);
|
|
577
|
+
|
|
578
|
+
// HTML - elements will be upgraded when discovered
|
|
579
|
+
// by the mount observer
|
|
580
|
+
<my-element></my-element>
|
|
581
|
+
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Applying properties on mount and dismount
|
|
585
|
+
|
|
586
|
+
For the common use case of setting properties on matching elements, MountObserver provides built-in support for the [assignGingerly](https://github.com/bahrus/assign-gingerly) library. This allows us to declaratively specify properties to apply to elements during their lifecycle without writing custom mount callbacks:
|
|
347
587
|
|
|
348
588
|
```JavaScript
|
|
349
589
|
const observer = new MountObserver({
|
|
350
590
|
whereElementMatches: 'input',
|
|
351
|
-
|
|
591
|
+
assignOnMount: {
|
|
352
592
|
disabled: true,
|
|
353
593
|
value: 'Default value',
|
|
354
594
|
title: 'This is a tooltip'
|
|
@@ -359,7 +599,66 @@ observer.observe(document);
|
|
|
359
599
|
|
|
360
600
|
This will automatically apply the specified properties to all matching input elements, both existing ones and those added dynamically.
|
|
361
601
|
|
|
362
|
-
[Implemented as [Requirement2](requirements/Requirement2.md)]
|
|
602
|
+
[Implemented as [Requirement2](requirements/Requirement2.md) and [Requirement16](requirements/Requirement16.md)]
|
|
603
|
+
|
|
604
|
+
### Assigning properties on dismount
|
|
605
|
+
|
|
606
|
+
You can also specify properties to apply when elements are removed from the DOM using `assignOnDismount`:
|
|
607
|
+
|
|
608
|
+
```JavaScript
|
|
609
|
+
const observer = new MountObserver({
|
|
610
|
+
whereElementMatches: '.status-indicator',
|
|
611
|
+
assignOnMount: {
|
|
612
|
+
'?.style?.color': 'green',
|
|
613
|
+
'?.dataset?.status': 'active'
|
|
614
|
+
},
|
|
615
|
+
assignOnDismount: {
|
|
616
|
+
'?.style?.color': 'red',
|
|
617
|
+
'?.dataset?.status': 'inactive'
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
observer.observe(document);
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
This is useful for cleanup operations, visual feedback, or maintaining state on elements that may be temporarily removed from the DOM but still referenced elsewhere in your code.
|
|
624
|
+
|
|
625
|
+
**Note:** The `assignOnDismount` properties are applied before the element is removed from the mounted elements tracking, so the element still has access to its DOM context.
|
|
626
|
+
|
|
627
|
+
#### Practical use case: Form validation feedback
|
|
628
|
+
|
|
629
|
+
A common use case is providing visual feedback for form validation:
|
|
630
|
+
|
|
631
|
+
```JavaScript
|
|
632
|
+
const observer = new MountObserver({
|
|
633
|
+
whereElementMatches: 'input.validated',
|
|
634
|
+
assignOnMount: {
|
|
635
|
+
'?.style?.borderColor': 'green',
|
|
636
|
+
'?.style?.backgroundColor': '#f0fff0',
|
|
637
|
+
'?.setAttribute': ['aria-invalid', 'false']
|
|
638
|
+
},
|
|
639
|
+
assignOnDismount: {
|
|
640
|
+
'?.style?.borderColor': '',
|
|
641
|
+
'?.style?.backgroundColor': '',
|
|
642
|
+
'?.removeAttribute': 'aria-invalid'
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
observer.observe(document);
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
When an input gains the `validated` class, it gets green styling. When the class is removed (dismount), the styling is cleaned up.
|
|
649
|
+
|
|
650
|
+
#### Remounting behavior
|
|
651
|
+
|
|
652
|
+
If an element is removed and then re-added to the DOM, the `assignOnMount` properties will be reapplied:
|
|
653
|
+
|
|
654
|
+
```JavaScript
|
|
655
|
+
const input = document.querySelector('input');
|
|
656
|
+
input.classList.add('validated'); // assignOnMount applied
|
|
657
|
+
input.classList.remove('validated'); // assignOnDismount applied
|
|
658
|
+
input.classList.add('validated'); // assignOnMount applied again
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
This ensures consistent behavior across the element's lifecycle.
|
|
363
662
|
|
|
364
663
|
### Nested properties with dataset
|
|
365
664
|
|
|
@@ -368,7 +667,7 @@ The `assignGingerly` library supports nested property assignment using the `?.`
|
|
|
368
667
|
```JavaScript
|
|
369
668
|
const observer = new MountObserver({
|
|
370
669
|
whereElementMatches: 'button',
|
|
371
|
-
|
|
670
|
+
assignOnMount: {
|
|
372
671
|
disabled: false,
|
|
373
672
|
'?.dataset?.action': 'submit',
|
|
374
673
|
'?.dataset?.trackingId': '12345',
|
|
@@ -385,13 +684,13 @@ The `?.` prefix tells assignGingerly to create nested properties if they don't e
|
|
|
385
684
|
|
|
386
685
|
### Combining with imports
|
|
387
686
|
|
|
388
|
-
You can combine `
|
|
687
|
+
You can combine `assignOn*` with lazy loading to both import resources and set properties:
|
|
389
688
|
|
|
390
689
|
```JavaScript
|
|
391
690
|
const observer = new MountObserver({
|
|
392
691
|
whereElementMatches: 'my-element',
|
|
393
692
|
import: './my-element.js',
|
|
394
|
-
|
|
693
|
+
assignOnMount: {
|
|
395
694
|
theme: 'dark',
|
|
396
695
|
'?.dataset?.initialized': 'true'
|
|
397
696
|
},
|
|
@@ -408,7 +707,7 @@ The `assignGingerly` properties are applied after imports are loaded but before
|
|
|
408
707
|
|
|
409
708
|
### Performance benefits
|
|
410
709
|
|
|
411
|
-
Using `
|
|
710
|
+
Using `assignOn*` provides several benefits:
|
|
412
711
|
|
|
413
712
|
1. **Lazy loading**: The assign-gingerly library is only loaded when needed (when the `assignGingerly` property is specified)
|
|
414
713
|
2. **Bulk operations**: Properties are applied efficiently to all matching elements
|
|
@@ -422,7 +721,7 @@ The `MountObserver` class provides a public `assignGingerly()` method that allow
|
|
|
422
721
|
```JavaScript
|
|
423
722
|
const observer = new MountObserver({
|
|
424
723
|
whereElementMatches: 'input',
|
|
425
|
-
|
|
724
|
+
assignOnMount: {
|
|
426
725
|
disabled: true,
|
|
427
726
|
value: 'Initial value'
|
|
428
727
|
}
|
|
@@ -590,7 +889,7 @@ mountedElemEmits: {
|
|
|
590
889
|
event: 'CustomEvent',
|
|
591
890
|
args: ['ready', { detail: {} }],
|
|
592
891
|
eventProps: {
|
|
593
|
-
timestamp: Date.now(),
|
|
892
|
+
timestamp: Date.now(), //TODO: magic string?
|
|
594
893
|
source: 'mount-observer',
|
|
595
894
|
element: '{{mountedElement}}'
|
|
596
895
|
}
|
|
@@ -663,6 +962,131 @@ observer.observe(document);
|
|
|
663
962
|
|
|
664
963
|
[Implemented as [Requirement10](requirements/Requirement10.md)]
|
|
665
964
|
|
|
965
|
+
## Element-specific lifecycle notifications with getNotifier
|
|
966
|
+
|
|
967
|
+
While the MountObserver dispatches lifecycle events (mount, dismount, disconnect, attrchange) at the observer level, sometimes you need to listen for events specific to a single element. The `getNotifier()` method returns an EventTarget that dispatches filtered events for only that element.
|
|
968
|
+
|
|
969
|
+
### Basic usage
|
|
970
|
+
|
|
971
|
+
```JavaScript
|
|
972
|
+
const observer = new MountObserver({
|
|
973
|
+
whereElementMatches: 'button',
|
|
974
|
+
do: (mountedElement, {observer}) => {
|
|
975
|
+
const notifier = observer.getNotifier(mountedElement);
|
|
976
|
+
|
|
977
|
+
notifier.addEventListener('mount', (e) => {
|
|
978
|
+
console.log('This specific button mounted', e.mountedElement);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
notifier.addEventListener('dismount', (e) => {
|
|
982
|
+
console.log('This specific button dismounted', e.mountedElement, e.reason);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
notifier.addEventListener('disconnect', (e) => {
|
|
986
|
+
console.log('This specific button disconnected', e.mountedElement);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
observer.observe(document);
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### When mount events fire on notifiers
|
|
994
|
+
|
|
995
|
+
The notifier follows a specific rule for mount events:
|
|
996
|
+
|
|
997
|
+
- **First mount**: If `getNotifier()` is called during the `do` callback (when the element is mounting), the mount event does NOT fire on the notifier
|
|
998
|
+
- **Subsequent mounts**: After the element dismounts and mounts again, the mount event WILL fire on the notifier
|
|
999
|
+
|
|
1000
|
+
This prevents duplicate mount notifications when setting up listeners during the initial mount.
|
|
1001
|
+
|
|
1002
|
+
```JavaScript
|
|
1003
|
+
const observer = new MountObserver({
|
|
1004
|
+
whereElementMatches: '#my-button',
|
|
1005
|
+
do: (element, {observer}) => {
|
|
1006
|
+
const notifier = observer.getNotifier(element);
|
|
1007
|
+
|
|
1008
|
+
// This listener won't fire for the current mount
|
|
1009
|
+
// (since we're inside the do callback)
|
|
1010
|
+
notifier.addEventListener('mount', () => {
|
|
1011
|
+
console.log('Element re-mounted after being removed');
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Creating notifiers before mounting
|
|
1018
|
+
|
|
1019
|
+
You can call `getNotifier()` at any time, even before an element mounts:
|
|
1020
|
+
|
|
1021
|
+
```JavaScript
|
|
1022
|
+
const observer = new MountObserver({
|
|
1023
|
+
whereElementMatches: '#future-button'
|
|
1024
|
+
});
|
|
1025
|
+
observer.observe(document);
|
|
1026
|
+
|
|
1027
|
+
// Get notifier before element exists
|
|
1028
|
+
const button = document.createElement('button');
|
|
1029
|
+
button.id = 'future-button';
|
|
1030
|
+
|
|
1031
|
+
const notifier = observer.getNotifier(button);
|
|
1032
|
+
notifier.addEventListener('mount', () => {
|
|
1033
|
+
console.log('Button mounted!'); // This WILL fire
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// Add to DOM later
|
|
1037
|
+
document.body.appendChild(button);
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
When the notifier is created before the element mounts, the mount event fires normally.
|
|
1041
|
+
|
|
1042
|
+
### Filtered attrchange events
|
|
1043
|
+
|
|
1044
|
+
For `attrchange` events, the notifier receives a filtered version containing only changes for that specific element:
|
|
1045
|
+
|
|
1046
|
+
```JavaScript
|
|
1047
|
+
const observer = new MountObserver({
|
|
1048
|
+
whereElementMatches: 'input',
|
|
1049
|
+
whereAttr: {
|
|
1050
|
+
hasBuiltInRootIn: ['data'],
|
|
1051
|
+
hasCERootIn: ['data'],
|
|
1052
|
+
hasBase: 'value',
|
|
1053
|
+
hasBranchIn: ['']
|
|
1054
|
+
},
|
|
1055
|
+
do: (element, {observer}) => {
|
|
1056
|
+
const notifier = observer.getNotifier(element);
|
|
1057
|
+
|
|
1058
|
+
notifier.addEventListener('attrchange', (e) => {
|
|
1059
|
+
// e.changes only contains changes for this specific input
|
|
1060
|
+
console.log('Attribute changed on this input:', e.changes);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
Even if multiple elements have attribute changes in the same mutation batch, each notifier only receives the changes relevant to its element.
|
|
1067
|
+
|
|
1068
|
+
### Use cases
|
|
1069
|
+
|
|
1070
|
+
Element-specific notifiers are useful for:
|
|
1071
|
+
|
|
1072
|
+
1. **Progressive enhancement**: Attach/detach behaviors when elements mount/dismount
|
|
1073
|
+
2. **Cleanup on disconnect**: Remove event listeners or cancel timers when elements are removed
|
|
1074
|
+
3. **Peer element coordination**: React to changes in related elements
|
|
1075
|
+
4. **Lifecycle-aware components**: Build components that respond to their own mounting state
|
|
1076
|
+
|
|
1077
|
+
### Performance notes
|
|
1078
|
+
|
|
1079
|
+
- Notifiers are cached in a WeakMap, so calling `getNotifier()` multiple times for the same element returns the same EventTarget
|
|
1080
|
+
- No explicit cleanup is needed - notifiers are garbage collected when their elements are
|
|
1081
|
+
- The notifier continues to exist even after the element disconnects, allowing it to receive mount events if the element is re-added
|
|
1082
|
+
|
|
1083
|
+
**Method signature:**
|
|
1084
|
+
```TypeScript
|
|
1085
|
+
getNotifier(element: Element): EventTarget
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
[Implemented as [Requirement13](requirements/Requirement13.md)]
|
|
1089
|
+
|
|
666
1090
|
|
|
667
1091
|
## Extra lazy loading
|
|
668
1092
|
|
|
@@ -681,13 +1105,13 @@ const observer = new MountObserver({
|
|
|
681
1105
|
});
|
|
682
1106
|
```
|
|
683
1107
|
|
|
684
|
-
## Media / container queries / instanceOf / custom checks
|
|
1108
|
+
## Media / container queries / instanceOf / custom checks [TODO] out of date
|
|
685
1109
|
|
|
686
1110
|
Unlike traditional CSS @import, CSS Modules don't support specifying different imports based on media queries. That can be another condition we can attach (and why not throw in container queries, based on the rootNode?):
|
|
687
1111
|
|
|
688
1112
|
```JavaScript
|
|
689
1113
|
const observer = new MountObserver({
|
|
690
|
-
select: 'div > p + p ~ span[class$="name"]',
|
|
1114
|
+
select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
|
|
691
1115
|
whereMediaMatches: '(max-width: 1250px)',
|
|
692
1116
|
whereSizeOfContainerMatches: '(min-width: 700px)',
|
|
693
1117
|
whereContainerHas: '[itemprop=isActive][value="true"]',
|
|
@@ -697,23 +1121,7 @@ const observer = new MountObserver({
|
|
|
697
1121
|
effectiveTypeIn: ["slow-2g"],
|
|
698
1122
|
},
|
|
699
1123
|
import: ['./my-element-small.css', {type: 'css'}],
|
|
700
|
-
do:
|
|
701
|
-
confirm: (matchingElement, (e: MountObserverConfirmEvent) => {
|
|
702
|
-
e.isSatisfied = true;
|
|
703
|
-
e.preventDefault();
|
|
704
|
-
}),
|
|
705
|
-
mount: ({localName}, {modules}) => {
|
|
706
|
-
...
|
|
707
|
-
},
|
|
708
|
-
dismount: ...,
|
|
709
|
-
disconnect: ...,
|
|
710
|
-
move: ...,
|
|
711
|
-
reconnect: ...,
|
|
712
|
-
confirm: ...,
|
|
713
|
-
reconfirm: ...,
|
|
714
|
-
exit: ...,
|
|
715
|
-
forget: ...,
|
|
716
|
-
}
|
|
1124
|
+
do: ...
|
|
717
1125
|
});
|
|
718
1126
|
```
|
|
719
1127
|
|
|
@@ -746,7 +1154,7 @@ observer.addEventListener('confirm', e => {
|
|
|
746
1154
|
});
|
|
747
1155
|
observer.addEventListener('mount', e => {
|
|
748
1156
|
console.log({
|
|
749
|
-
|
|
1157
|
+
mountedElement: e.mountedElement,
|
|
750
1158
|
module: e.module
|
|
751
1159
|
});
|
|
752
1160
|
});
|
|
@@ -869,10 +1277,8 @@ const oContainerNode = document.getElementById('myTest');
|
|
|
869
1277
|
const observer = new MountObserver({
|
|
870
1278
|
whereElementMatches:'[itemprop]',
|
|
871
1279
|
whereOutside: '[itemscope]'
|
|
872
|
-
do: {
|
|
873
|
-
|
|
874
|
-
...
|
|
875
|
-
},
|
|
1280
|
+
do: ({localName}, {modules, observer}) => {
|
|
1281
|
+
...
|
|
876
1282
|
},
|
|
877
1283
|
disconnectedSignal: new AbortController().signal
|
|
878
1284
|
});
|
|
@@ -952,10 +1358,10 @@ const mo = new MountObserver({
|
|
|
952
1358
|
observedAttrsWhenMounted: ['lang', 'contenteditable']
|
|
953
1359
|
});
|
|
954
1360
|
|
|
955
|
-
mo.addEventListener('
|
|
1361
|
+
mo.addEventListener('attrchange', e => {
|
|
956
1362
|
console.log(e);
|
|
957
1363
|
// {
|
|
958
|
-
//
|
|
1364
|
+
// mountedElement,
|
|
959
1365
|
// attrChangeInfo:[{
|
|
960
1366
|
// idx: 0,
|
|
961
1367
|
// name: 'lang'
|
|
@@ -1099,7 +1505,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
1099
1505
|
mo.addEventListener('attrChange', e => {
|
|
1100
1506
|
console.log(e);
|
|
1101
1507
|
// {
|
|
1102
|
-
//
|
|
1508
|
+
// mountedElement,
|
|
1103
1509
|
// attrChangeInfo:[{
|
|
1104
1510
|
// idx: 0,
|
|
1105
1511
|
// oldValue: null,
|
|
@@ -1464,78 +1870,6 @@ Just as it is useful to be able lazy load external imports when needed, it would
|
|
|
1464
1870
|
</compose>
|
|
1465
1871
|
```
|
|
1466
1872
|
|
|
1467
|
-
|
|
1468
|
-
## Creating "frameworks" that revolve around MOSEs.
|
|
1469
|
-
|
|
1470
|
-
Often, we will want to define a large number of "mount observer script elements (MOSEs)" programmatically, and we need it to be done in a generic way, that can be published and easily referenced.
|
|
1471
|
-
|
|
1472
|
-
This is a problem space that [be-hive](https://github.com/bahrus/be-hive) is grappling with, and is used as an example for this section, to simply make things more concrete. But we can certainly envision other "frameworks" that could leverage this feature for a variety of purposes, including other families of behaviors/enhancements, or "binding from a distance" syntaxes.
|
|
1473
|
-
|
|
1474
|
-
In particular, *be-hive* supports publishing [enhancements](https://github.com/bahrus/be-enhanced) that take advantage of the DOM filtering ability that the MountObserver provides, that "ties the knot" based on CSS matches in the DOM to behaviors/enhancements that we want to attach directly onto the matching elements. *be-hive* seeks to take advantage of the inheritable infrastructure that MOSEs provide, but we don't want to burden the developer with having to manually list all these configurations, we want it to happen automatically, only expecting manual intervention when we need some special customizations within a specific ShadowDOM realm.
|
|
1475
|
-
|
|
1476
|
-
To support this, we propose these highlights:
|
|
1477
|
-
|
|
1478
|
-
1. Adding a static "synthesize" method to the MountObserver api. This would provide a kind of passage-way from the imperative api to the declarative one.
|
|
1479
|
-
2. As the *synthesize* method is called repeatedly from different packages that work within that framework, it creates a cluster of MOSEs wrapped inside the "synthesizing" custom element ("be-hive") that the framework developer authors. It appends script elements with type="mountobserver" to the custom element instance sitting in the DOM, that dispatches events from the synthesizing custom element it gets appended to, so subscribers in child Shadow DOM's don't need to add a general mutation observer in order to know when parent shadow roots had a MOSE inserted that it needs to act on. This allows the child Shadow DOM's to inherit (in this case) behaviors/enhancements from the parent Shadow DOM.
|
|
1480
|
-
|
|
1481
|
-
So framework developers can develop a bespoke custom element that inherits from the "abstract" class "*Synthesizer*" that is part of this package / proposal, that is used to group families of MountObserver's together.
|
|
1482
|
-
|
|
1483
|
-
Some attributes that the base "Synthesizer" supports are listed below. They are all related to allowing individual ShadowDOM realms to be able to easily opt in or opt out, depending on the level of control/trust that is exerted by a web component / Shadow Root, as far as the HTML it imports in.
|
|
1484
|
-
|
|
1485
|
-
1. passthrough. Allows for the inheritance of behaviors to flow through from above (or from the root document), while not actually activating any of them within the Shadow DOM realm itself.
|
|
1486
|
-
2. exclude. List of specific MOSE id's to block. Allows them to flow through to child Shadow Roots.
|
|
1487
|
-
3. include. List of specific MOSE id's to allow.
|
|
1488
|
-
|
|
1489
|
-
What functionality do these "synthesizing" custom elements provide, what value-add proposition do they fulfill over what is built into the MountObserver polyfill / package?
|
|
1490
|
-
|
|
1491
|
-
The sky is the limit, but focusing on the first example, be-hive, they are:
|
|
1492
|
-
|
|
1493
|
-
1. Managing, interpreting and parsing the attributes that add semantic enhancement vocabularies onto exiting elements.
|
|
1494
|
-
2. Establishing the "handshake" that imports the enhancement package, instantiates the enhancement, and passes properties that were previously assigned to the pre-enhanced element to the attached enhancement/behavior.
|
|
1495
|
-
3. Providing an inheritable "registry" of reusable scriptlets that can be leveraged in a declarative way.
|
|
1496
|
-
|
|
1497
|
-
If one inspects the DOM, one will see grouped (already "parsed") MOSEs, like so:
|
|
1498
|
-
|
|
1499
|
-
```html
|
|
1500
|
-
<be-hive>
|
|
1501
|
-
<script type=mountobserver id=be-hive.be-searching></script>
|
|
1502
|
-
<script type=mountobserver id=be-hive.be-counted></script>
|
|
1503
|
-
</be-hive>
|
|
1504
|
-
```
|
|
1505
|
-
|
|
1506
|
-
Without the help of the synthesize method / Synthesizer base class, the developer would need to set these up manually, so this lifts a significant burden from the shoulders of people who want to leverage these behaviors/enhancements in a seamless way.
|
|
1507
|
-
|
|
1508
|
-
The developer of each package defines their MOSE "template", and then syndicates it via the synthesize method:
|
|
1509
|
-
|
|
1510
|
-
```JavaScript
|
|
1511
|
-
MountObserver.synthesize(root: document | shadowRootNode, ctr: ({new() => Synthesizer}), mose: MOSE)
|
|
1512
|
-
```
|
|
1513
|
-
|
|
1514
|
-
What this method does is it:
|
|
1515
|
-
|
|
1516
|
-
1. Uses [customElements.getName](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/getName) to get the name of the custom element (say it is 'be-hive') from the provided constructor.
|
|
1517
|
-
2. Searches for a be-hive tag inside the root node (with special logic for the "head" element). If not found, creates it.
|
|
1518
|
-
3. Places the MOSE inside.
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
Then in our shadowroot, rather than adding a script type=mountobserver for every single mount observer we want to inherit, we could reference the group via simply:
|
|
1522
|
-
|
|
1523
|
-
```html
|
|
1524
|
-
<be-hive></be-hive>
|
|
1525
|
-
```
|
|
1526
|
-
|
|
1527
|
-
And we can give each inheriting ShadowRoot a personality of its own by customizing the settings within that shadow scope, by manually adding a MOSE with matching id that overrides the inheriting settings with custom settings:
|
|
1528
|
-
|
|
1529
|
-
```html
|
|
1530
|
-
<be-hive>
|
|
1531
|
-
<script type=mountobserver id=be-hive.be-searching>
|
|
1532
|
-
{
|
|
1533
|
-
...my custom settings
|
|
1534
|
-
}
|
|
1535
|
-
</script>
|
|
1536
|
-
</be-hive>
|
|
1537
|
-
```
|
|
1538
|
-
|
|
1539
1873
|
## Creating an Element-To-RefID DOM traversal API
|
|
1540
1874
|
|
|
1541
1875
|
The platform provides some nice help with managing forms, including IDREF dependency support:
|
|
@@ -1652,4 +1986,4 @@ To keep the api uniform, we hide this discrepancy by pretending the form element
|
|
|
1652
1986
|
// includes both field1 and field2
|
|
1653
1987
|
|
|
1654
1988
|
</script>
|
|
1655
|
-
```
|
|
1989
|
+
```
|