mount-observer 0.0.7 → 0.0.9
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 +160 -58
- package/README.md +118 -25
- package/doWhereAttr.js +43 -0
- package/getWhereAttrSelector.js +35 -0
- package/package.json +1 -1
- package/types.d.ts +29 -12
package/MountObserver.js
CHANGED
|
@@ -3,13 +3,14 @@ const mutationObserverLookup = new WeakMap();
|
|
|
3
3
|
const refCount = new WeakMap();
|
|
4
4
|
export class MountObserver extends EventTarget {
|
|
5
5
|
#mountInit;
|
|
6
|
-
|
|
6
|
+
//#rootMutObs: RootMutObs | undefined;
|
|
7
7
|
#abortController;
|
|
8
8
|
#mounted;
|
|
9
9
|
#mountedList;
|
|
10
10
|
#disconnected;
|
|
11
11
|
//#unmounted: WeakSet<Element>;
|
|
12
12
|
#isComplex;
|
|
13
|
+
#observe;
|
|
13
14
|
constructor(init) {
|
|
14
15
|
super();
|
|
15
16
|
const { on, whereElementIntersectsWith, whereMediaMatches } = init;
|
|
@@ -28,26 +29,107 @@ export class MountObserver extends EventTarget {
|
|
|
28
29
|
//this.#unmounted = new WeakSet();
|
|
29
30
|
}
|
|
30
31
|
#calculatedSelector;
|
|
31
|
-
|
|
32
|
+
#fullListOfAttrs;
|
|
33
|
+
//get #attrVals
|
|
34
|
+
async #selector() {
|
|
32
35
|
if (this.#calculatedSelector !== undefined)
|
|
33
36
|
return this.#calculatedSelector;
|
|
34
37
|
const { on, whereAttr } = this.#mountInit;
|
|
35
|
-
const
|
|
38
|
+
const withoutAttrs = on || '*';
|
|
36
39
|
if (whereAttr === undefined)
|
|
37
|
-
return
|
|
38
|
-
const {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
return withoutAttrs;
|
|
41
|
+
const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
|
|
42
|
+
const info = getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
43
|
+
const { fullListOfAttrs, calculatedSelector } = info;
|
|
44
|
+
this.#fullListOfAttrs = fullListOfAttrs;
|
|
45
|
+
this.#calculatedSelector = calculatedSelector;
|
|
46
|
+
return this.#calculatedSelector;
|
|
47
|
+
}
|
|
48
|
+
async #birtualizeFragment(fragment, level) {
|
|
49
|
+
const bis = Array.from(fragment.querySelectorAll(biQry));
|
|
50
|
+
for (const bi of bis) {
|
|
51
|
+
await this.#birtalizeMatch(bi, level);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async #birtalizeMatch(el, level) {
|
|
55
|
+
const href = el.getAttribute('href');
|
|
56
|
+
el.removeAttribute('href');
|
|
57
|
+
const templID = href.substring(1);
|
|
58
|
+
const fragment = this.#observe?.deref();
|
|
59
|
+
if (fragment === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const templ = this.#findByID(templID, fragment);
|
|
62
|
+
if (!(templ instanceof HTMLTemplateElement))
|
|
63
|
+
throw 404;
|
|
64
|
+
const clone = templ.content.cloneNode(true);
|
|
65
|
+
const slots = el.querySelectorAll(`[slot]`);
|
|
66
|
+
for (const slot of slots) {
|
|
67
|
+
const name = slot.getAttribute('slot');
|
|
68
|
+
const targets = Array.from(clone.querySelectorAll(`slot[name="${name}"]`));
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
const slotClone = slot.cloneNode(true);
|
|
71
|
+
target.after(slotClone);
|
|
72
|
+
target.remove();
|
|
47
73
|
}
|
|
48
74
|
}
|
|
49
|
-
this.#
|
|
50
|
-
|
|
75
|
+
this.#birtualizeFragment(clone, level + 1);
|
|
76
|
+
if (level === 0) {
|
|
77
|
+
const slotMap = el.getAttribute('slotmap');
|
|
78
|
+
let map = slotMap === null ? undefined : JSON.parse(slotMap);
|
|
79
|
+
const slots = Array.from(clone.querySelectorAll('[slot]'));
|
|
80
|
+
for (const slot of slots) {
|
|
81
|
+
if (map !== undefined) {
|
|
82
|
+
const slotName = slot.slot;
|
|
83
|
+
for (const key in map) {
|
|
84
|
+
if (slot.matches(key)) {
|
|
85
|
+
const targetAttSymbols = map[key];
|
|
86
|
+
for (const sym of targetAttSymbols) {
|
|
87
|
+
switch (sym) {
|
|
88
|
+
case '|':
|
|
89
|
+
slot.setAttribute('itemprop', slotName);
|
|
90
|
+
break;
|
|
91
|
+
case '$':
|
|
92
|
+
slot.setAttribute('itemscope', '');
|
|
93
|
+
slot.setAttribute('itemprop', slotName);
|
|
94
|
+
break;
|
|
95
|
+
case '@':
|
|
96
|
+
slot.setAttribute('name', slotName);
|
|
97
|
+
break;
|
|
98
|
+
case '.':
|
|
99
|
+
slot.classList.add(slotName);
|
|
100
|
+
break;
|
|
101
|
+
case '%':
|
|
102
|
+
slot.part.add(slotName);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
slot.removeAttribute('slot');
|
|
110
|
+
}
|
|
111
|
+
el.dispatchEvent(new LoadEvent(clone));
|
|
112
|
+
//console.log('dispatched')
|
|
113
|
+
}
|
|
114
|
+
el.before(clone);
|
|
115
|
+
el.remove();
|
|
116
|
+
}
|
|
117
|
+
#templLookUp = new Map();
|
|
118
|
+
#findByID(id, fragment) {
|
|
119
|
+
if (this.#templLookUp.has(id))
|
|
120
|
+
return this.#templLookUp.get(id);
|
|
121
|
+
let templ = fragment.getElementById(id);
|
|
122
|
+
if (templ === null) {
|
|
123
|
+
let rootToSearchOutwardFrom = ((fragment.isConnected ? fragment.getRootNode() : this.#mountInit.withTargetShadowRoot) || document);
|
|
124
|
+
templ = rootToSearchOutwardFrom.getElementById(id);
|
|
125
|
+
while (templ === null && rootToSearchOutwardFrom !== document) {
|
|
126
|
+
rootToSearchOutwardFrom = (rootToSearchOutwardFrom.host || rootToSearchOutwardFrom).getRootNode();
|
|
127
|
+
templ = rootToSearchOutwardFrom.getElementById(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (templ !== null)
|
|
131
|
+
this.#templLookUp.set(id, templ);
|
|
132
|
+
return templ;
|
|
51
133
|
}
|
|
52
134
|
unobserve(within) {
|
|
53
135
|
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
@@ -75,6 +157,7 @@ export class MountObserver extends EventTarget {
|
|
|
75
157
|
}
|
|
76
158
|
}
|
|
77
159
|
async observe(within) {
|
|
160
|
+
this.#observe = new WeakRef(within);
|
|
78
161
|
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
79
162
|
if (!mutationObserverLookup.has(nodeToMonitor)) {
|
|
80
163
|
mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
|
|
@@ -90,8 +173,9 @@ export class MountObserver extends EventTarget {
|
|
|
90
173
|
}
|
|
91
174
|
}
|
|
92
175
|
const rootMutObs = mutationObserverLookup.get(within);
|
|
93
|
-
const {
|
|
94
|
-
|
|
176
|
+
//const {whereAttr} = this.#mountInit;
|
|
177
|
+
const fullListOfAttrs = this.#fullListOfAttrs;
|
|
178
|
+
rootMutObs.addEventListener('mutation-event', async (e) => {
|
|
95
179
|
//TODO: disconnected
|
|
96
180
|
if (this.#isComplex) {
|
|
97
181
|
this.#inspectWithin(within, false);
|
|
@@ -108,22 +192,11 @@ export class MountObserver extends EventTarget {
|
|
|
108
192
|
addedElements.forEach(x => elsToInspect.push(x));
|
|
109
193
|
if (type === 'attributes') {
|
|
110
194
|
const { target, attributeName, oldValue } = mutationRecord;
|
|
111
|
-
if (target instanceof Element && attributeName !== null &&
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (names.includes(attributeName)) {
|
|
195
|
+
if (target instanceof Element && attributeName !== null && this.#mounted.has(target)) {
|
|
196
|
+
if (fullListOfAttrs !== undefined) {
|
|
197
|
+
const idx = fullListOfAttrs.indexOf(attributeName);
|
|
198
|
+
if (idx > -1) {
|
|
116
199
|
const newValue = target.getAttribute(attributeName);
|
|
117
|
-
// let parsedNewValue = undefined;
|
|
118
|
-
// switch(type){
|
|
119
|
-
// case 'boolean':
|
|
120
|
-
// parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
|
|
121
|
-
// break;
|
|
122
|
-
// case 'date':
|
|
123
|
-
// parsedNewValue = newValue === null ? null : new Date(newValue);
|
|
124
|
-
// break;
|
|
125
|
-
// case ''
|
|
126
|
-
// }
|
|
127
200
|
const attrChangeInfo = {
|
|
128
201
|
name: attributeName,
|
|
129
202
|
oldValue,
|
|
@@ -132,7 +205,13 @@ export class MountObserver extends EventTarget {
|
|
|
132
205
|
};
|
|
133
206
|
this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
|
|
134
207
|
}
|
|
135
|
-
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const { whereAttr } = this.#mountInit;
|
|
211
|
+
if (whereAttr !== undefined) {
|
|
212
|
+
const { doWhereAttr } = await import('./doWhereAttr.js');
|
|
213
|
+
doWhereAttr(whereAttr, attributeName, target, oldValue, this);
|
|
214
|
+
}
|
|
136
215
|
}
|
|
137
216
|
}
|
|
138
217
|
elsToInspect.push(target);
|
|
@@ -164,9 +243,10 @@ export class MountObserver extends EventTarget {
|
|
|
164
243
|
}
|
|
165
244
|
async #mount(matching, initializing) {
|
|
166
245
|
//first unmount non matching
|
|
167
|
-
const alreadyMounted = this.#filterAndDismount();
|
|
246
|
+
const alreadyMounted = await this.#filterAndDismount();
|
|
168
247
|
const mount = this.#mountInit.do?.mount;
|
|
169
|
-
const { import: imp
|
|
248
|
+
const { import: imp } = this.#mountInit;
|
|
249
|
+
const fullListOfAttrs = this.#fullListOfAttrs;
|
|
170
250
|
for (const match of matching) {
|
|
171
251
|
if (alreadyMounted.has(match))
|
|
172
252
|
continue;
|
|
@@ -197,27 +277,33 @@ export class MountObserver extends EventTarget {
|
|
|
197
277
|
});
|
|
198
278
|
}
|
|
199
279
|
this.dispatchEvent(new MountEvent(match, initializing));
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
for (const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const name of names) {
|
|
207
|
-
const attrVal = match.getAttribute(name);
|
|
208
|
-
if (attrVal !== null)
|
|
209
|
-
nonNullName = name;
|
|
210
|
-
newValue = newValue || attrVal;
|
|
280
|
+
if (fullListOfAttrs !== undefined) {
|
|
281
|
+
const { whereAttr } = this.#mountInit;
|
|
282
|
+
for (const name of fullListOfAttrs) {
|
|
283
|
+
if (whereAttr !== undefined) {
|
|
284
|
+
const { doWhereAttr } = await import('./doWhereAttr.js');
|
|
285
|
+
doWhereAttr(whereAttr, name, match, null, this);
|
|
211
286
|
}
|
|
212
|
-
const attribInfo = {
|
|
213
|
-
oldValue: null,
|
|
214
|
-
newValue,
|
|
215
|
-
idx,
|
|
216
|
-
name: nonNullName
|
|
217
|
-
};
|
|
218
|
-
this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
|
|
219
|
-
idx++;
|
|
220
287
|
}
|
|
288
|
+
// let idx = 0;
|
|
289
|
+
// for(const attribMatch of attribMatches){
|
|
290
|
+
// let newValue = null;
|
|
291
|
+
// const {names} = attribMatch;
|
|
292
|
+
// let nonNullName = names[0];
|
|
293
|
+
// for(const name of names){
|
|
294
|
+
// const attrVal = match.getAttribute(name);
|
|
295
|
+
// if(attrVal !== null) nonNullName = name;
|
|
296
|
+
// newValue = newValue || attrVal;
|
|
297
|
+
// }
|
|
298
|
+
// const attribInfo: AttrChangeInfo = {
|
|
299
|
+
// oldValue: null,
|
|
300
|
+
// newValue,
|
|
301
|
+
// idx,
|
|
302
|
+
// name: nonNullName
|
|
303
|
+
// };
|
|
304
|
+
// this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
|
|
305
|
+
// idx++;
|
|
306
|
+
// }
|
|
221
307
|
}
|
|
222
308
|
this.#mountedList?.push(new WeakRef(match));
|
|
223
309
|
//if(this.#unmounted.has(match)) this.#unmounted.delete(match);
|
|
@@ -232,12 +318,12 @@ export class MountObserver extends EventTarget {
|
|
|
232
318
|
this.dispatchEvent(new DismountEvent(unmatch));
|
|
233
319
|
}
|
|
234
320
|
}
|
|
235
|
-
#filterAndDismount() {
|
|
321
|
+
async #filterAndDismount() {
|
|
236
322
|
const returnSet = new Set();
|
|
237
323
|
if (this.#mountedList !== undefined) {
|
|
238
324
|
const previouslyMounted = this.#mountedList.map(x => x.deref());
|
|
239
325
|
const { whereSatisfies, whereInstanceOf } = this.#mountInit;
|
|
240
|
-
const match = this.#selector;
|
|
326
|
+
const match = await this.#selector();
|
|
241
327
|
const elsToUnMount = previouslyMounted.filter(x => {
|
|
242
328
|
if (x === undefined)
|
|
243
329
|
return false;
|
|
@@ -257,7 +343,7 @@ export class MountObserver extends EventTarget {
|
|
|
257
343
|
}
|
|
258
344
|
async #filterAndMount(els, checkMatch, initializing) {
|
|
259
345
|
const { whereSatisfies, whereInstanceOf } = this.#mountInit;
|
|
260
|
-
const match = this.#selector;
|
|
346
|
+
const match = await this.#selector();
|
|
261
347
|
const elsToMount = els.filter(x => {
|
|
262
348
|
if (checkMatch) {
|
|
263
349
|
if (!x.matches(match))
|
|
@@ -273,14 +359,21 @@ export class MountObserver extends EventTarget {
|
|
|
273
359
|
}
|
|
274
360
|
return true;
|
|
275
361
|
});
|
|
362
|
+
for (const elToMount of elsToMount) {
|
|
363
|
+
if (elToMount.matches(biQry)) {
|
|
364
|
+
await this.#birtalizeMatch(elToMount, 0);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
276
367
|
this.#mount(elsToMount, initializing);
|
|
277
368
|
}
|
|
278
369
|
async #inspectWithin(within, initializing) {
|
|
279
|
-
|
|
370
|
+
await this.#birtualizeFragment(within, 0);
|
|
371
|
+
const els = Array.from(within.querySelectorAll(await this.#selector()));
|
|
280
372
|
this.#filterAndMount(els, false, initializing);
|
|
281
373
|
}
|
|
282
374
|
}
|
|
283
375
|
const refCountErr = 'mount-observer ref count mismatch';
|
|
376
|
+
const biQry = 'b-i[href^="#"]:not([disabled])';
|
|
284
377
|
// https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
|
|
285
378
|
/**
|
|
286
379
|
* The `mutation-event` event represents something that happened.
|
|
@@ -322,3 +415,12 @@ export class AttrChangeEvent extends Event {
|
|
|
322
415
|
this.attrChangeInfo = attrChangeInfo;
|
|
323
416
|
}
|
|
324
417
|
}
|
|
418
|
+
export class LoadEvent extends Event {
|
|
419
|
+
clone;
|
|
420
|
+
static eventName = 'load';
|
|
421
|
+
constructor(clone) {
|
|
422
|
+
super(LoadEvent.eventName);
|
|
423
|
+
this.clone = clone;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
//const hasRootInDefault = ['data', 'enh', 'data-enh']
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://bundlephobia.com/result?p=mount-observer)
|
|
4
4
|
<img src="http://img.badgesize.io/https://cdn.jsdelivr.net/npm/mount-observer?compression=gzip">
|
|
5
5
|
|
|
6
|
+
Note that much of what is described below has not yet been polyfilled.
|
|
6
7
|
|
|
7
8
|
# The MountObserver api.
|
|
8
9
|
|
|
@@ -10,7 +11,7 @@ Author: Bruce B. Anderson
|
|
|
10
11
|
|
|
11
12
|
Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
|
|
12
13
|
|
|
13
|
-
Last Update: 2024-2-
|
|
14
|
+
Last Update: 2024-2-17
|
|
14
15
|
|
|
15
16
|
## Benefits of this API
|
|
16
17
|
|
|
@@ -24,7 +25,7 @@ The underlying theme is this api is meant to make it easy for the developer to d
|
|
|
24
25
|
|
|
25
26
|
There is quite a bit of functionality this proposal would open up, that is exceedingly difficult to polyfill reliably:
|
|
26
27
|
|
|
27
|
-
1. It is unclear how to use mutation observers to observe changes to [custom state](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet). The closest thing might be a solution like [this](https://davidwalsh.name/detect-node-insertion), but that falls short for elements that aren't visible, or during template instantiation.
|
|
28
|
+
1. It is unclear how to use mutation observers to observe changes to [custom state](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet). The closest thing might be a solution like [this](https://davidwalsh.name/detect-node-insertion), but that falls short for elements that aren't visible, or during template instantiation, and requires carefully constructed "negating" queries if needing to know when the css selector is no longer matching.
|
|
28
29
|
|
|
29
30
|
2. For simple css matches, like "my-element", or "[name='hello']" it is enough to use a mutation observer, and only observe the elements within the specified DOM region (more on that below). But as CSS has evolved, it is quite easy to think of numerous css selectors that would require us to expand our mutation observer to need to scan the entire Shadow DOM realm, or the entire DOM tree outside any Shadow DOM, for any and all mutations (including attribute changes), and re-evaluate every single element within the specified DOM region for new matches or old matches that no longer match. Things like child selectors, :has, and so on. All this is done, miraculously, by the browser in a performant way. Reproducing this in userland using JavaScript alone, matching the same performance seems impossible.
|
|
30
31
|
|
|
@@ -38,7 +39,7 @@ The amount of code necessary to accomplish these common tasks designed to improv
|
|
|
38
39
|
1. Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
|
|
39
40
|
2. Supporting "binding from a distance" that can set property values of elements in bulk as the HTML streams in. For example, say a web page is streaming in HTML with thousands of input elements (say a long tax form). We want to have some indication in the head tag of the HTML (for example) to make all the input elements read only as they stream through the page. With css, we could do similar things, for example set the background to red of all input elements. Why can't we do something similar with setting properties like readOnly, disabled, etc? With this api, giving developers the "keys" to css filtering, so they can "mount a campaign" to apply common settings on them all feels like something that almost every web developer has mentally screamed to themselves "why can't I do that?", doesn't it?
|
|
40
41
|
3. Supporting "progressive enhancement" more effectively.
|
|
41
|
-
2. Potentially by allowing the platform to do more work in the low-level (c/c++/rust?) code, without as much context switching into the JavaScript memory space, which may reduce cpu cycles as well. This is done by passing into the API substantial number of conditions, which can all be evaluated at a lower level, before
|
|
42
|
+
2. Potentially by allowing the platform to do more work in the low-level (c/c++/rust?) code, without as much context switching into the JavaScript memory space, which may reduce cpu cycles as well. This is done by passing into the API substantial number of conditions, which can all be evaluated at a lower level, before the api needs to surface up to the developer "found one!".
|
|
42
43
|
3. As discussed earlier, to do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
|
|
43
44
|
|
|
44
45
|
The extra flexibility this new primitive would provide could be quite useful to things other than lazy loading of custom elements, such as implementing [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) as well as [binding from a distance](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) in userland.
|
|
@@ -100,6 +101,8 @@ const observer = new MountObserver({
|
|
|
100
101
|
})
|
|
101
102
|
```
|
|
102
103
|
|
|
104
|
+
... would work.
|
|
105
|
+
|
|
103
106
|
This would allow developers to create "stylesheet" like capabilities.
|
|
104
107
|
|
|
105
108
|
|
|
@@ -150,7 +153,9 @@ const observer = new MountObserver({
|
|
|
150
153
|
})
|
|
151
154
|
```
|
|
152
155
|
|
|
153
|
-
Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance.
|
|
156
|
+
Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance. The expression that the "do" field points to could also be a (stateful) user defined class instance.
|
|
157
|
+
|
|
158
|
+
However, since these rules may be of interest to multiple parties, it is useful to also provide the ability for multiple parties to subscribe to these css rules. This can be done via:
|
|
154
159
|
|
|
155
160
|
## Subscribing
|
|
156
161
|
|
|
@@ -206,7 +211,7 @@ Being that for both custom elements, as well as (hopefully) [custom enhancements
|
|
|
206
211
|
|
|
207
212
|
We want to be alerted by the discovery of elements adorned by these attributes, but then continue to be alerted to changes of their values, and we can't enumerate which values we are interested in, so we must subscribe to all values as they change.
|
|
208
213
|
|
|
209
|
-
### Scenario 1 -- Custom Element integration with
|
|
214
|
+
### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
|
|
210
215
|
|
|
211
216
|
Example:
|
|
212
217
|
|
|
@@ -247,6 +252,8 @@ To make this discussion concrete, let's suppose the "canonical" names of those a
|
|
|
247
252
|
my-enhancement=greetings
|
|
248
253
|
my-enhancement-first-aspect=hello
|
|
249
254
|
my-enhancement-second-aspect=goodbye
|
|
255
|
+
my-enhancement-first-aspect-wow-this-is-deep
|
|
256
|
+
my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
|
|
250
257
|
></section>
|
|
251
258
|
</div>
|
|
252
259
|
```
|
|
@@ -263,17 +270,19 @@ We want to also support:
|
|
|
263
270
|
data-my-enhancement=greetings
|
|
264
271
|
data-my-enhancement-first-aspect=hello
|
|
265
272
|
data-my-enhancement-second-aspect=goodbye
|
|
273
|
+
data-my-enhancement-first-aspect-wow-this-is-deep
|
|
274
|
+
data-my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
|
|
266
275
|
></section>
|
|
267
276
|
</div>
|
|
268
277
|
```
|
|
269
278
|
|
|
270
279
|
Based on the current unspoken rules, no one will raise an eyebrow with these attributes, because the platform has indicated it will generally avoid dashes in attributes (with an exception or two that will only happen in a blue moon, like aria-*).
|
|
271
280
|
|
|
272
|
-
But now when we consider applying this enhancement to custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement
|
|
281
|
+
But now when we consider applying this enhancement to custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement?
|
|
273
282
|
|
|
274
|
-
So let's say we want to insist that on custom elements, we must have the
|
|
283
|
+
So let's say we want to insist that on custom elements, we must have the data- prefix?
|
|
275
284
|
|
|
276
|
-
And we want to support an alternative prefix to data, say enh-*, endorsed by [this proposal](https://github.com/WICG/webcomponents/issues/1000).
|
|
285
|
+
And we want to support an alternative, more semantic sounding prefix to data, say enh-*, endorsed by [this proposal](https://github.com/WICG/webcomponents/issues/1000).
|
|
277
286
|
|
|
278
287
|
Here's what the api provides:
|
|
279
288
|
|
|
@@ -291,18 +300,20 @@ const mo = new MountObserver({
|
|
|
291
300
|
'enh-my-enhancement',
|
|
292
301
|
'enh-my-enhancement-first-aspect',
|
|
293
302
|
'enh-my-enhancement-second-aspect',
|
|
303
|
+
//...some ten more combinations not listed
|
|
294
304
|
{
|
|
295
305
|
name: 'my-enhancement',
|
|
296
306
|
builtIn: true
|
|
297
307
|
},
|
|
298
308
|
{
|
|
299
|
-
name: 'my-enhancement-first-
|
|
309
|
+
name: 'my-enhancement-first-aspect',
|
|
300
310
|
builtIn: true
|
|
301
311
|
},
|
|
302
312
|
{
|
|
303
313
|
name: 'my-enhancement-second-aspect',
|
|
304
314
|
builtIn: true
|
|
305
|
-
}
|
|
315
|
+
},
|
|
316
|
+
...
|
|
306
317
|
]
|
|
307
318
|
|
|
308
319
|
}
|
|
@@ -316,30 +327,33 @@ import {MountObserver} from '../MountObserver.js';
|
|
|
316
327
|
const mo = new MountObserver({
|
|
317
328
|
on: '*',
|
|
318
329
|
whereAttr:{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
330
|
+
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
331
|
+
hasBase: 'my-enhancement',
|
|
332
|
+
hasBranchIn: ['first-aspect', 'second-aspect', ''],
|
|
333
|
+
hasLeafIn: {
|
|
334
|
+
'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
|
|
335
|
+
}
|
|
322
336
|
}
|
|
323
337
|
});
|
|
324
338
|
```
|
|
325
339
|
|
|
326
|
-
MountObserver
|
|
340
|
+
MountObserver provides a breakdown of the matching attribute when encountered:
|
|
327
341
|
|
|
328
342
|
```html
|
|
329
343
|
<div id=div>
|
|
330
|
-
<section class=hello my-enhancement-first-
|
|
344
|
+
<section class=hello my-enhancement-first-aspect-wow-this-is-deep="hello"></section>
|
|
331
345
|
</div>
|
|
332
346
|
<script type=module>
|
|
333
347
|
import {MountObserver} from '../MountObserver.js';
|
|
334
348
|
const mo = new MountObserver({
|
|
335
349
|
on: '*',
|
|
336
350
|
whereAttr:{
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
351
|
+
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
352
|
+
hasBase: 'my-enhancement',
|
|
353
|
+
hasBranchIn: ['first-aspect', 'second-aspect', ''],
|
|
354
|
+
hasLeafIn: {
|
|
355
|
+
'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
|
|
356
|
+
}
|
|
343
357
|
}
|
|
344
358
|
});
|
|
345
359
|
mo.addEventListener('observed-attr-change', e => {
|
|
@@ -347,10 +361,13 @@ MountObserver supports both approaches
|
|
|
347
361
|
// {
|
|
348
362
|
// matchingElement,
|
|
349
363
|
// attrChangeInfo:{
|
|
350
|
-
//
|
|
351
|
-
//
|
|
364
|
+
// name: 'data-my-enhancement-first-aspect-wow-this-is-deep'
|
|
365
|
+
// root: 'data',
|
|
366
|
+
// base: 'my-enhancement',
|
|
367
|
+
// branch: 'first-aspect',
|
|
368
|
+
// leaf: 'wow-this-is-deep',
|
|
352
369
|
// oldValue: null,
|
|
353
|
-
// newValue: '
|
|
370
|
+
// newValue: 'good-bye'
|
|
354
371
|
// idx: 0,
|
|
355
372
|
// }
|
|
356
373
|
// }
|
|
@@ -358,11 +375,44 @@ MountObserver supports both approaches
|
|
|
358
375
|
mo.observe(div);
|
|
359
376
|
setTimeout(() => {
|
|
360
377
|
const myCustomElement = document.querySelector('my-custom-element');
|
|
361
|
-
myCustomElement.setAttribute('my-first-
|
|
378
|
+
myCustomElement.setAttribute('data-my-enhancement-first-aspect-wow-this-is-deep', 'good-bye');
|
|
362
379
|
}, 1000);
|
|
363
380
|
</script>
|
|
364
381
|
```
|
|
365
382
|
|
|
383
|
+
Some libraries prefer to use the colon (:) rather than a dash to separate these levels of settings:
|
|
384
|
+
|
|
385
|
+
Possibly some libraries may prefer to mix it up a bit:
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
```html
|
|
389
|
+
<div id=div>
|
|
390
|
+
<section class=hello
|
|
391
|
+
data-my-enhancement=greetings
|
|
392
|
+
data-my-enhancement:first-aspect=hello
|
|
393
|
+
data-my-enhancement:second-aspect=goodbye
|
|
394
|
+
data-my-enhancement:first-aspect--wow-this-is-deep
|
|
395
|
+
data-my-enhancement:first-aspect--have-you-considered-using-json-for-this=just-saying
|
|
396
|
+
></section>
|
|
397
|
+
</div>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
To support such syntax, specify the delimiter thusly:
|
|
401
|
+
|
|
402
|
+
```JavaScript
|
|
403
|
+
const mo = new MountObserver({
|
|
404
|
+
on: '*',
|
|
405
|
+
whereAttr:{
|
|
406
|
+
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
407
|
+
hasBase: ['-', 'my-enhancement'],
|
|
408
|
+
hasBranchIn: [':', ['first-aspect', 'second-aspect', '']],
|
|
409
|
+
hasLeafIn: {
|
|
410
|
+
'first-aspect': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
366
416
|
## Preemptive downloading
|
|
367
417
|
|
|
368
418
|
There are two significant steps to imports, each of which imposes a cost:
|
|
@@ -389,4 +439,47 @@ const observer = new MountObserver({
|
|
|
389
439
|
|
|
390
440
|
So what this does is only check for the presence of an element with tag name "my-element", and it starts downloading the resource, even before the element has "mounted" based on other criteria.
|
|
391
441
|
|
|
442
|
+
## Birtual Inclusions
|
|
443
|
+
|
|
444
|
+
This proposal "sneaks in" one more feature, that perhaps should stand separately as its own proposal. Because the MountObserver api allows us to attach behaviors on the fly based on css matching, and because the MountObserver would provide developers the "first point of contact" for such functionality, the efficiency argument seemingly "screams out" for this feature.
|
|
445
|
+
|
|
446
|
+
The mount-observer is always on the lookout for a special tag that takes the form:
|
|
447
|
+
|
|
448
|
+
```html
|
|
449
|
+
<div>Some prior stuff</div>
|
|
450
|
+
<b-i href=#my-snippet-of-html>
|
|
451
|
+
<div slot=slot1>hello</div>
|
|
452
|
+
<div slot=slot2>goodbye<div>
|
|
453
|
+
</b-i>
|
|
454
|
+
<div>Some additional stuff</div>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
"b-i" stands for "birtual inclusion" where birtual is a made up word that is a portmanteau of birth and virtual.
|
|
458
|
+
|
|
459
|
+
When it encounters such a thing, it searches "upwardly" through the chain of ShadowRoots for a template with id=my-kinda-sorta-custom-element (in this case), and caches them as it finds them.
|
|
460
|
+
|
|
461
|
+
Let's say the template looks as follows:
|
|
462
|
+
|
|
463
|
+
```html
|
|
464
|
+
<template id=my-snippet-of-html>
|
|
465
|
+
This is an example of a snippet of HTML that appears repeatedly.
|
|
466
|
+
<slot name=slot1></slot>
|
|
467
|
+
<slot name=slot2></slot>
|
|
468
|
+
</template>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
What we would end up with is:
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
```html
|
|
475
|
+
<div>Some prior stuff</div>
|
|
476
|
+
This is an example of a snippet of HTML that appears repeatedly.
|
|
477
|
+
<div>hello</div>
|
|
478
|
+
<div>goodbye</div>
|
|
479
|
+
<div>Some additional stuff</div>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Some significant differences with genuine slot support as used with (ShadowDOM'd) custom elements
|
|
392
483
|
|
|
484
|
+
1. There is no mechanism for updating the slots. That is something under investigation with this userland [custom enhancement](https://github.com/bahrus/be-inclusive), that could possibly lead to a future implementation request tied to template instantiation.
|
|
485
|
+
2. ShadowDOM's slots act on a "many to one" basis. Multiple light children with identical slot identifiers all get merged into a single (first?) matching slot within the Shadow DOM. These birtual inclusions, instead, follow the opposite approach -- a single element with a slot identifier can get cloned into multiple slot targets as it weaves itself into the templates as they get merged together.
|
package/doWhereAttr.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AttrChangeEvent } from "./MountObserver.js";
|
|
2
|
+
export function doWhereAttr(whereAttr, attributeName, target, oldValue, mo) {
|
|
3
|
+
const { hasRootIn, hasBase, hasBranchIn } = whereAttr;
|
|
4
|
+
const name = attributeName;
|
|
5
|
+
let restOfName = name;
|
|
6
|
+
let root;
|
|
7
|
+
let branch;
|
|
8
|
+
let idx = 0;
|
|
9
|
+
const hasBaseIsString = typeof hasBase === 'string';
|
|
10
|
+
const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
|
|
11
|
+
const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
|
|
12
|
+
if (hasRootIn !== undefined) {
|
|
13
|
+
for (const rootTest in hasRootIn) {
|
|
14
|
+
if (restOfName.startsWith(rootTest)) {
|
|
15
|
+
root = rootTest;
|
|
16
|
+
restOfName = restOfName.substring(root.length + rootToBaseDelimiter.length);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!restOfName.startsWith(baseSelector))
|
|
22
|
+
return;
|
|
23
|
+
restOfName = restOfName.substring(hasBase.length);
|
|
24
|
+
if (hasBranchIn) {
|
|
25
|
+
for (const branchTest in hasBranchIn) {
|
|
26
|
+
if (restOfName.startsWith(branchTest)) {
|
|
27
|
+
branch = branchTest;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const newValue = target.getAttribute(attributeName);
|
|
33
|
+
const attrChangeInfo = {
|
|
34
|
+
name,
|
|
35
|
+
root,
|
|
36
|
+
base: baseSelector,
|
|
37
|
+
branch,
|
|
38
|
+
oldValue,
|
|
39
|
+
newValue,
|
|
40
|
+
idx
|
|
41
|
+
};
|
|
42
|
+
mo.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
+
const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
|
|
3
|
+
const fullListOfAttrs = [];
|
|
4
|
+
//TODO: share this block with doWhereAttr?
|
|
5
|
+
const hasBaseIsString = typeof hasBase === 'string';
|
|
6
|
+
const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
|
|
7
|
+
const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
|
|
8
|
+
//end TODO
|
|
9
|
+
let prefixLessMatches = [baseSelector];
|
|
10
|
+
if (hasBranchIn !== undefined) {
|
|
11
|
+
let baseToBranchDelimiter = '-';
|
|
12
|
+
let branches;
|
|
13
|
+
if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
|
|
14
|
+
baseToBranchDelimiter = hasBranchIn[0];
|
|
15
|
+
branches = hasBranchIn[1];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
branches = hasBranchIn;
|
|
19
|
+
}
|
|
20
|
+
prefixLessMatches = branches.map(x => `${baseSelector}${baseToBranchDelimiter}x`);
|
|
21
|
+
}
|
|
22
|
+
const stems = hasRootIn || [''];
|
|
23
|
+
for (const stem of stems) {
|
|
24
|
+
const prefix = typeof stem === 'string' ? stem : stem.path;
|
|
25
|
+
for (const prefixLessMatch of prefixLessMatches) {
|
|
26
|
+
fullListOfAttrs.push(prefix.length === 0 ? prefixLessMatch : `${prefix}${rootToBaseDelimiter}${prefixLessMatch}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const listOfSelectors = fullListOfAttrs.map(s => `${withoutAttrs}[${s}]`);
|
|
30
|
+
const calculatedSelector = listOfSelectors.join(',');
|
|
31
|
+
return {
|
|
32
|
+
fullListOfAttrs,
|
|
33
|
+
calculatedSelector
|
|
34
|
+
};
|
|
35
|
+
}
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
export interface MountInit{
|
|
2
2
|
readonly on?: CSSMatch,
|
|
3
|
-
readonly attribMatches?: Array<AttribMatch>,
|
|
4
|
-
readonly
|
|
5
|
-
|
|
6
|
-
andQualifiers?: Array<string>,
|
|
7
|
-
withStemsIn?: Array<string | {
|
|
8
|
-
stem: string,
|
|
9
|
-
context: 'BuiltIn' | 'CustomElement'
|
|
10
|
-
}>,
|
|
11
|
-
},
|
|
3
|
+
//readonly attribMatches?: Array<AttribMatch>,
|
|
4
|
+
readonly withTargetShadowRoot?: ShadowRoot,
|
|
5
|
+
readonly whereAttr?: WhereAttr,
|
|
12
6
|
readonly whereElementIntersectsWith?: IntersectionObserverInit,
|
|
13
7
|
readonly whereMediaMatches?: MediaQuery,
|
|
14
8
|
readonly whereInstanceOf?: Array<typeof Node>, //[TODO] What's the best way to type this?,
|
|
@@ -27,6 +21,14 @@ export interface MountInit{
|
|
|
27
21
|
// */
|
|
28
22
|
// readonly ignoreInitialMatches?: boolean,
|
|
29
23
|
}
|
|
24
|
+
export interface WhereAttr{
|
|
25
|
+
hasBase: string | [string, string],
|
|
26
|
+
hasBranchIn?: Array<string> | [string, Array<string>],
|
|
27
|
+
hasRootIn?: Array<string | {
|
|
28
|
+
path: string,
|
|
29
|
+
context: 'BuiltIn' | 'CustomElement' | 'Both'
|
|
30
|
+
}>,
|
|
31
|
+
}
|
|
30
32
|
type CSSMatch = string;
|
|
31
33
|
type ImportString = string;
|
|
32
34
|
type MediaQuery = string;
|
|
@@ -70,6 +72,10 @@ export interface AddMutationEventListener {
|
|
|
70
72
|
|
|
71
73
|
interface AttrChangeInfo{
|
|
72
74
|
name: string,
|
|
75
|
+
root?: string,
|
|
76
|
+
base?: string,
|
|
77
|
+
branch?: string,
|
|
78
|
+
leaf?: string, //TODO
|
|
73
79
|
oldValue: string | null,
|
|
74
80
|
newValue: string | null,
|
|
75
81
|
idx: number,
|
|
@@ -119,9 +125,20 @@ export type attrChangeEventName = 'attr-change';
|
|
|
119
125
|
export interface IAttrChangeEvent extends IMountEvent {
|
|
120
126
|
attrChangeInfo: AttrChangeInfo,
|
|
121
127
|
}
|
|
122
|
-
export type
|
|
123
|
-
export interface
|
|
124
|
-
addEventListener(eventName: attrChangeEventName, handler:
|
|
128
|
+
export type attrChangeEventHandler = (e: IAttrChangeEvent) => void;
|
|
129
|
+
export interface AddAttrChangeEventListener{
|
|
130
|
+
addEventListener(eventName: attrChangeEventName, handler: attrChangeEventHandler, options?: AddEventListenerOptions): void;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
|
|
134
|
+
//#region load event
|
|
135
|
+
export type loadEventName = 'load';
|
|
136
|
+
export interface ILoadEvent {
|
|
137
|
+
clone: DocumentFragment
|
|
138
|
+
}
|
|
139
|
+
export type loadEventHandler = (e: ILoadEvent) => void;
|
|
140
|
+
export interface AddLoadEventListener{
|
|
141
|
+
addEventListener(eventName: loadEventName, handler: loadEventHandler, options?: AddEventListenerOptions): void
|
|
125
142
|
}
|
|
126
143
|
//#endregion
|
|
127
144
|
|