mount-observer 0.0.20 → 0.0.21
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 +5 -3
- package/README.md +60 -32
- package/getWhereAttrSelector.js +66 -64
- package/package.json +1 -1
- package/types.d.ts +4 -3
package/MountObserver.js
CHANGED
|
@@ -15,7 +15,7 @@ export class MountObserver extends EventTarget {
|
|
|
15
15
|
super();
|
|
16
16
|
const { on, whereElementIntersectsWith, whereMediaMatches } = init;
|
|
17
17
|
let isComplex = false;
|
|
18
|
-
//TODO:
|
|
18
|
+
//TODO: study this problem further. Starting to think this is basically not polyfillable
|
|
19
19
|
if (on !== undefined) {
|
|
20
20
|
const reducedMatch = on.replaceAll(':not(', '');
|
|
21
21
|
isComplex = reducedMatch.includes(' ') || (reducedMatch.includes(':') && reducedMatch.includes('('));
|
|
@@ -41,7 +41,7 @@ export class MountObserver extends EventTarget {
|
|
|
41
41
|
if (whereAttr === undefined)
|
|
42
42
|
return withoutAttrs;
|
|
43
43
|
const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
|
|
44
|
-
const info = getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
44
|
+
const info = await getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
45
45
|
const { fullListOfAttrs, calculatedSelector, partitionedAttrs } = info;
|
|
46
46
|
this.#fullListOfAttrs = fullListOfAttrs;
|
|
47
47
|
this.#attrParts = partitionedAttrs;
|
|
@@ -152,6 +152,7 @@ export class MountObserver extends EventTarget {
|
|
|
152
152
|
const parts = this.#attrParts[idx];
|
|
153
153
|
const attrChangeInfo = {
|
|
154
154
|
oldValue,
|
|
155
|
+
name: attributeName,
|
|
155
156
|
newValue,
|
|
156
157
|
idx,
|
|
157
158
|
parts
|
|
@@ -245,6 +246,7 @@ export class MountObserver extends EventTarget {
|
|
|
245
246
|
idx,
|
|
246
247
|
newValue,
|
|
247
248
|
oldValue,
|
|
249
|
+
name,
|
|
248
250
|
parts
|
|
249
251
|
});
|
|
250
252
|
}
|
|
@@ -351,7 +353,7 @@ export class DisconnectEvent extends Event {
|
|
|
351
353
|
export class AttrChangeEvent extends Event {
|
|
352
354
|
mountedElement;
|
|
353
355
|
attrChangeInfos;
|
|
354
|
-
static eventName = '
|
|
356
|
+
static eventName = 'attrChange';
|
|
355
357
|
constructor(mountedElement, attrChangeInfos) {
|
|
356
358
|
super(AttrChangeEvent.eventName);
|
|
357
359
|
this.mountedElement = mountedElement;
|
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-21
|
|
15
15
|
|
|
16
16
|
## Benefits of this API
|
|
17
17
|
|
|
@@ -281,40 +281,59 @@ Being that for both custom elements, as well as (hopefully) [custom enhancements
|
|
|
281
281
|
|
|
282
282
|
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.
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
|
|
284
|
+
## A key attribute of attributes
|
|
286
285
|
|
|
287
|
-
|
|
286
|
+
I think it is useful to divide [attributes](https://jakearchibald.com/2024/attributes-vs-properties/) that we we would want to observe into two categories:
|
|
288
287
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
288
|
+
1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of the truth".
|
|
289
|
+
|
|
290
|
+
Examples are many built-in global attributes, like lang, or contenteditable, or more specialized examples such as "content" for the meta tag. Often, setting the property values corresponding to these attributes results in directly reflecting those property values to the attributes (perhaps in a round about way). And there are usually 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 is often the most effective way of observing when the property/attribute state for these elements change. Some attributes of custom elements may fit this category (but maybe a minority of them).
|
|
291
|
+
|
|
292
|
+
And in some application environments, adjusting state via attributes may be the preferred approach, so we want to support this scenario, even if it doesn't abide by a common view of what constitutes "best practices." Again, the distinguishing feature of the attributes that we would want to monitor in this way is that they are "top-level" and unlikely to differ in name across different Shadow DOM scopes.
|
|
293
|
+
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
We want our api to be able to distinguish between these two, and to be able to combine both types in one mount observer instance.
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
### Attributes that are the "source of truth"
|
|
300
|
+
|
|
301
|
+
So for the first scenario, we can specify attributes to listen for as follows:
|
|
302
|
+
|
|
303
|
+
```JavaScript
|
|
304
|
+
import {MountObserver} from 'mount-observer/MountObserver.js';
|
|
305
|
+
const mo = new MountObserver({
|
|
306
|
+
on: '*',
|
|
307
|
+
whereAttr:{
|
|
308
|
+
isIn: ['lang', 'contenteditable']
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
mo.addEventListener('attrChange', e => {
|
|
313
|
+
console.log(e);
|
|
314
|
+
// {
|
|
315
|
+
// matchingElement,
|
|
316
|
+
// attrChangeInfo:[{
|
|
317
|
+
// idx: 0,
|
|
318
|
+
// name: 'lang'
|
|
319
|
+
// oldValue: null,
|
|
320
|
+
// newValue: 'en-GB',
|
|
321
|
+
// }]
|
|
322
|
+
// }
|
|
323
|
+
});
|
|
309
324
|
```
|
|
310
|
-
-->
|
|
311
325
|
|
|
326
|
+
### Help with parsing?
|
|
327
|
+
|
|
328
|
+
This proposal is likely to evolve going forward, attempting to synthesize [separate ideas](https://github.com/WICG/webcomponents/issues/1045) for declaratively specifying how to interpret the attributes, parsing them so that they may be merged into properties of a class instance.
|
|
329
|
+
|
|
330
|
+
But for now, such support is not part of this proposal (though we can see a glimpse of what that support might look like below).
|
|
312
331
|
|
|
313
332
|
### Custom Enhancements in userland
|
|
314
333
|
|
|
315
|
-
[This proposal could take quite a while to see the light of day, if ever](https://github.com/WICG/webcomponents/issues/1000).
|
|
334
|
+
[This proposal, support for (progressive) enhancement of built-in or third-party custom elements, could take quite a while to see the light of day, if ever](https://github.com/WICG/webcomponents/issues/1000).
|
|
316
335
|
|
|
317
|
-
In the meantime, we want to provide the most help for providing for custom enhancements in userland, and for any other kind of (progressive) enhancement based on attributes going forward.
|
|
336
|
+
In the meantime, we want to provide the most help for providing for custom enhancements in userland, and for any other kind of (progressive) enhancement based on (server-rendered) attributes going forward.
|
|
318
337
|
|
|
319
338
|
Suppose we have a (progressive) enhancement that we want to apply based on the presence of 1 or more attributes.
|
|
320
339
|
|
|
@@ -352,7 +371,7 @@ We want to also support:
|
|
|
352
371
|
|
|
353
372
|
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-*).
|
|
354
373
|
|
|
355
|
-
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?
|
|
374
|
+
But now when we consider applying this enhancement to third party custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement?
|
|
356
375
|
|
|
357
376
|
So let's say we want to insist that on custom elements, we must have the data- prefix?
|
|
358
377
|
|
|
@@ -360,7 +379,9 @@ And we want to support an alternative, more semantic sounding prefix to data, sa
|
|
|
360
379
|
|
|
361
380
|
Here's what the api **doesn't** provide (as originally proposed):
|
|
362
381
|
|
|
363
|
-
|
|
382
|
+
#### The carpal syndrome syntax
|
|
383
|
+
|
|
384
|
+
Using the same expression structure as above, we would end up with this avalanche of settings:
|
|
364
385
|
|
|
365
386
|
```JavaScript
|
|
366
387
|
import {MountObserver} from '../MountObserver.js';
|
|
@@ -394,7 +415,9 @@ const mo = new MountObserver({
|
|
|
394
415
|
});
|
|
395
416
|
```
|
|
396
417
|
|
|
397
|
-
|
|
418
|
+
#### The DRY Way
|
|
419
|
+
|
|
420
|
+
This seems like a much better approach, and is supported by this proposal:
|
|
398
421
|
|
|
399
422
|
```JavaScript
|
|
400
423
|
import {MountObserver} from '../MountObserver.js';
|
|
@@ -430,7 +453,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
430
453
|
}
|
|
431
454
|
}
|
|
432
455
|
});
|
|
433
|
-
mo.addEventListener('
|
|
456
|
+
mo.addEventListener('attrChange', e => {
|
|
434
457
|
console.log(e);
|
|
435
458
|
// {
|
|
436
459
|
// matchingElement,
|
|
@@ -501,6 +524,8 @@ Tentative rules:
|
|
|
501
524
|
|
|
502
525
|
The thinking here is that longer roots indicate higher "specificity", so it is safer to use that one.
|
|
503
526
|
|
|
527
|
+
|
|
528
|
+
|
|
504
529
|
## Preemptive downloading
|
|
505
530
|
|
|
506
531
|
There are two significant steps to imports, each of which imposes a cost:
|
|
@@ -517,7 +542,7 @@ So for this we add option:
|
|
|
517
542
|
```JavaScript
|
|
518
543
|
const observer = new MountObserver({
|
|
519
544
|
on: 'my-element',
|
|
520
|
-
|
|
545
|
+
loadingEagerness: 'eager',
|
|
521
546
|
import: './my-element.js',
|
|
522
547
|
do:{
|
|
523
548
|
mount: (matchingElement, {modules}) => customElements.define(modules[0].MyElement)
|
|
@@ -527,6 +552,9 @@ const observer = new MountObserver({
|
|
|
527
552
|
|
|
528
553
|
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.
|
|
529
554
|
|
|
555
|
+
> [!NOTE]
|
|
556
|
+
> 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.
|
|
557
|
+
|
|
530
558
|
## Intra document html imports
|
|
531
559
|
|
|
532
560
|
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.
|
package/getWhereAttrSelector.js
CHANGED
|
@@ -1,72 +1,74 @@
|
|
|
1
|
-
export function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
-
const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
|
|
3
|
-
const
|
|
4
|
-
start: '',
|
|
5
|
-
context: 'Both'
|
|
6
|
-
}];
|
|
7
|
-
const fullListOfAttrs = [];
|
|
1
|
+
export async function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
+
const { hasBase, hasBranchIn, hasRootIn, isIn } = whereAttr;
|
|
3
|
+
const fullListOfAttrs = isIn !== undefined ? [...isIn] : [];
|
|
8
4
|
const partitionedAttrs = [];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
if (hasBase !== undefined) {
|
|
6
|
+
const hasRootInGuaranteed = hasRootIn || [{
|
|
7
|
+
start: '',
|
|
8
|
+
context: 'Both'
|
|
9
|
+
}];
|
|
10
|
+
let prefixLessMatches = [];
|
|
11
|
+
const hasBaseIsString = typeof hasBase === 'string';
|
|
12
|
+
const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
|
|
13
|
+
const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
|
|
14
|
+
if (hasBranchIn !== undefined) {
|
|
15
|
+
let baseToBranchDelimiter = '-';
|
|
16
|
+
let branches;
|
|
17
|
+
if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
|
|
18
|
+
baseToBranchDelimiter = hasBranchIn[0];
|
|
19
|
+
branches = hasBranchIn[1];
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
branches = hasBranchIn;
|
|
23
|
+
}
|
|
24
|
+
prefixLessMatches = branches.map(x => ({
|
|
25
|
+
rootToBaseDelimiter,
|
|
26
|
+
base: baseSelector,
|
|
27
|
+
baseToBranchDelimiter: x ? baseToBranchDelimiter : '',
|
|
28
|
+
branch: x
|
|
29
|
+
}));
|
|
19
30
|
}
|
|
20
31
|
else {
|
|
21
|
-
|
|
32
|
+
prefixLessMatches.push({
|
|
33
|
+
rootToBaseDelimiter,
|
|
34
|
+
base: baseSelector,
|
|
35
|
+
});
|
|
22
36
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
const name = `${startAndRootToBaseDelimiter}${base}`;
|
|
60
|
-
fullListOfAttrs.push(name);
|
|
61
|
-
partitionedAttrs.push({
|
|
62
|
-
root: start,
|
|
63
|
-
name,
|
|
64
|
-
base,
|
|
65
|
-
rootCnfg,
|
|
66
|
-
branchIdx
|
|
67
|
-
});
|
|
37
|
+
for (const rootCnfg of hasRootInGuaranteed) {
|
|
38
|
+
const { start } = rootCnfg;
|
|
39
|
+
for (const match of prefixLessMatches) {
|
|
40
|
+
const { base, baseToBranchDelimiter, branch, rootToBaseDelimiter } = match;
|
|
41
|
+
let branchIdx = 0;
|
|
42
|
+
for (const prefixLessMatch of prefixLessMatches) {
|
|
43
|
+
const { base, baseToBranchDelimiter, branch } = prefixLessMatch;
|
|
44
|
+
const startAndRootToBaseDelimiter = start ? `${start}${rootToBaseDelimiter}` : '';
|
|
45
|
+
//TODO: could probably reduce the size of the code below
|
|
46
|
+
if (branch) {
|
|
47
|
+
//will always have branch?
|
|
48
|
+
const name = `${startAndRootToBaseDelimiter}${base}${baseToBranchDelimiter}${branch}`;
|
|
49
|
+
fullListOfAttrs.push(name);
|
|
50
|
+
partitionedAttrs.push({
|
|
51
|
+
root: start,
|
|
52
|
+
name,
|
|
53
|
+
base,
|
|
54
|
+
branch,
|
|
55
|
+
branchIdx,
|
|
56
|
+
rootCnfg
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const name = `${startAndRootToBaseDelimiter}${base}`;
|
|
61
|
+
fullListOfAttrs.push(name);
|
|
62
|
+
partitionedAttrs.push({
|
|
63
|
+
root: start,
|
|
64
|
+
name,
|
|
65
|
+
base,
|
|
66
|
+
rootCnfg,
|
|
67
|
+
branchIdx
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
branchIdx++;
|
|
68
71
|
}
|
|
69
|
-
branchIdx++;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
}
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -30,7 +30,8 @@ export interface RootCnfg{
|
|
|
30
30
|
//export type RootAttrOptions = Array<string | RootCnfg>;
|
|
31
31
|
export type delimiter = string;
|
|
32
32
|
export interface WhereAttr{
|
|
33
|
-
|
|
33
|
+
isIn?: Array<string>,
|
|
34
|
+
hasBase?: string | [delimiter, string],
|
|
34
35
|
hasBranchIn?: Array<string> | [delimiter, Array<string>],
|
|
35
36
|
hasRootIn?: Array<RootCnfg>,
|
|
36
37
|
/**
|
|
@@ -96,8 +97,8 @@ interface AttrChangeInfo{
|
|
|
96
97
|
oldValue: string | null,
|
|
97
98
|
newValue: string | null,
|
|
98
99
|
idx: number,
|
|
100
|
+
name: string,
|
|
99
101
|
parts: AttrParts,
|
|
100
|
-
//parsedNewValue?: any,
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
//#region mount event
|
|
@@ -139,7 +140,7 @@ export interface AddDisconnectEventListener {
|
|
|
139
140
|
//endregion
|
|
140
141
|
|
|
141
142
|
//#region attribute change event
|
|
142
|
-
export type attrChangeEventName = '
|
|
143
|
+
export type attrChangeEventName = 'attrChange';
|
|
143
144
|
export interface IAttrChangeEvent extends IMountEvent {
|
|
144
145
|
attrChangeInfos: Array<AttrChangeInfo>,
|
|
145
146
|
}
|