mount-observer 0.0.21 → 0.0.23

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
@@ -5,7 +5,7 @@ export class MountObserver extends EventTarget {
5
5
  #mountInit;
6
6
  //#rootMutObs: RootMutObs | undefined;
7
7
  #abortController;
8
- #mounted;
8
+ mountedElements;
9
9
  #mountedList;
10
10
  #disconnected;
11
11
  //#unmounted: WeakSet<Element>;
@@ -25,7 +25,7 @@ export class MountObserver extends EventTarget {
25
25
  throw 'NI'; //not implemented
26
26
  this.#mountInit = init;
27
27
  this.#abortController = new AbortController();
28
- this.#mounted = new WeakSet();
28
+ this.mountedElements = new WeakSet();
29
29
  this.#disconnected = new WeakSet();
30
30
  //this.#unmounted = new WeakSet();
31
31
  }
@@ -181,6 +181,23 @@ export class MountObserver extends EventTarget {
181
181
  }, { signal: this.#abortController.signal });
182
182
  await this.#inspectWithin(within, true);
183
183
  }
184
+ static synthesize(within, customElement, mose) {
185
+ mose.type = 'mountobserver';
186
+ const name = customElements.getName(customElement);
187
+ if (name === null)
188
+ throw 400;
189
+ let instance = within.querySelector(name);
190
+ if (instance === null) {
191
+ instance = new customElement();
192
+ if (within === document) {
193
+ within.head.appendChild(instance);
194
+ }
195
+ else {
196
+ within.appendChild(instance);
197
+ }
198
+ }
199
+ instance.appendChild(mose);
200
+ }
184
201
  #confirmInstanceOf(el, whereInstanceOf) {
185
202
  for (const test of whereInstanceOf) {
186
203
  if (el instanceof test)
@@ -196,7 +213,7 @@ export class MountObserver extends EventTarget {
196
213
  for (const match of matching) {
197
214
  if (alreadyMounted.has(match))
198
215
  continue;
199
- this.#mounted.add(match);
216
+ this.mountedElements.add(match);
200
217
  if (imp !== undefined) {
201
218
  switch (typeof imp) {
202
219
  case 'string':
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-21
14
+ Last Update: 2024-5-22
15
15
 
16
16
  ## Benefits of this API
17
17
 
@@ -30,8 +30,6 @@ There is quite a bit of functionality this proposal would open up, that is excee
30
30
 
31
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
32
 
33
-
34
-
35
33
  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.
36
34
 
37
35
  ### Most significant use cases.
@@ -113,7 +111,57 @@ Previously, this proposal called for allowing arrow functions as well, thinking
113
111
 
114
112
  This proposal would also include support for JSON and HTML module imports.
115
113
 
114
+ ## MountObserver script element
115
+
116
+ Following an approach similar to the [speculation api](https://developer.chrome.com/blog/speculation-rules-improvements), we can add a script element anywhere in the DOM:
117
+
118
+ ```html
119
+ <script type="mountobserver" id=myMountObserver onmount="
120
+ const {matchingElement} = event;
121
+ const {localName} = matchingElement;
122
+ if(!customElements.get(localName)) {
123
+ customElements.define(localName, modules[1].MyElement);
124
+ }
125
+ observer.disconnect();
126
+ ">
127
+ {
128
+ "on":"my-element",
129
+ "import": [
130
+ ["./my-element-small.css", {type: "css"}],
131
+ "./my-element.js",
132
+ ]
133
+ }
134
+ </script>
135
+ ```
136
+
137
+ The objects modules, observer, mountedElements (array of weak refs) would be available as properties of the script element:
138
+
139
+ ```JavaScript
140
+ const {modules, observer, mountedElements, mountInit} = myMountObserver;
141
+ ```
142
+
143
+ The "scope" of the observer would be the ShadowRoot containing the script element (or the document outside Shadow if placed outside any shadow DOM, like in the head element).
144
+
145
+ No arrays of settings would be supported within a single tag (as this causes issues as far as supporting a single onmount, ondismount, etc event attributes).
116
146
 
147
+ ## Shadow Root inheritance
148
+
149
+ Inside a shadow root, we can plop a script element, also with type mountobserver, optionally giving it the same id as above:
150
+
151
+ ```html
152
+ #shadowRoot
153
+ <script id=myMountObserver type=mountobserver>
154
+ {
155
+ "on":"your-element"
156
+ }
157
+ </script>
158
+ ```
159
+
160
+ If no id is found in the parent ShadowRoot (or in the parent window if the shadow root is at the top level), then this becomes a new set of rules to observe.
161
+
162
+ But if a matching id is found, then the values from the parent script element get merged in with the one in the child, with the child settings, including the event handling attributes.
163
+
164
+ We will come back to some [additional features](#mountobserver-script-element-minutiae) of using these script elements later, but wanted to cover the highlights of this proposal before getting bogged down in some tedious logistics.
117
165
 
118
166
  ## Binding from a distance
119
167
 
@@ -275,38 +323,48 @@ The alternative to providing this feature, which I'm leaning towards, is to just
275
323
 
276
324
  ## A tribute to attributes
277
325
 
326
+ Attributes of DOM elements are tricky. They've been around since the get-go, and they've survived multiple generations of the Web where different philosophies have prevailed, so prepare yourself for some subtle discussion in what follows.
327
+
278
328
  Extra support is provided for monitoring attributes. There are two primary reasons for needing to provide special support for attributes with this API:
279
329
 
280
330
  Being that for both custom elements, as well as (hopefully) [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) we need to carefully work with sets of "owned" [observed](https://github.com/WICG/webcomponents/issues/1045) attributes, and in some cases we may need to manage combinations of prefixes and suffixes for better name-spacing management, creating the most effective css query becomes challenging.
281
331
 
282
332
  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
333
 
284
- ## A key attribute of attributes
285
-
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:
334
+ ## Attributes of attributes
287
335
 
288
- 1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of the truth".
336
+ I think it is useful to divide [attributes](https://jakearchibald.com/2024/attributes-vs-properties/) that we would want to observe into two categories:
289
337
 
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).
338
+ 1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of the truth" for key features of the DOM element itself. We will refer to these attributes as "Source of Truth" attributes.
291
339
 
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.
340
+ Examples are many built-in global attributes, like lang, or contenteditable, or more specialized examples such as "content" for the meta tag. I think in the vast majority of cases, setting the property values corresponding to these attributes results in directly reflecting those property values to the attributes. There are exceptions, especially for non-string attributes like the checked property of the input element / type=checkbox. 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. And some attributes (like the microdata attributes such as itemprop) don't even have properties that they pair with!
341
+
293
342
 
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.
343
+ 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. We'll refer to these attributes as "Enhancement Attributes."
295
344
 
296
345
  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
346
 
347
+ > [!NOTE]
348
+ > The most important reason for pointing out this distinction is this: "Source of Truth" attributes will only be *observed*, and will **not** trigger mount/unmount states unless they are part of the "on" selector string. And unlike all the other "where" conditions this proposal supports, the where clauses for the "Enhancement Attributes" are "one-way" -- they trigger a "mount" event / callback, followed by the ability to observe the stream of changes (including removal of those attributes), but they never trigger a "dismount".
349
+
350
+ ### Counterpoint
351
+
352
+ Does it make sense to even support "Source of Truth" attributes in a "MountObserver" api, if they have no impact on mounted state?
353
+
354
+ We think it does, because some Enhancement Attributes will need to work in conjunction with Source of Truth attributes, in order to provide the observer a coherent picture of the full state of the element.
298
355
 
299
- ### Attributes that are the "source of truth"
356
+ This realization (hopefully correct) struck me while trying to implement a [userland implementation](https://github.com/bahrus/be-intl) of [this proposal](https://github.com/whatwg/html/issues/9294).
300
357
 
301
- So for the first scenario, we can specify attributes to listen for as follows:
358
+
359
+ ### Source of Truth Attributes
360
+
361
+ Let's focus on the first scenario. It doesn't make sense to use the word "where" for these, because we don't want these attributes to affect our mount/dismount state
302
362
 
303
363
  ```JavaScript
304
364
  import {MountObserver} from 'mount-observer/MountObserver.js';
305
365
  const mo = new MountObserver({
306
366
  on: '*',
307
- whereAttr:{
308
- isIn: ['lang', 'contenteditable']
309
- }
367
+ observedAttrsWhenMounted: ['lang', 'contenteditable']
310
368
  });
311
369
 
312
370
  mo.addEventListener('attrChange', e => {
@@ -657,3 +715,57 @@ This proposal (and polyfill) also supports the option to utilize ShadowDOM / slo
657
715
 
658
716
  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
717
 
718
+ ## MountObserver script element minutiae
719
+
720
+ Often, we will want to define a large number of "mount observers" programmatically, and we need it to be done in a generic way. This is a problem space that [be-hive](https://github.com/bahrus/be-hive) is grappling with. In particular, we want to publish enhancements that take advantage of this inheritable infrastructure of declarative configuration, but we don't want to burden the developer with having to manually list all these configurations, we want it to happen automatically.
721
+
722
+ To support this, we propose these highlights:
723
+
724
+ 1. Adding a "synthesize" method to the MountObserver api, only if observing a shadowRoot (or the top level document). This would provide a kind of passage way from the imperative api to the declarative one.
725
+ 2. Synthesize method appends a script element of type MountObserver, that dispatches event from the synthesizing custom element it gets appended to, so subscribers don't need to add a general mutation observer in order to know when parent shadow roots had a MountObserver script tag inserted.
726
+
727
+
728
+ So developers can develop a custom element, used to group families of MountObservers together.
729
+
730
+ If one inspects the DOM, one would see grouped (already "parsed") MountObservers, like so:
731
+
732
+ ```html
733
+ <be-hive>
734
+ <script type=mountobserver id=be-searching></script>
735
+ <script type=mountobserver id=be-counted></script>
736
+ </be-hive>
737
+ ```
738
+
739
+ But the developer would not need to set these up automatically.
740
+
741
+ Instead, the framework developer would define a custom element that inherits from base class that this proposal/polyfill provides.
742
+
743
+ Let's say the framework developer creates an extending Web Component with constructor: BeHive.
744
+
745
+ Then rather than invoking:
746
+
747
+ ```JavaScript
748
+ mountObserver.observe(rootNode);
749
+ ```
750
+
751
+ we would invoke:
752
+
753
+ ```JavaScript
754
+ mountObserver.synthesize(rootNode, BeHive, mountObserverScriptElement)
755
+ ```
756
+
757
+ The MountObserver api would:
758
+
759
+ 1. Use [customElements.getName](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/getName) to get the name of the custom element (say it is 'be-hive').
760
+ 2. Search for a be-hive tag inside the root node (with special logic for the "head" element). If not found, create it.
761
+ 3. Place the script element inside.
762
+
763
+
764
+ Then in our shadowroot, rather than adding a script type=mountobserver for every single mount observer we want to inherit, we could reference the group via simply:
765
+
766
+ ```html
767
+ <be-hive></be-hive>
768
+ ```
769
+
770
+
771
+
package/Synthesizer.js ADDED
@@ -0,0 +1,91 @@
1
+ import { MountObserver } from './MountObserver.js';
2
+ export class Synthesizer extends HTMLElement {
3
+ #mutationObserver;
4
+ mountObserverElements = [];
5
+ mutationCallback(mutationList) {
6
+ for (const mutation of mutationList) {
7
+ const { addedNodes } = mutation;
8
+ for (const node of addedNodes) {
9
+ if (!(node instanceof HTMLScriptElement) || node.type !== 'mountobserver')
10
+ continue;
11
+ const mose = node;
12
+ this.mountObserverElements.push(mose);
13
+ this.activate(mose);
14
+ const e = new SynthetizeEvent(mose);
15
+ this.dispatchEvent(e);
16
+ }
17
+ }
18
+ }
19
+ connectedCallback() {
20
+ this.hidden = true;
21
+ const init = {
22
+ childList: true
23
+ };
24
+ this.querySelectorAll('script[type="mountobserver"]').forEach(s => {
25
+ const mose = s;
26
+ this.mountObserverElements.push(mose);
27
+ this.activate(mose);
28
+ });
29
+ this.#mutationObserver = new MutationObserver(this.mutationCallback.bind(this));
30
+ this.#mutationObserver.observe(this, init);
31
+ this.inherit();
32
+ }
33
+ activate(mose) {
34
+ const { init, do: d, id } = mose;
35
+ const mi = {
36
+ do: d,
37
+ ...init
38
+ };
39
+ const mo = new MountObserver(mi);
40
+ mose.observer = mo;
41
+ mo.observe(this.getRootNode());
42
+ }
43
+ import(mose) {
44
+ const { init, do: d, id, synConfig } = mose;
45
+ const se = document.createElement('script');
46
+ se.init = { ...init };
47
+ se.id = id;
48
+ se.do = { ...d };
49
+ se.synConfig = { ...synConfig };
50
+ this.appendChild(se);
51
+ }
52
+ inherit() {
53
+ const rn = this.getRootNode();
54
+ const host = rn.host;
55
+ if (!host)
56
+ return;
57
+ const parentShadowRealm = host.getRootNode();
58
+ const { localName } = this;
59
+ const parentScopeSynthesizer = parentShadowRealm.querySelector(localName);
60
+ const { mountObserverElements } = parentScopeSynthesizer;
61
+ for (const moe of mountObserverElements) {
62
+ this.import(moe);
63
+ }
64
+ if (parentScopeSynthesizer !== null) {
65
+ parentScopeSynthesizer.addEventListener(SynthetizeEvent.eventName, e => {
66
+ this.import(e.mountObserverElement);
67
+ });
68
+ }
69
+ }
70
+ disconnectedCallback() {
71
+ if (this.#mutationObserver !== undefined) {
72
+ this.#mutationObserver.disconnect();
73
+ }
74
+ for (const mose of this.mountObserverElements) {
75
+ mose.observer.disconnect(this.getRootNode());
76
+ }
77
+ }
78
+ }
79
+ // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
80
+ /**
81
+ * The `mutation-event` event represents something that happened.
82
+ * We can document it here.
83
+ */
84
+ export class SynthetizeEvent extends Event {
85
+ mountObserverElement;
86
+ static eventName = 'synthesize';
87
+ constructor(mountObserverElement) {
88
+ super(SynthetizeEvent.eventName);
89
+ this.mountObserverElement = mountObserverElement;
90
+ }
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "exports": {
12
12
  ".": "./MountObserver.js",
13
- "./MountObserver.js": "./MountObserver.js"
13
+ "./MountObserver.js": "./MountObserver.js",
14
+ "./Synthesizer.js": "./Synthesizer.js"
14
15
  },
15
16
  "files": [
16
17
  "*.js",
package/types.d.ts CHANGED
@@ -1,20 +1,19 @@
1
- export interface MountInit{
1
+ //import { MountObserver } from "./MountObserver";
2
+
3
+ export interface JSONSerializableMountInit{
2
4
  readonly on?: CSSMatch,
3
- //readonly attribMatches?: Array<AttribMatch>,
4
- readonly withTargetShadowRoot?: ShadowRoot,
5
5
  readonly whereAttr?: WhereAttr,
6
6
  readonly whereElementIntersectsWith?: IntersectionObserverInit,
7
7
  readonly whereMediaMatches?: MediaQuery,
8
+ readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
9
+
10
+ }
11
+ export interface MountInit extends JSONSerializableMountInit{
12
+
13
+ readonly withTargetShadowRoot?: ShadowRoot,
8
14
  readonly whereInstanceOf?: Array<{new(): Element}>,
9
15
  readonly whereSatisfies?: PipelineProcessor<boolean>,
10
- readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
11
- readonly do?: {
12
- readonly mount?: PipelineProcessor,
13
- readonly dismount?: PipelineProcessor,
14
- readonly disconnect?: PipelineProcessor,
15
- readonly reconfirm?: PipelineProcessor,
16
- readonly exit?: PipelineProcessor,
17
- }
16
+ readonly do?: MountObserverCallbacks
18
17
  // /**
19
18
  // * Purpose -- there are scenarios where we may only want to affect changes that occur after the initial
20
19
  // * server rendering, so we only want to mount elements that appear
@@ -22,6 +21,14 @@ export interface MountInit{
22
21
  // readonly ignoreInitialMatches?: boolean,
23
22
  }
24
23
 
24
+ export interface MountObserverCallbacks{
25
+ readonly mount?: PipelineProcessor,
26
+ readonly dismount?: PipelineProcessor,
27
+ readonly disconnect?: PipelineProcessor,
28
+ readonly reconfirm?: PipelineProcessor,
29
+ readonly exit?: PipelineProcessor,
30
+ }
31
+
25
32
  export interface RootCnfg{
26
33
  start: string,
27
34
  context: 'BuiltIn' | 'CustomElement' | 'Both'
@@ -59,6 +66,7 @@ export interface IMountObserver {
59
66
  observe(within: Node): void;
60
67
  disconnect(within: Node): void;
61
68
  module?: any;
69
+ mountedElements: WeakSet<Element>;
62
70
  }
63
71
 
64
72
  export interface MountContext{
@@ -161,3 +169,18 @@ export interface AddLoadEventListener{
161
169
  }
162
170
  //#endregion
163
171
 
172
+ //#region MountObserver Script Element
173
+ export interface MountObserverScriptElementEndUserProps<TSynConfig=any>{
174
+ init: JSONSerializableMountInit;
175
+ observer: IMountObserver;
176
+ do: MountObserverCallbacks;
177
+ synConfig: TSynConfig;
178
+ }
179
+ export interface MountObserverScriptElement<TSynConfig=any>
180
+ extends HTMLScriptElement, MountObserverScriptElementEndUserProps<TSynConfig>{
181
+
182
+ }
183
+ //#endregion
184
+
185
+
186
+