mount-observer 0.0.19 → 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,9 +15,10 @@ export class MountObserver extends EventTarget {
15
15
  super();
16
16
  const { on, whereElementIntersectsWith, whereMediaMatches } = init;
17
17
  let isComplex = false;
18
+ //TODO: study this problem further. Starting to think this is basically not polyfillable
18
19
  if (on !== undefined) {
19
20
  const reducedMatch = on.replaceAll(':not(', '');
20
- isComplex = reducedMatch.includes(' ') || reducedMatch.includes(':');
21
+ isComplex = reducedMatch.includes(' ') || (reducedMatch.includes(':') && reducedMatch.includes('('));
21
22
  }
22
23
  this.#isComplex = isComplex;
23
24
  if (whereElementIntersectsWith || whereMediaMatches)
@@ -40,7 +41,7 @@ export class MountObserver extends EventTarget {
40
41
  if (whereAttr === undefined)
41
42
  return withoutAttrs;
42
43
  const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
43
- const info = getWhereAttrSelector(whereAttr, withoutAttrs);
44
+ const info = await getWhereAttrSelector(whereAttr, withoutAttrs);
44
45
  const { fullListOfAttrs, calculatedSelector, partitionedAttrs } = info;
45
46
  this.#fullListOfAttrs = fullListOfAttrs;
46
47
  this.#attrParts = partitionedAttrs;
@@ -151,6 +152,7 @@ export class MountObserver extends EventTarget {
151
152
  const parts = this.#attrParts[idx];
152
153
  const attrChangeInfo = {
153
154
  oldValue,
155
+ name: attributeName,
154
156
  newValue,
155
157
  idx,
156
158
  parts
@@ -244,6 +246,7 @@ export class MountObserver extends EventTarget {
244
246
  idx,
245
247
  newValue,
246
248
  oldValue,
249
+ name,
247
250
  parts
248
251
  });
249
252
  }
@@ -350,7 +353,7 @@ export class DisconnectEvent extends Event {
350
353
  export class AttrChangeEvent extends Event {
351
354
  mountedElement;
352
355
  attrChangeInfos;
353
- static eventName = 'attr-change';
356
+ static eventName = 'attrChange';
354
357
  constructor(mountedElement, attrChangeInfos) {
355
358
  super(AttrChangeEvent.eventName);
356
359
  this.mountedElement = mountedElement;
package/README.md CHANGED
@@ -7,11 +7,11 @@ Note that much of what is described below has not yet been polyfilled.
7
7
 
8
8
  # The MountObserver api.
9
9
 
10
- Author: Bruce B. Anderson (with valuable feedback from [doeixd](https://github.com/doeixd) )
10
+ 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-4
14
+ Last Update: 2024-5-21
15
15
 
16
16
  ## Benefits of this API
17
17
 
@@ -72,6 +72,8 @@ observer.observe(document);
72
72
 
73
73
  Invoking "disconnect" as shown above causes the observer to emit event "disconnectedCallback".
74
74
 
75
+ The argument can also be an array of objects that fit the pattern shown above.
76
+
75
77
  If no imports are specified, it would go straight to do.* (if any such callbacks are specified), and it will also dispatch events as discussed below.
76
78
 
77
79
  This only searches for elements matching 'my-element' outside any shadow DOM.
@@ -235,7 +237,41 @@ If an element that is in "mounted" state according to a MountObserver instance i
235
237
  3) If the mounted element is added outside the rootNode being observed, the mountObserver instance will dispatch event "exit", and the MountObserver instance will relinquish any further responsibility for this element.
236
238
  4) Ideally event "forget" would be dispatched just before the platform garbage collects an element the MountObserver instance is still monitoring, after all hard references are relinquished (or is that self-contradictory?).
237
239
  5) If the new place it was added remains within the original rootNode and remains mounted, the MountObserver instance dispatches event "reconfirmed".
238
- 6) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
240
+ 6) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
241
+
242
+ ## Dismounting
243
+
244
+ In many cases, it will be critical to inform the developer **why** the element no longer satisfies all the criteria. For example, we may be using an intersection observer, and when we've scrolled away from view, we can "shut down" until the element is (nearly) scrolled back into view. We may also be displaying things differently depending on the network speed. How we should respond when one of the original conditions, but not the other, no longer applies, is of paramount importance.
245
+
246
+ So the dismount event should provide a "checklist" of all the conditions, and their current value:
247
+
248
+ ```JavaScript
249
+ mediaMatches: true,
250
+ containerMatches: true,
251
+ satisifiesCustomCondition: true,
252
+ whereLangIn: ['en-GB'],
253
+ whereConnection:{
254
+ effectiveTypeMatches: true
255
+ },
256
+ isIntersecting: false,
257
+ changedConditions: ['isIntersecting']
258
+ ```
259
+
260
+ ## Get play-by-play updates?
261
+
262
+ An issue raised by @doeixd, I think, is what if we want to be informed of the status of all the conditions that are applicable to an element being mounted / dismounted? I can see scenarios where this would be useful, for reasons similar to wanting to know why the element dismounted.
263
+
264
+ Since this could have a negative impact on performance, I think it should be something we opt-in to:
265
+
266
+ ```JavaScript
267
+ getPlayByPlay: true
268
+ ```
269
+
270
+ Now the question is when should this progress reporting start? It could either start the moment the element becomes mounted the first time. Or it could happen the moment any of the conditions are satisfied. But some of the conditions could be trivially satisfied for the vast majority of elements (e.g. network speed is 4g or greater).
271
+
272
+ So I believe the prudent thing to do is wait for all the conditions to be satisfied, before engaging in this kind of commentary, i.e. after the first mount.
273
+
274
+ The alternative to providing this feature, which I'm leaning towards, is to just ask the developer to create "specialized" mountObserver construction arguments, that turn on and off precisely when the developer needs to know.
239
275
 
240
276
  ## A tribute to attributes
241
277
 
@@ -245,40 +281,59 @@ Being that for both custom elements, as well as (hopefully) [custom enhancements
245
281
 
246
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.
247
283
 
248
- <!--
249
- ### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
284
+ ## A key attribute of attributes
250
285
 
251
- 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:
252
287
 
253
- ```html
254
- <div id=div>
255
- <my-custom-element my-first-observed-attribute="hello"></my-custom-element>
256
- </div>
257
- <script type=module>
258
- import {MountObserver} from '../MountObserver.js';
259
- const mo = new MountObserver({
260
- on: '*',
261
- whereInstanceOf: [MyCustomElement]
262
- });
263
- mo.addEventListener('parsed-attrs-changed', e => {
264
- const {matchingElement, modifiedObjectFieldValues, preModifiedFieldValues} = e;
265
- console.log({matchingElement, modifiedObjectFieldValues, preModifiedFieldValues});
266
- });
267
- mo.observe(div);
268
- setTimeout(() => {
269
- const myCustomElement = document.querySelector('my-custom-element');
270
- myCustomElement.setAttribute('my-first-observed-attribute', 'good-bye');
271
- }, 1000);
272
- </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
+ });
273
324
  ```
274
- -->
275
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).
276
331
 
277
332
  ### Custom Enhancements in userland
278
333
 
279
- [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).
280
335
 
281
- 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.
282
337
 
283
338
  Suppose we have a (progressive) enhancement that we want to apply based on the presence of 1 or more attributes.
284
339
 
@@ -316,7 +371,7 @@ We want to also support:
316
371
 
317
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-*).
318
373
 
319
- 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?
320
375
 
321
376
  So let's say we want to insist that on custom elements, we must have the data- prefix?
322
377
 
@@ -324,7 +379,9 @@ And we want to support an alternative, more semantic sounding prefix to data, sa
324
379
 
325
380
  Here's what the api **doesn't** provide (as originally proposed):
326
381
 
327
- ## 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:
328
385
 
329
386
  ```JavaScript
330
387
  import {MountObserver} from '../MountObserver.js';
@@ -358,7 +415,9 @@ const mo = new MountObserver({
358
415
  });
359
416
  ```
360
417
 
361
- ## Supported -- The DRY Way
418
+ #### The DRY Way
419
+
420
+ This seems like a much better approach, and is supported by this proposal:
362
421
 
363
422
  ```JavaScript
364
423
  import {MountObserver} from '../MountObserver.js';
@@ -394,7 +453,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
394
453
  }
395
454
  }
396
455
  });
397
- mo.addEventListener('observed-attr-change', e => {
456
+ mo.addEventListener('attrChange', e => {
398
457
  console.log(e);
399
458
  // {
400
459
  // matchingElement,
@@ -465,6 +524,8 @@ Tentative rules:
465
524
 
466
525
  The thinking here is that longer roots indicate higher "specificity", so it is safer to use that one.
467
526
 
527
+
528
+
468
529
  ## Preemptive downloading
469
530
 
470
531
  There are two significant steps to imports, each of which imposes a cost:
@@ -481,7 +542,7 @@ So for this we add option:
481
542
  ```JavaScript
482
543
  const observer = new MountObserver({
483
544
  on: 'my-element',
484
- loading: 'eager',
545
+ loadingEagerness: 'eager',
485
546
  import: './my-element.js',
486
547
  do:{
487
548
  mount: (matchingElement, {modules}) => customElements.define(modules[0].MyElement)
@@ -491,13 +552,16 @@ const observer = new MountObserver({
491
552
 
492
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.
493
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
+
494
558
  ## Intra document html imports
495
559
 
496
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.
497
561
 
498
562
  Also, this proposal is partly focused on better management of importing resources "from a distance", in particular via imports carried out via http. Is it such a stretch to look closely at scenarios where that distance happens to be shorter, i.e. found somewhere [in the document tree structure](https://github.com/tc39/proposal-module-expressions)?
499
563
 
500
- The mount-observer is always on the lookout for template tags with an href attribute starting with #:
564
+ The mount-observer is always on the lookout for template tags with a src attribute starting with #:
501
565
 
502
566
  ```html
503
567
  <template src=#id-of-source-template></template>
@@ -521,7 +585,7 @@ Let's say the source template looks as follows:
521
585
 
522
586
  ```html
523
587
  <template id=id-of-source-template>
524
- I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot>
588
+ <div>I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot></div>
525
589
  </template>
526
590
  ```
527
591
 
@@ -583,3 +647,13 @@ This proposal (and polyfill) also supports the option to utilize ShadowDOM / slo
583
647
  </div>
584
648
  ```
585
649
 
650
+ > [!NOTE]
651
+ > An intriguing sounding alternative to using the template tag that disappears, as shown above, is to use a new tag for this purpose. I think something along the lines of what is [proposed here](https://github.com/WICG/webcomponents/issues/1059) has a much better semantic ring to it:
652
+
653
+ ```html
654
+ <compose src="#sharedHeader"></compose>
655
+ <compose src="#productCard"></compose>
656
+ ```
657
+
658
+ The discussion there leads to an open question whether a processing instruction would be better. I think the compose tag would make much more sense, vs a processing instruction, as it could then support slotted children (behaving similar to the Beatles' example above). Or maybe another tag should be introduced that is the equivalent of the slot, to avoid confusion. or some equivalent. But I strongly suspect that could significantly reduce the payload size of some documents, if we can reuse blocks of HTML, inserting sections of customized content for each instance.
659
+
@@ -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.19",
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
  /**
@@ -66,7 +67,7 @@ export interface MountContext{
66
67
  }
67
68
 
68
69
  type PipelineStage = 'Inspecting' | 'PreImport' | 'PostImport' | 'Import'
69
- export type PipelineProcessor<ReturnType = void> = (matchingElement: Element, observer: IMountObserver, ctx: MountContext) => Promise<ReturnType>;
70
+ export type PipelineProcessor<ReturnType = void> = (matchingElement: Element, observer: IMountObserver, ctx: MountContext) => Promise<ReturnType> | ReturnType;
70
71
 
71
72
  //#region mutation event
72
73
  export type mutationEventName = 'mutation-event';
@@ -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
  }