mount-observer 0.0.24 → 0.0.26
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/MountObserver.js +31 -6
- package/README.md +16 -13
- package/Synthesizer.js +6 -5
- package/getWhereAttrSelector.js +2 -2
- package/package.json +2 -2
- package/types.d.ts +15 -3
package/MountObserver.js
CHANGED
|
@@ -31,7 +31,7 @@ export class MountObserver extends EventTarget {
|
|
|
31
31
|
}
|
|
32
32
|
#calculatedSelector;
|
|
33
33
|
#attrParts;
|
|
34
|
-
#
|
|
34
|
+
#fullListOfEnhancementAttrs;
|
|
35
35
|
//get #attrVals
|
|
36
36
|
async #selector() {
|
|
37
37
|
if (this.#calculatedSelector !== undefined)
|
|
@@ -43,7 +43,7 @@ export class MountObserver extends EventTarget {
|
|
|
43
43
|
const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
|
|
44
44
|
const info = await getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
45
45
|
const { fullListOfAttrs, calculatedSelector, partitionedAttrs } = info;
|
|
46
|
-
this.#
|
|
46
|
+
this.#fullListOfEnhancementAttrs = fullListOfAttrs;
|
|
47
47
|
this.#attrParts = partitionedAttrs;
|
|
48
48
|
this.#calculatedSelector = calculatedSelector;
|
|
49
49
|
return this.#calculatedSelector;
|
|
@@ -119,7 +119,7 @@ export class MountObserver extends EventTarget {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
const rootMutObs = mutationObserverLookup.get(within);
|
|
122
|
-
const fullListOfAttrs = this.#
|
|
122
|
+
const fullListOfAttrs = this.#fullListOfEnhancementAttrs;
|
|
123
123
|
rootMutObs.addEventListener('mutation-event', async (e) => {
|
|
124
124
|
//TODO: disconnected
|
|
125
125
|
if (this.#isComplex) {
|
|
@@ -151,6 +151,7 @@ export class MountObserver extends EventTarget {
|
|
|
151
151
|
const newValue = target.getAttribute(attributeName);
|
|
152
152
|
const parts = this.#attrParts[idx];
|
|
153
153
|
const attrChangeInfo = {
|
|
154
|
+
isSOfTAttr: false,
|
|
154
155
|
oldValue,
|
|
155
156
|
name: attributeName,
|
|
156
157
|
newValue,
|
|
@@ -245,8 +246,9 @@ export class MountObserver extends EventTarget {
|
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
readAttrs(match, branchIndexes) {
|
|
248
|
-
const fullListOfAttrs = this.#
|
|
249
|
+
const fullListOfAttrs = this.#fullListOfEnhancementAttrs;
|
|
249
250
|
const attrChangeInfos = [];
|
|
251
|
+
const oldValue = null;
|
|
250
252
|
if (fullListOfAttrs !== undefined) {
|
|
251
253
|
const attrParts = this.#attrParts;
|
|
252
254
|
for (let idx = 0, ii = fullListOfAttrs.length; idx < ii; idx++) {
|
|
@@ -257,17 +259,40 @@ export class MountObserver extends EventTarget {
|
|
|
257
259
|
continue;
|
|
258
260
|
}
|
|
259
261
|
const name = fullListOfAttrs[idx];
|
|
260
|
-
const oldValue = null;
|
|
261
262
|
const newValue = match.getAttribute(name);
|
|
262
263
|
attrChangeInfos.push({
|
|
263
264
|
idx,
|
|
265
|
+
isSOfTAttr: false,
|
|
264
266
|
newValue,
|
|
265
267
|
oldValue,
|
|
266
268
|
name,
|
|
267
269
|
parts
|
|
268
270
|
});
|
|
269
271
|
}
|
|
270
|
-
|
|
272
|
+
}
|
|
273
|
+
const { observedAttrsWhenMounted } = this.#mountInit;
|
|
274
|
+
if (observedAttrsWhenMounted !== undefined) {
|
|
275
|
+
for (const observedAttr of observedAttrsWhenMounted) {
|
|
276
|
+
const attrIsString = typeof observedAttr === 'string';
|
|
277
|
+
const name = attrIsString ? observedAttr : observedAttr.name;
|
|
278
|
+
let mapsTo;
|
|
279
|
+
let newValue = match.getAttribute(name);
|
|
280
|
+
if (!attrIsString) {
|
|
281
|
+
const { customParser, instanceOf, mapsTo: mt, valIfNull } = observedAttr;
|
|
282
|
+
if (instanceOf || customParser)
|
|
283
|
+
throw 'NI';
|
|
284
|
+
if (newValue === null)
|
|
285
|
+
newValue = valIfNull;
|
|
286
|
+
mapsTo = mt;
|
|
287
|
+
}
|
|
288
|
+
attrChangeInfos.push({
|
|
289
|
+
isSOfTAttr: true,
|
|
290
|
+
newValue,
|
|
291
|
+
oldValue,
|
|
292
|
+
name,
|
|
293
|
+
mapsTo
|
|
294
|
+
});
|
|
295
|
+
}
|
|
271
296
|
}
|
|
272
297
|
return attrChangeInfos;
|
|
273
298
|
}
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Author: Bruce B. Anderson (with valuable feedback from @doeixd )
|
|
|
11
11
|
|
|
12
12
|
Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
|
|
13
13
|
|
|
14
|
-
Last Update: 2024-5-
|
|
14
|
+
Last Update: 2024-5-27
|
|
15
15
|
|
|
16
16
|
## Benefits of this API
|
|
17
17
|
|
|
@@ -105,7 +105,7 @@ const observer = new MountObserver({
|
|
|
105
105
|
observer.observe(document);
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
Once again, the key can accept either a single import
|
|
108
|
+
Once again, the key can accept either a single import and it can also support multiple imports (via an array).
|
|
109
109
|
|
|
110
110
|
The do event won't be invoked until all the imports have been successfully completed and inserted into the modules array.
|
|
111
111
|
|
|
@@ -113,7 +113,7 @@ Previously, this proposal called for allowing arrow functions as well, thinking
|
|
|
113
113
|
|
|
114
114
|
This proposal would also include support for JSON and HTML module imports.
|
|
115
115
|
|
|
116
|
-
##
|
|
116
|
+
## Mount Observer Script Elements (MOSEs)
|
|
117
117
|
|
|
118
118
|
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:
|
|
119
119
|
|
|
@@ -136,7 +136,7 @@ Following an approach similar to the [speculation api](https://developer.chrome.
|
|
|
136
136
|
</script>
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
The
|
|
139
|
+
The things that make this API work together, namely the "modules", "observer", and "mountedElements" (an array of an array of weak refs to elements that match all the criteria for the ith "on" selector) would be accessible as properties of the script element:
|
|
140
140
|
|
|
141
141
|
```JavaScript
|
|
142
142
|
const {modules, observer, mountedElements, mountInit} = myMountObserver;
|
|
@@ -144,11 +144,11 @@ const {modules, observer, mountedElements, mountInit} = myMountObserver;
|
|
|
144
144
|
|
|
145
145
|
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).
|
|
146
146
|
|
|
147
|
-
No arrays of settings would be supported within a single tag (as this causes issues as far as supporting a single onmount, ondismount, etc event attributes).
|
|
147
|
+
No arrays of settings would be supported within a single tag (as this causes issues as far as supporting a single onmount, ondismount, etc event attributes), but remember that the on criteria can be an array of selectors.
|
|
148
148
|
|
|
149
149
|
## Shadow Root inheritance
|
|
150
150
|
|
|
151
|
-
Inside a shadow root, we can plop a script element, also with type mountobserver, optionally giving it the same id as above:
|
|
151
|
+
Inside a shadow root, we can plop a script element, also with type "mountobserver", optionally giving it the same id as above:
|
|
152
152
|
|
|
153
153
|
```html
|
|
154
154
|
#shadowRoot
|
|
@@ -163,7 +163,7 @@ If no id is found in the parent ShadowRoot (or in the parent window if the shado
|
|
|
163
163
|
|
|
164
164
|
But if a matching id is found, then the values from the parent script element get merged in with the one in the child, with the child settings, including the event handling attributes.
|
|
165
165
|
|
|
166
|
-
We will come back to some [additional features](#creating-frameworks-that-revolve-around-moses) of using these script elements later, but
|
|
166
|
+
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.
|
|
167
167
|
|
|
168
168
|
## Binding from a distance
|
|
169
169
|
|
|
@@ -325,9 +325,9 @@ The alternative to providing this feature, which I'm leaning towards, is to just
|
|
|
325
325
|
|
|
326
326
|
## A tribute to attributes
|
|
327
327
|
|
|
328
|
-
Attributes of DOM elements are tricky. They've been around since the get-go of the Web, and they've survived multiple
|
|
328
|
+
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.
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
The MountObserver API provides explicit support for monitoring attributes. There are two primary reasons for why it is important to provide this as part of the API:
|
|
331
331
|
|
|
332
332
|
Being that for both custom elements, as well as (hopefully) [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) we need to carefully work with sets of "owned" [observed](https://github.com/WICG/webcomponents/issues/1045) attributes, and in some cases we may need to manage combinations of prefixes and suffixes for better name-spacing management, creating the most effective css query becomes challenging.
|
|
333
333
|
|
|
@@ -337,9 +337,11 @@ We want to be alerted by the discovery of elements adorned by these attributes,
|
|
|
337
337
|
|
|
338
338
|
I think it is useful to divide [attributes](https://jakearchibald.com/2024/attributes-vs-properties/) that we would want to observe into two categories:
|
|
339
339
|
|
|
340
|
-
1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of
|
|
340
|
+
1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of truth" for key features of the DOM element itself. We will refer to these attributes as "Source of Truth" attributes.
|
|
341
341
|
|
|
342
|
-
Examples are many built-in global attributes, like lang, or contenteditable, or more specialized examples such as "content" for the meta tag. I think in the vast majority of cases, setting the property values corresponding to these attributes results in directly reflecting those property values to the attributes. There are exceptions, especially for non-string attributes like the checked property of the input element / type=checkbox
|
|
342
|
+
Examples are many built-in global attributes, like lang, or contenteditable, or more specialized examples such as "content" for the meta tag. I think in the vast majority of cases, setting the property values corresponding to these attributes results in directly reflecting those property values to the attributes. There are exceptions, especially for non-string attributes like the checked property of the input element / type=checkbox, and JSON based attributes for custom elements.
|
|
343
|
+
|
|
344
|
+
Usually, that are no events we can subscribe to in order to know when the property changes. Hijacking the property setter in order to observe changes may not always work or feel very resilient. So monitoring the attribute value associated with the property is often the most effective way of observing when the property/attribute state for these elements change. And some attributes (like the microdata attributes such as itemprop) don't even have properties that they pair with!
|
|
343
345
|
|
|
344
346
|
|
|
345
347
|
2. In contrast, there are scenarios where we want to support somewhat fluid, renamable attributes within different Shadow DOM scopes, which add behavior/enhancement capabilities on top of built-in or third party custom elements. We'll refer to these attributes as "Enhancement Attributes."
|
|
@@ -376,6 +378,7 @@ mo.addEventListener('attrChange', e => {
|
|
|
376
378
|
// attrChangeInfo:[{
|
|
377
379
|
// idx: 0,
|
|
378
380
|
// name: 'lang'
|
|
381
|
+
// isSOfTAttr: true,
|
|
379
382
|
// oldValue: null,
|
|
380
383
|
// newValue: 'en-GB',
|
|
381
384
|
// }]
|
|
@@ -722,12 +725,12 @@ Often, we will want to define a large number of "mount observer script elements
|
|
|
722
725
|
|
|
723
726
|
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.
|
|
724
727
|
|
|
725
|
-
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
|
|
728
|
+
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 scope.
|
|
726
729
|
|
|
727
730
|
To support this, we propose these highlights:
|
|
728
731
|
|
|
729
732
|
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.
|
|
730
|
-
2. As the
|
|
733
|
+
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.
|
|
731
734
|
|
|
732
735
|
So framework developers can develop a bespoke custom element that inherits from the class "*Synthesizer*" that is part of this package / proposal, that is used to group families of MountObservers together.
|
|
733
736
|
|
package/Synthesizer.js
CHANGED
|
@@ -43,6 +43,7 @@ export class Synthesizer extends HTMLElement {
|
|
|
43
43
|
import(mose) {
|
|
44
44
|
const { init, do: d, id, synConfig } = mose;
|
|
45
45
|
const se = document.createElement('script');
|
|
46
|
+
se.type = 'mountobserver';
|
|
46
47
|
se.init = { ...init };
|
|
47
48
|
se.id = id;
|
|
48
49
|
se.do = { ...d };
|
|
@@ -57,15 +58,15 @@ export class Synthesizer extends HTMLElement {
|
|
|
57
58
|
const parentShadowRealm = host.getRootNode();
|
|
58
59
|
const { localName } = this;
|
|
59
60
|
const parentScopeSynthesizer = parentShadowRealm.querySelector(localName);
|
|
61
|
+
if (parentScopeSynthesizer === null)
|
|
62
|
+
throw 404;
|
|
60
63
|
const { mountObserverElements } = parentScopeSynthesizer;
|
|
61
64
|
for (const moe of mountObserverElements) {
|
|
62
65
|
this.import(moe);
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
}
|
|
67
|
+
parentScopeSynthesizer.addEventListener(SynthesizeEvent.eventName, e => {
|
|
68
|
+
this.import(e.mountObserverElement);
|
|
69
|
+
});
|
|
69
70
|
}
|
|
70
71
|
disconnectedCallback() {
|
|
71
72
|
if (this.#mutationObserver !== undefined) {
|
package/getWhereAttrSelector.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export async function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
-
const { hasBase, hasBranchIn, hasRootIn
|
|
3
|
-
const fullListOfAttrs =
|
|
2
|
+
const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
|
|
3
|
+
const fullListOfAttrs = [];
|
|
4
4
|
const partitionedAttrs = [];
|
|
5
5
|
if (hasBase !== undefined) {
|
|
6
6
|
const hasRootInGuaranteed = hasRootIn || [{
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mount-observer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "Observe and act on css matches.",
|
|
5
5
|
"main": "MountObserver.js",
|
|
6
6
|
"module": "MountObserver.js",
|
|
7
7
|
"devDependencies": {
|
|
8
|
-
"@playwright/test": "1.
|
|
8
|
+
"@playwright/test": "1.44.1",
|
|
9
9
|
"may-it-serve": "0.0.6"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
package/types.d.ts
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
export interface JSONSerializableMountInit{
|
|
4
4
|
readonly on?: CSSMatch,
|
|
5
|
-
observedAttrsWhenMounted?: string[],
|
|
5
|
+
readonly observedAttrsWhenMounted?: (string | ObservedSourceOfTruthAttribute)[],
|
|
6
6
|
readonly whereAttr?: WhereAttr,
|
|
7
7
|
readonly whereElementIntersectsWith?: IntersectionObserverInit,
|
|
8
8
|
readonly whereMediaMatches?: MediaQuery,
|
|
9
9
|
readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
|
|
10
10
|
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export interface ObservedSourceOfTruthAttribute {
|
|
14
|
+
name: string,
|
|
15
|
+
mapsTo?: string,
|
|
16
|
+
valIfNull?: any,
|
|
17
|
+
valIfFalsy?: any,
|
|
18
|
+
instanceOf?: any,
|
|
19
|
+
customParser?: (newValue: string | null, oldValue: string | null, instance: Element) => any
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export interface MountInit extends JSONSerializableMountInit{
|
|
13
23
|
|
|
14
24
|
readonly withTargetShadowRoot?: ShadowRoot,
|
|
@@ -106,9 +116,11 @@ interface AttrParts{
|
|
|
106
116
|
interface AttrChangeInfo{
|
|
107
117
|
oldValue: string | null,
|
|
108
118
|
newValue: string | null,
|
|
109
|
-
|
|
119
|
+
isSOfTAttr: boolean,
|
|
120
|
+
idx?: number,
|
|
110
121
|
name: string,
|
|
111
|
-
parts
|
|
122
|
+
parts?: AttrParts,
|
|
123
|
+
mapsTo?: string,
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
//#region mount event
|