mount-observer 0.0.18 → 0.0.19

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
@@ -74,7 +74,7 @@ export class MountObserver extends EventTarget {
74
74
  this.#templLookUp.set(id, templ);
75
75
  return templ;
76
76
  }
77
- unobserve(within) {
77
+ disconnect(within) {
78
78
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
79
79
  const currentCount = refCount.get(nodeToMonitor);
80
80
  if (currentCount !== undefined) {
@@ -98,6 +98,7 @@ export class MountObserver extends EventTarget {
98
98
  console.warn(refCountErr);
99
99
  }
100
100
  }
101
+ this.dispatchEvent(new Event('disconnectedCallback'));
101
102
  }
102
103
  async observe(within) {
103
104
  await this.#selector();
@@ -314,7 +315,7 @@ export class MountObserver extends EventTarget {
314
315
  }
315
316
  }
316
317
  const refCountErr = 'mount-observer ref count mismatch';
317
- export const inclTemplQry = 'template[href^="#"]:not([hidden])';
318
+ export const inclTemplQry = 'template[src^="#"]:not([hidden])';
318
319
  // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
319
320
  /**
320
321
  * The `mutation-event` event represents something that happened.
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
10
+ Author: Bruce B. Anderson (with valuable feedback from [doeixd](https://github.com/doeixd) )
11
11
 
12
12
  Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
13
13
 
14
- Last Update: 2024-2-20
14
+ Last Update: 2024-5-4
15
15
 
16
16
  ## Benefits of this API
17
17
 
@@ -19,7 +19,8 @@ What follows is a far more ambitious alternative to the [lazy custom element pro
19
19
 
20
20
  ["Binding from a distance"](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc, to the HTML as it streams in. Libraries that take this approach include [Corset](https://corset.dev/) and [trans-render](https://github.com/bahrus/trans-render). The concept has been promoted by a [number](https://bkardell.com/blog/CSSLike.html) [of](https://www.w3.org/TR/NOTE-AS) [prominent](https://www.xanthir.com/blog/b4K_0) voices in the community.
21
21
 
22
- The underlying theme is this api is meant to make it easy for the developer to do the right thing, by encouraging lazy loading and smaller footprints. It rolls up most all the other observer api's into one.
22
+ The underlying theme is this api is meant to make it easy for the developer to do the right thing, by encouraging lazy loading and smaller footprints. It rolls up most all the other observer api's into one, including, potentially, [this one](https://github.com/whatwg/dom/issues/1285).
23
+
23
24
 
24
25
  ### Does this api make the impossible possible?
25
26
 
@@ -27,7 +28,9 @@ There is quite a bit of functionality this proposal would open up, that is excee
27
28
 
28
29
  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.
29
30
 
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.
31
+ 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.
32
+
33
+
31
34
 
32
35
  3. Knowing when an element, previously being monitored for, passes totally "out-of-scope", so that no more hard references to the element remain. This would allow for cleanup of no longer needed weak references without requiring polling.
33
36
 
@@ -44,6 +47,9 @@ The amount of code necessary to accomplish these common tasks designed to improv
44
47
 
45
48
  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.
46
49
 
50
+ > [!Note]
51
+ > Reading through the historical links tied to the selector-observer proposal this proposal helped spawn, I may have painted an overly optimistic picture of [what the platform is capable of](https://github.com/whatwg/dom/issues/398). It does leave me a little puzzled why this isn't an issue when it comes to styling, and also if some of the advances that were utilized to support :has could be applied to this problem space, so that maybe the arguments raised there have weakened. Even if the concerns raised are as relevant today, I think considering the use cases this proposal envisions, that the objections could be overcome, for the following reasons: 1. For scenarios where lazy loading is the primary objective, "bunching" multiple DOM mutations together and only reevaluating when things are quite idle is perfectly reasonable. Also, for binding from a distance, most of the mutations that need responding to quickly will be when the *state of the host* changes, so DOM mutations play a somewhat muted role in that regard. Again, bunching multiple DOM mutations together, even if adds a bit of a delay, also seems reasonable. I also think the platform could add an "analysis" step to look at the query and categorize it as "simple" queries vs complex. Selector queries that are driven by the characteristics of the element itself (localName, attributes, etc) could be handled in a more expedited fashion. Those that the platform does expect to require more babysitting could be monitored for less vigilantly. Maybe in the latter case, a console.warning could be emitted during initialization. The other use case, for lazy loading custom elements and custom enhancements based on attributes, I think most of the time this would fit the "simple" scenario, so again there wouldn't be much of an issue.
52
+
47
53
  ## First use case -- lazy loading custom elements
48
54
 
49
55
  To specify the equivalent of what the alternative proposal linked to above would do, we can do the following:
@@ -53,37 +59,59 @@ const observer = new MountObserver({
53
59
  on:'my-element',
54
60
  import: './my-element.js',
55
61
  do: {
56
- mount: ({localName}, {module}) => {
62
+ mount: ({localName}, {modules, observer}) => {
57
63
  if(!customElements.get(localName)) {
58
- customElements.define(localName, module.MyElement);
64
+ customElements.define(localName, modules[0].MyElement);
59
65
  }
66
+ observer.disconnect();
60
67
  }
61
68
  }
62
69
  });
63
70
  observer.observe(document);
64
71
  ```
65
72
 
66
- If no import is specified, it would go straight to do.* (if any such callbacks are specified), and it will also dispatch events as discussed below.
73
+ Invoking "disconnect" as shown above causes the observer to emit event "disconnectedCallback".
74
+
75
+ 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.
67
76
 
68
77
  This only searches for elements matching 'my-element' outside any shadow DOM.
69
78
 
70
79
  But the observe method can accept a node within the document, or a shadowRoot, or a node inside a shadowRoot as well.
71
80
 
72
- The import can also be a function:
81
+ The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
82
+
83
+ ## The import key
84
+
85
+ This proposal has been amended to support multiple imports, including of different types:
73
86
 
74
87
  ```JavaScript
75
88
  const observer = new MountObserver({
76
- on: 'my-element',
77
- import: async (matchingElement, {module}) => await import('./my-element.js');
89
+ on:'my-element',
90
+ import: [
91
+ ['./my-element-small.css', {type: 'css'}],
92
+ './my-element.js',
93
+ ]
94
+ do: {
95
+ mount: ({localName}, {modules, observer}) => {
96
+ if(!customElements.get(localName)) {
97
+ customElements.define(localName, modules[1].MyElement);
98
+ }
99
+ observer.disconnect();
100
+ }
101
+ }
78
102
  });
79
- observer.observe(myRootNode);
103
+ observer.observe(document);
80
104
  ```
81
105
 
82
- which would work better with current bundlers, I suspect. Also, we can do interesting things like merge multiple imports into one "module". But should this API be built into the platform, such functions wouldn't be necessary, as bundlers could start to recognize strings that are passed to the MountObserver's constructor.
106
+ Th key can accept either a single import or multiple (via an array).
107
+
108
+ The do event won't be invoked until all the imports have been successfully completed and inserted into the modules array.
109
+
110
+ Previously, this proposal called for allowing arrow functions as well, thinking that could be a good interim way to support bundlers. But the valuable input provided by [doeixd](https://github.com/doeixd) makes me think that that interim support could just as effectively be done by the developer in the do methods.
111
+
112
+ This proposal would also include support for JSON and HTML module imports.
83
113
 
84
- This proposal would also include support for CSS, JSON, HTML module imports.
85
114
 
86
- The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
87
115
 
88
116
  ## Binding from a distance
89
117
 
@@ -140,7 +168,7 @@ const observer = new MountObserver({
140
168
  },
141
169
  import: ['./my-element-small.css', {type: 'css'}],
142
170
  do: {
143
- mount: ({localName}, {module}) => {
171
+ mount: ({localName}, {modules}) => {
144
172
  ...
145
173
  },
146
174
  dismount: ...,
@@ -153,7 +181,13 @@ const observer = new MountObserver({
153
181
  })
154
182
  ```
155
183
 
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.
184
+ 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.
185
+
186
+ <!--
187
+
188
+ [TODO] Maybe should also (optionally?) pass back which checks failed and which succeeded on dismount. Not sure I really see a use case for it, but leaving the thought here for now
189
+
190
+ -->
157
191
 
158
192
  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:
159
193
 
@@ -211,6 +245,7 @@ Being that for both custom elements, as well as (hopefully) [custom enhancements
211
245
 
212
246
  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.
213
247
 
248
+ <!--
214
249
  ### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
215
250
 
216
251
  Example:
@@ -236,13 +271,16 @@ Example:
236
271
  }, 1000);
237
272
  </script>
238
273
  ```
274
+ -->
275
+
239
276
 
277
+ ### Custom Enhancements in userland
240
278
 
241
- ### Scenario 2 -- Custom Enhancements in userland
279
+ [This proposal could take quite a while to see the light of day, if ever](https://github.com/WICG/webcomponents/issues/1000).
242
280
 
243
- Based on [the proposal as it currently stands](https://github.com/WICG/webcomponents/issues/1000), in this case the class prototype would *not* have the attributes defined as a static property of the class, so that the constructor arguments in the previous scenario wouldn't be sufficient. So instead, what would seem 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.
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.
244
282
 
245
- Suppose we have a progressive enhancement that we want to apply based on the presence of 1 or more attributes.
283
+ Suppose we have a (progressive) enhancement that we want to apply based on the presence of 1 or more attributes.
246
284
 
247
285
  To make this discussion concrete, let's suppose the "canonical" names of those attributes are:
248
286
 
@@ -284,7 +322,7 @@ So let's say we want to insist that on custom elements, we must have the data- p
284
322
 
285
323
  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).
286
324
 
287
- Here's what the api **doesn't** provide:
325
+ Here's what the api **doesn't** provide (as originally proposed):
288
326
 
289
327
  ## Rejected option -- The carpal syndrome syntax
290
328
 
@@ -415,6 +453,18 @@ const mo = new MountObserver({
415
453
  });
416
454
  ```
417
455
 
456
+ ## Resolving ambiguity
457
+
458
+ Because we want the multiple root values (enh-*, data-enh-*, *) to be treated as equivalent, from a developer point of view, we have a possible ambiguity -- what if more than one root is present for the same base, branch and leaf? Which value trumps the others?
459
+
460
+ Tentative rules:
461
+
462
+ 1. Roots must differ in length.
463
+ 2. If one value is null (attribute not present) and the other a string, the one with the string value trumps.
464
+ 3. If two or more equivalent attributes have string values, the one with the longer root prevails.
465
+
466
+ The thinking here is that longer roots indicate higher "specificity", so it is safer to use that one.
467
+
418
468
  ## Preemptive downloading
419
469
 
420
470
  There are two significant steps to imports, each of which imposes a cost:
@@ -434,7 +484,7 @@ const observer = new MountObserver({
434
484
  loading: 'eager',
435
485
  import: './my-element.js',
436
486
  do:{
437
- mount: (matchingElement, {module}) => customElements.define(module.MyElement)
487
+ mount: (matchingElement, {modules}) => customElements.define(modules[0].MyElement)
438
488
  }
439
489
  })
440
490
  ```
@@ -450,18 +500,19 @@ Also, this proposal is partly focused on better management of importing resource
450
500
  The mount-observer is always on the lookout for template tags with an href attribute starting with #:
451
501
 
452
502
  ```html
453
- <template href=#id-of-source-template></template>
503
+ <template src=#id-of-source-template></template>
454
504
  ```
455
505
 
456
506
  For example:
457
507
 
458
508
  ```html
459
- <div>Some prior stuff</div>
460
- <template href=#id-of-source-template>
461
- <div slot=slot1>hello</div>
462
- <div slot=slot2>goodbye<div>
509
+ <div>Your Mother Should Know</div>
510
+ <div>I Am the Walrus</div>
511
+ <template src=#id-of-source-template>
512
+ <span slot=slot1>hello</span>
513
+ <span slot=slot2>goodbye<span>
463
514
  </template>
464
- <div>Some additional stuff</div>
515
+ <div>Strawberry Fields Forever</div>
465
516
  ```
466
517
 
467
518
  When it encounters such a thing, it searches "upwardly" through the chain of ShadowRoots for a template with id=id-of-source-template (in this case), and caches them as it finds them.
@@ -470,9 +521,7 @@ Let's say the source template looks as follows:
470
521
 
471
522
  ```html
472
523
  <template id=id-of-source-template>
473
- This is an example of a snippet of HTML that appears repeatedly.
474
- <slot name=slot1></slot>
475
- <slot name=slot2></slot>
524
+ I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot>
476
525
  </template>
477
526
  ```
478
527
 
@@ -480,11 +529,10 @@ What we would end up with is:
480
529
 
481
530
 
482
531
  ```html
483
- <div>Some prior stuff</div>
484
- This is an example of a snippet of HTML that appears repeatedly.
485
- <div>hello</div>
486
- <div>goodbye</div>
487
- <div>Some additional stuff</div>
532
+ <div>Your Mother Should Know</div>
533
+ <div>I Am the Walrus</div>
534
+ <div>I don't know why you say <span>goodbye</span> I say <span>hello</span></div>
535
+ <div>Strawberry Fields Forever</div>
488
536
  ```
489
537
 
490
538
  Some significant differences with genuine slot support as used with (ShadowDOM'd) custom elements
@@ -498,7 +546,7 @@ This proposal (and polyfill) also supports the option to utilize ShadowDOM / slo
498
546
 
499
547
  ```html
500
548
  <template id=chorus>
501
- <template href=#beautiful>
549
+ <template src=#beautiful>
502
550
  <span slot=subjectIs>
503
551
  <slot name=subjectIs1></slot>
504
552
  </span>
@@ -509,7 +557,7 @@ This proposal (and polyfill) also supports the option to utilize ShadowDOM / slo
509
557
  <slot name=verb1></slot> bring
510
558
  <slot name=pronoun1></slot> down</div>
511
559
  <div>Oh no</div>
512
- <template href=#beautiful>
560
+ <template src=#beautiful>
513
561
  <span slot=subjectIs>
514
562
  <slot name=subjectIs2></slot>
515
563
  </span>
@@ -521,11 +569,11 @@ This proposal (and polyfill) also supports the option to utilize ShadowDOM / slo
521
569
  </div>
522
570
  <div>Oh no</div>
523
571
 
524
- <template href=#down></template>
572
+ <template src=#down></template>
525
573
  </template>
526
574
 
527
575
  <div class=chorus>
528
- <template href=#chorus shadowRootModeOnLoad=open></template>
576
+ <template src=#chorus shadowRootModeOnLoad=open></template>
529
577
  <span slot=verb1>can't</span>
530
578
  <span slot=verb2>can't</span>
531
579
  <span slot=pronoun1>me</span>
@@ -1,8 +1,8 @@
1
1
  import { inclTemplQry } from './MountObserver.js';
2
2
  export async function birtualizeMatch(self, el, level) {
3
- const href = el.getAttribute('href');
4
- el.removeAttribute('href');
5
- const templID = href.substring(1);
3
+ const src = el.getAttribute('src');
4
+ el.removeAttribute('src');
5
+ const templID = src.substring(1);
6
6
  const fragment = self.objNde?.deref();
7
7
  if (fragment === undefined)
8
8
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
package/types.d.ts CHANGED
@@ -33,6 +33,11 @@ export interface WhereAttr{
33
33
  hasBase: string | [delimiter, string],
34
34
  hasBranchIn?: Array<string> | [delimiter, Array<string>],
35
35
  hasRootIn?: Array<RootCnfg>,
36
+ /**
37
+ * Used by consumers to track the universal meaning of this combination
38
+ * regardless of how the actual name values may be changed.
39
+ */
40
+ metadata?: any,
36
41
  }
37
42
  type CSSMatch = string;
38
43
  type ImportString = string;
@@ -51,7 +56,7 @@ export interface IMountObserver {
51
56
  // readonly mountedRefs: WeakRef<Element>[],
52
57
  // readonly dismountedRefs: WeakRef<Element>[],
53
58
  observe(within: Node): void;
54
- unobserve(within: Node): void;
59
+ disconnect(within: Node): void;
55
60
  module?: any;
56
61
  }
57
62
 
@@ -83,7 +88,8 @@ interface AttrParts{
83
88
  branchIdx: number,
84
89
  leaf?: string, //TODO
85
90
  leafIdx?: number, //TODO
86
- rootCnfg?: RootCnfg
91
+ rootCnfg?: RootCnfg,
92
+ metadata?: any,
87
93
  }
88
94
 
89
95
  interface AttrChangeInfo{