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 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: further this problem further. Starting to think this is basically not polyfillable
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 = 'attr-change';
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-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
- Example:
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
- ```html
290
- <div id=div>
291
- <my-custom-element my-first-observed-attribute="hello"></my-custom-element>
292
- </div>
293
- <script type=module>
294
- import {MountObserver} from '../MountObserver.js';
295
- const mo = new MountObserver({
296
- on: '*',
297
- whereInstanceOf: [MyCustomElement]
298
- });
299
- mo.addEventListener('parsed-attrs-changed', e => {
300
- const {matchingElement, modifiedObjectFieldValues, preModifiedFieldValues} = e;
301
- console.log({matchingElement, modifiedObjectFieldValues, preModifiedFieldValues});
302
- });
303
- mo.observe(div);
304
- setTimeout(() => {
305
- const myCustomElement = document.querySelector('my-custom-element');
306
- myCustomElement.setAttribute('my-first-observed-attribute', 'good-bye');
307
- }, 1000);
308
- </script>
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
- ## Rejected option -- The carpal syndrome syntax
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
- ## Supported -- The DRY Way
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('observed-attr-change', e => {
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
- loading: 'eager',
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.
@@ -1,72 +1,74 @@
1
- export function getWhereAttrSelector(whereAttr, withoutAttrs) {
2
- const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
3
- const hasRootInGuaranteed = hasRootIn || [{
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
- let prefixLessMatches = [];
10
- const hasBaseIsString = typeof hasBase === 'string';
11
- const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
12
- const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
13
- if (hasBranchIn !== undefined) {
14
- let baseToBranchDelimiter = '-';
15
- let branches;
16
- if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
17
- baseToBranchDelimiter = hasBranchIn[0];
18
- branches = hasBranchIn[1];
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
- branches = hasBranchIn;
32
+ prefixLessMatches.push({
33
+ rootToBaseDelimiter,
34
+ base: baseSelector,
35
+ });
22
36
  }
23
- prefixLessMatches = branches.map(x => ({
24
- rootToBaseDelimiter,
25
- base: baseSelector,
26
- baseToBranchDelimiter: x ? baseToBranchDelimiter : '',
27
- branch: x
28
- }));
29
- }
30
- else {
31
- prefixLessMatches.push({
32
- rootToBaseDelimiter,
33
- base: baseSelector,
34
- });
35
- }
36
- for (const rootCnfg of hasRootInGuaranteed) {
37
- const { start } = rootCnfg;
38
- for (const match of prefixLessMatches) {
39
- const { base, baseToBranchDelimiter, branch, rootToBaseDelimiter } = match;
40
- let branchIdx = 0;
41
- for (const prefixLessMatch of prefixLessMatches) {
42
- const { base, baseToBranchDelimiter, branch } = prefixLessMatch;
43
- const startAndRootToBaseDelimiter = start ? `${start}${rootToBaseDelimiter}` : '';
44
- //TODO: could probably reduce the size of the code below
45
- if (branch) {
46
- //will always have branch?
47
- const name = `${startAndRootToBaseDelimiter}${base}${baseToBranchDelimiter}${branch}`;
48
- fullListOfAttrs.push(name);
49
- partitionedAttrs.push({
50
- root: start,
51
- name,
52
- base,
53
- branch,
54
- branchIdx,
55
- rootCnfg
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
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
- hasBase: string | [delimiter, string],
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 = 'attr-change';
143
+ export type attrChangeEventName = 'attrChange';
143
144
  export interface IAttrChangeEvent extends IMountEvent {
144
145
  attrChangeInfos: Array<AttrChangeInfo>,
145
146
  }