mount-observer 0.0.6 → 0.0.8

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
@@ -12,10 +12,10 @@ export class MountObserver extends EventTarget {
12
12
  #isComplex;
13
13
  constructor(init) {
14
14
  super();
15
- const { match, whereElementIntersectsWith, whereMediaMatches } = init;
15
+ const { on, whereElementIntersectsWith, whereMediaMatches } = init;
16
16
  let isComplex = false;
17
- if (match !== undefined) {
18
- const reducedMatch = match.replaceAll(':not(', '');
17
+ if (on !== undefined) {
18
+ const reducedMatch = on.replaceAll(':not(', '');
19
19
  isComplex = reducedMatch.includes(' ') || reducedMatch.includes(':');
20
20
  }
21
21
  this.#isComplex = isComplex;
@@ -28,21 +28,20 @@ export class MountObserver extends EventTarget {
28
28
  //this.#unmounted = new WeakSet();
29
29
  }
30
30
  #calculatedSelector;
31
- get #selector() {
31
+ #fullListOfAttrs;
32
+ //get #attrVals
33
+ async #selector() {
32
34
  if (this.#calculatedSelector !== undefined)
33
35
  return this.#calculatedSelector;
34
- const { match, attribMatches } = this.#mountInit;
35
- const base = match || '*';
36
- if (attribMatches === undefined)
37
- return base;
38
- const matches = [];
39
- attribMatches.forEach(x => {
40
- const { names } = x;
41
- names.forEach(y => {
42
- matches.push(`${base}[${y}]`);
43
- });
44
- });
45
- this.#calculatedSelector = matches.join(',');
36
+ const { on, whereAttr } = this.#mountInit;
37
+ const withoutAttrs = on || '*';
38
+ if (whereAttr === undefined)
39
+ return withoutAttrs;
40
+ const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
41
+ const info = getWhereAttrSelector(whereAttr, withoutAttrs);
42
+ const { fullListOfAttrs, calculatedSelector } = info;
43
+ this.#fullListOfAttrs = fullListOfAttrs;
44
+ this.#calculatedSelector = calculatedSelector;
46
45
  return this.#calculatedSelector;
47
46
  }
48
47
  unobserve(within) {
@@ -86,8 +85,9 @@ export class MountObserver extends EventTarget {
86
85
  }
87
86
  }
88
87
  const rootMutObs = mutationObserverLookup.get(within);
89
- const { attribMatches } = this.#mountInit;
90
- rootMutObs.addEventListener('mutation-event', (e) => {
88
+ //const {whereAttr} = this.#mountInit;
89
+ const fullListOfAttrs = this.#fullListOfAttrs;
90
+ rootMutObs.addEventListener('mutation-event', async (e) => {
91
91
  //TODO: disconnected
92
92
  if (this.#isComplex) {
93
93
  this.#inspectWithin(within, false);
@@ -96,7 +96,7 @@ export class MountObserver extends EventTarget {
96
96
  const { mutationRecords } = e;
97
97
  const elsToInspect = [];
98
98
  //const elsToDisconnect: Array<Element> = [];
99
- const doDisconnect = this.#mountInit.do?.onDisconnect;
99
+ const doDisconnect = this.#mountInit.do?.disconnect;
100
100
  for (const mutationRecord of mutationRecords) {
101
101
  const { addedNodes, type, removedNodes } = mutationRecord;
102
102
  //console.log(mutationRecord);
@@ -104,22 +104,11 @@ export class MountObserver extends EventTarget {
104
104
  addedElements.forEach(x => elsToInspect.push(x));
105
105
  if (type === 'attributes') {
106
106
  const { target, attributeName, oldValue } = mutationRecord;
107
- if (target instanceof Element && attributeName !== null && attribMatches !== undefined && this.#mounted.has(target)) {
108
- let idx = 0;
109
- for (const attrMatch of attribMatches) {
110
- const { names } = attrMatch;
111
- if (names.includes(attributeName)) {
107
+ if (target instanceof Element && attributeName !== null && this.#mounted.has(target)) {
108
+ if (fullListOfAttrs !== undefined) {
109
+ const idx = fullListOfAttrs.indexOf(attributeName);
110
+ if (idx > -1) {
112
111
  const newValue = target.getAttribute(attributeName);
113
- // let parsedNewValue = undefined;
114
- // switch(type){
115
- // case 'boolean':
116
- // parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
117
- // break;
118
- // case 'date':
119
- // parsedNewValue = newValue === null ? null : new Date(newValue);
120
- // break;
121
- // case ''
122
- // }
123
112
  const attrChangeInfo = {
124
113
  name: attributeName,
125
114
  oldValue,
@@ -128,7 +117,13 @@ export class MountObserver extends EventTarget {
128
117
  };
129
118
  this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
130
119
  }
131
- idx++;
120
+ }
121
+ else {
122
+ const { whereAttr } = this.#mountInit;
123
+ if (whereAttr !== undefined) {
124
+ const { doWhereAttr } = await import('./doWhereAttr.js');
125
+ doWhereAttr(whereAttr, attributeName, target, oldValue, this);
126
+ }
132
127
  }
133
128
  }
134
129
  elsToInspect.push(target);
@@ -140,7 +135,7 @@ export class MountObserver extends EventTarget {
140
135
  // this.#mountedList = this.#mountedList?.filter(x => x.deref() !== deletedElement);
141
136
  this.#disconnected.add(deletedElement);
142
137
  if (doDisconnect !== undefined) {
143
- doDisconnect(deletedElement, this);
138
+ doDisconnect(deletedElement, this, {});
144
139
  }
145
140
  this.dispatchEvent(new DisconnectEvent(deletedElement));
146
141
  }
@@ -160,9 +155,10 @@ export class MountObserver extends EventTarget {
160
155
  }
161
156
  async #mount(matching, initializing) {
162
157
  //first unmount non matching
163
- const alreadyMounted = this.#filterAndDismount();
164
- const onMount = this.#mountInit.do?.onMount;
165
- const { import: imp, attribMatches } = this.#mountInit;
158
+ const alreadyMounted = await this.#filterAndDismount();
159
+ const mount = this.#mountInit.do?.mount;
160
+ const { import: imp } = this.#mountInit;
161
+ const fullListOfAttrs = this.#fullListOfAttrs;
166
162
  for (const match of matching) {
167
163
  if (alreadyMounted.has(match))
168
164
  continue;
@@ -186,41 +182,47 @@ export class MountObserver extends EventTarget {
186
182
  break;
187
183
  }
188
184
  }
189
- if (onMount !== undefined) {
190
- onMount(match, this, {
185
+ if (mount !== undefined) {
186
+ mount(match, this, {
191
187
  stage: 'PostImport',
192
188
  initializing
193
189
  });
194
190
  }
195
191
  this.dispatchEvent(new MountEvent(match, initializing));
196
- if (attribMatches !== undefined) {
197
- let idx = 0;
198
- for (const attribMatch of attribMatches) {
199
- let newValue = null;
200
- const { names } = attribMatch;
201
- let nonNullName = names[0];
202
- for (const name of names) {
203
- const attrVal = match.getAttribute(name);
204
- if (attrVal !== null)
205
- nonNullName = name;
206
- newValue = newValue || attrVal;
192
+ if (fullListOfAttrs !== undefined) {
193
+ const { whereAttr } = this.#mountInit;
194
+ for (const name of fullListOfAttrs) {
195
+ if (whereAttr !== undefined) {
196
+ const { doWhereAttr } = await import('./doWhereAttr.js');
197
+ doWhereAttr(whereAttr, name, match, null, this);
207
198
  }
208
- const attribInfo = {
209
- oldValue: null,
210
- newValue,
211
- idx,
212
- name: nonNullName
213
- };
214
- this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
215
- idx++;
216
199
  }
200
+ // let idx = 0;
201
+ // for(const attribMatch of attribMatches){
202
+ // let newValue = null;
203
+ // const {names} = attribMatch;
204
+ // let nonNullName = names[0];
205
+ // for(const name of names){
206
+ // const attrVal = match.getAttribute(name);
207
+ // if(attrVal !== null) nonNullName = name;
208
+ // newValue = newValue || attrVal;
209
+ // }
210
+ // const attribInfo: AttrChangeInfo = {
211
+ // oldValue: null,
212
+ // newValue,
213
+ // idx,
214
+ // name: nonNullName
215
+ // };
216
+ // this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
217
+ // idx++;
218
+ // }
217
219
  }
218
220
  this.#mountedList?.push(new WeakRef(match));
219
221
  //if(this.#unmounted.has(match)) this.#unmounted.delete(match);
220
222
  }
221
223
  }
222
224
  async #dismount(unmatching) {
223
- const onDismount = this.#mountInit.do?.onDismount;
225
+ const onDismount = this.#mountInit.do?.dismount;
224
226
  for (const unmatch of unmatching) {
225
227
  if (onDismount !== undefined) {
226
228
  onDismount(unmatch, this, {});
@@ -228,12 +230,12 @@ export class MountObserver extends EventTarget {
228
230
  this.dispatchEvent(new DismountEvent(unmatch));
229
231
  }
230
232
  }
231
- #filterAndDismount() {
233
+ async #filterAndDismount() {
232
234
  const returnSet = new Set();
233
235
  if (this.#mountedList !== undefined) {
234
236
  const previouslyMounted = this.#mountedList.map(x => x.deref());
235
237
  const { whereSatisfies, whereInstanceOf } = this.#mountInit;
236
- const match = this.#selector;
238
+ const match = await this.#selector();
237
239
  const elsToUnMount = previouslyMounted.filter(x => {
238
240
  if (x === undefined)
239
241
  return false;
@@ -253,7 +255,7 @@ export class MountObserver extends EventTarget {
253
255
  }
254
256
  async #filterAndMount(els, checkMatch, initializing) {
255
257
  const { whereSatisfies, whereInstanceOf } = this.#mountInit;
256
- const match = this.#selector;
258
+ const match = await this.#selector();
257
259
  const elsToMount = els.filter(x => {
258
260
  if (checkMatch) {
259
261
  if (!x.matches(match))
@@ -272,7 +274,7 @@ export class MountObserver extends EventTarget {
272
274
  this.#mount(elsToMount, initializing);
273
275
  }
274
276
  async #inspectWithin(within, initializing) {
275
- const els = Array.from(within.querySelectorAll(this.#selector));
277
+ const els = Array.from(within.querySelectorAll(await this.#selector()));
276
278
  this.#filterAndMount(els, false, initializing);
277
279
  }
278
280
  }
@@ -318,3 +320,4 @@ export class AttrChangeEvent extends Event {
318
320
  this.attrChangeInfo = attrChangeInfo;
319
321
  }
320
322
  }
323
+ //const hasRootInDefault = ['data', 'enh', 'data-enh']
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/mount-observer?style=for-the-badge)](https://bundlephobia.com/result?p=mount-observer)
4
4
  <img src="http://img.badgesize.io/https://cdn.jsdelivr.net/npm/mount-observer?compression=gzip">
5
5
 
6
+ Note that much of what is described below has not yet been polyfilled.
6
7
 
7
8
  # The MountObserver api.
8
9
 
@@ -10,13 +11,13 @@ Author: Bruce B. Anderson
10
11
 
11
12
  Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
12
13
 
13
- Last Update: 2023-11-23
14
+ Last Update: 2024-2-14
14
15
 
15
16
  ## Benefits of this API
16
17
 
17
- What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver api are more encompassing, and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one api. It is basically mapping common filtering conditions in the DOM, to common actions, like importing a resource, or progressively enhancing an element, or "binding from a distance".
18
+ What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver api are more encompassing, and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one api. It is basically mapping common filtering conditions in the DOM, to mounting a "campaign" of some sort, like importing a resource, and/or progressively enhancing an element, and/or "binding from a distance".
18
19
 
19
- "Binding from a distance" 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.
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 voices in the community.
20
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
23
 
@@ -24,7 +25,7 @@ The underlying theme is this api is meant to make it easy for the developer to d
24
25
 
25
26
  There is quite a bit of functionality this proposal would open up, that is exceedingly difficult to polyfill reliably:
26
27
 
27
- 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.
28
+ 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.
28
29
 
29
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.
30
31
 
@@ -36,11 +37,10 @@ The amount of code necessary to accomplish these common tasks designed to improv
36
37
 
37
38
  1. Give the developer a strong signal to do the right thing, by
38
39
  1. Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
39
- 2. Supporting "binding from a distance" that can allow SSR to provide common, shared data using the "DRY" philosophy, similar to how CSS can reduce the amount of repetitive styling instructions found inline within the HTML Markup.
40
- 3. Supporting "progressive enhancement."
41
- 2. Allow numerous components / libraries to leverage this common functionality, which could potentially significantly reduce bandwidth.
42
- 3. Potentially by allowing the platform to do more work in the low-level (c/c++/rust?) code, without as much context switching into the JavaScript memory space, which may reduce cpu cycles as well.
43
- 4. To do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
40
+ 2. Supporting "binding from a distance" that can set property values of elements in bulk as the HTML streams in. For example, say a web page is streaming in HTML with thousands of input elements (say a long tax form). We want to have some indication in the head tag of the HTML (for example) to make all the input elements read only as they stream through the page. With css, we could do similar things, for example set the background to red of all input elements. Why can't we do something similar with setting properties like readOnly, disabled, etc? With this api, giving developers the "keys" to css filtering, so they can "mount a campaign" to apply common settings on them all feels like something that almost every web developer has mentally screamed to themselves "why can't I do that?", doesn't it?
41
+ 3. Supporting "progressive enhancement" more effectively.
42
+ 2. Potentially by allowing the platform to do more work in the low-level (c/c++/rust?) code, without as much context switching into the JavaScript memory space, which may reduce cpu cycles as well. This is done by passing into the API substantial number of conditions, which can all be evaluated at a lower level, before the api needs to surface up to the developer "found one!".
43
+ 3. As discussed earlier, to do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
44
44
 
45
45
  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
46
 
@@ -50,12 +50,12 @@ To specify the equivalent of what the alternative proposal linked to above would
50
50
 
51
51
  ```JavaScript
52
52
  const observer = new MountObserver({
53
- match:'my-element',
53
+ on:'my-element',
54
54
  import: './my-element.js',
55
55
  do: {
56
- onMount: ({localName}, {module}) => {
56
+ mount: ({localName}, {module}) => {
57
57
  if(!customElements.get(localName)) {
58
- customElements.define(localName, module.default);
58
+ customElements.define(localName, module.MyElement);
59
59
  }
60
60
  }
61
61
  }
@@ -67,36 +67,43 @@ If no import is specified, it would go straight to do.* (if any such callbacks a
67
67
 
68
68
  This only searches for elements matching 'my-element' outside any shadow DOM.
69
69
 
70
- But the observe method can accept a shadowRoot, or a node inside a shadowRoot as well.
70
+ But the observe method can accept a node within the document, or a shadowRoot, or a node inside a shadowRoot as well.
71
71
 
72
72
  The import can also be a function:
73
73
 
74
74
  ```JavaScript
75
75
  const observer = new MountObserver({
76
- match: 'my-element',
76
+ on: 'my-element',
77
77
  import: async (matchingElement, {module}) => await import('./my-element.js');
78
78
  });
79
79
  observer.observe(myRootNode);
80
80
  ```
81
81
 
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 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.
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.
83
83
 
84
- This proposal would also include support for CSS, JSON, HTML module imports.
85
-
86
- "match" is a css query, and could include multiple matches using the comma separator, i.e. no limitation on CSS expressions.
84
+ This proposal would also include support for CSS, JSON, HTML module imports.
87
85
 
88
86
  The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
89
87
 
90
- <!-- As matches are found (for example, right away if matching elements are immediately found), the imports object would maintain a read-only array of weak references, along with the imported module:
88
+ ## Binding from a distance
89
+
90
+ It is important to note that "on" is a css query with no restrictions. So something like:
91
91
 
92
- ```TypeScript
93
- interface MountContext {
94
- weakReferences: readonly WeakRef<Element>[];
95
- module: any;
96
- }
92
+ ```JavaScript
93
+ const observer = new MountObserver({
94
+ on:'div > p + p ~ span[class$="name"]',
95
+ do:{
96
+ mount: (matchingElement) => {
97
+ //attach some behavior or set some property value or add an event listener, etc.
98
+ matchingElement.textContent = 'hello';
99
+ }
100
+ }
101
+ })
97
102
  ```
98
103
 
99
- This allows code that comes into being after the matching elements were found, to "get caught up" on all the matches. -->
104
+ ... would work.
105
+
106
+ This would allow developers to create "stylesheet" like capabilities.
100
107
 
101
108
 
102
109
  ## Extra lazy loading
@@ -107,7 +114,7 @@ However, we could make the loading even more lazy by specifying intersection opt
107
114
 
108
115
  ```JavaScript
109
116
  const observer = new MountObserver({
110
- match: 'my-element',
117
+ on: 'my-element',
111
118
  whereElementIntersectsWith:{
112
119
  rootMargin: "0px",
113
120
  threshold: 1.0,
@@ -122,27 +129,31 @@ Unlike traditional CSS @import, CSS Modules don't support specifying different i
122
129
 
123
130
  ```JavaScript
124
131
  const observer = new MountObserver({
125
- match: 'my-element',
132
+ on: 'div > p + p ~ span[class$="name"]',
126
133
  whereMediaMatches: '(max-width: 1250px)',
127
134
  whereSizeOfContainerMatches: '(min-width: 700px)',
128
135
  whereInstanceOf: [HTMLMarqueeElement],
129
136
  whereSatisfies: async (matchingElement, context) => true,
137
+ whereLangIn: ['en-GB'],
138
+ whereConnection:{
139
+ effectiveTypeIn: ["slow-2g"],
140
+ },
130
141
  import: ['./my-element-small.css', {type: 'css'}],
131
142
  do: {
132
- onMount: ({localName}, {module}) => {
143
+ mount: ({localName}, {module}) => {
133
144
  ...
134
145
  },
135
- onDismount: ...,
136
- onDisconnect: ...,
137
- onOutsideRootNode: ...,
138
- onReconnect: ...,
139
- onReconfirm: ...,
140
- onOutOfScope: ...,
146
+ dismount: ...,
147
+ disconnect: ...,
148
+ reconnect: ...,
149
+ reconfirm: ...,
150
+ exit: ...,
151
+ forget: ...,
141
152
  }
142
153
  })
143
154
  ```
144
155
 
145
- Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance. However, since these rules may be of interest to multiple parties, it is usesful to also provide the ability for multiple parties to subscribe to these css rules. This can be done via:
156
+ Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance. 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:
146
157
 
147
158
  ## Subscribing
148
159
 
@@ -155,21 +166,22 @@ observer.addEventListener('mount', e => {
155
166
  module: e.module
156
167
  });
157
168
  });
158
-
159
169
  observer.addEventListener('dismount', e => {
160
170
  ...
161
171
  });
162
-
163
- observer.addEventListener('reconnect', e => {
172
+ observer.addEventListener('disconnect', e => {
164
173
  ...
165
174
  });
166
- observer.addEventListener('disconnect', e => {
175
+ observer.addEventListener('reconnect', e => {
167
176
  ...
168
177
  });
169
178
  observer.addEventListener('reconfirm', e => {
170
179
  ...
171
180
  });
172
- observer.addEventListener('out-of-scope', e => {
181
+ observer.addEventListener('exit', e => {
182
+ ...
183
+ });
184
+ observer.addEventListener('forget', e => {
173
185
  ...
174
186
  });
175
187
  ```
@@ -184,49 +196,220 @@ If an element that is in "mounted" state according to a MountObserver instance i
184
196
 
185
197
  1) "disconnect" event is dispatched from the MountObserver instance the moment the mounted element is disconnected from the DOM fragment.
186
198
  2) If/when the element is added somewhere else in the DOM tree, the mountObserver instance will dispatch event "reconnect", regardless of where. [Note: can't polyfill this very easily]
187
- 3) If the mounted element is added outside the rootNode being observed, the mountObserver instance will dispatch event "outside-root-node", and the MountObserver instance will relinquish any further responsibility for this element. Ideally this would also be dispatched when the platform garbage collects the element as well after all hard references are relinquished.
188
- 4) If the new place it was added remains within the original rootNode and remains mounted, the MountObserver instance dispatches event "reconfirmed".
189
- 5) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
199
+ 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.
200
+ 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?).
201
+ 5) If the new place it was added remains within the original rootNode and remains mounted, the MountObserver instance dispatches event "reconfirmed".
202
+ 6) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
190
203
 
191
- ## Special support for observable attributes
204
+ ## A tribute to attributes
192
205
 
193
- Extra support is provided for monitoring attributes.
206
+ Extra support is provided for monitoring attributes. There are two primary reasons for needing to provide special support for attributes with this API:
207
+
208
+ 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.
209
+
210
+ 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.
211
+
212
+ ### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
194
213
 
195
214
  Example:
196
215
 
197
216
  ```html
198
217
  <div id=div>
199
- <span id=span></span>
218
+ <my-custom-element my-first-observed-attribute="hello"></my-custom-element>
200
219
  </div>
201
220
  <script type=module>
202
221
  import {MountObserver} from '../MountObserver.js';
203
222
  const mo = new MountObserver({
204
- match: '#span',
205
- attribMatches:[
223
+ on: '*',
224
+ whereInstanceOf: [MyCustomElement]
225
+ });
226
+ mo.addEventListener('parsed-attrs-changed', e => {
227
+ const {matchingElement, modifiedObjectFieldValues, preModifiedFieldValues} = e;
228
+ console.log({matchingElement, modifiedObjectFieldValues, preModifiedFieldValues});
229
+ });
230
+ mo.observe(div);
231
+ setTimeout(() => {
232
+ const myCustomElement = document.querySelector('my-custom-element');
233
+ myCustomElement.setAttribute('my-first-observed-attribute', 'good-bye');
234
+ }, 1000);
235
+ </script>
236
+ ```
237
+
238
+
239
+ ### Scenario 2 -- Custom Enhancements in userland
240
+
241
+ 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.
242
+
243
+ Suppose we have a progressive enhancement that we want to apply based on the presence of 1 or more attributes.
244
+
245
+ To make this discussion concrete, let's suppose the "canonical" names of those attributes are:
246
+
247
+ ```html
248
+ <div id=div>
249
+ <section
250
+ my-enhancement=greetings
251
+ my-enhancement-first-aspect=hello
252
+ my-enhancement-second-aspect=goodbye
253
+ my-enhancement-first-aspect-wow-this-is-deep
254
+ my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
255
+ ></section>
256
+ </div>
257
+ ```
258
+
259
+ Now suppose we are worried about namespace clashes, plus we want to serve environments where HTML5 compliance is a must.
260
+
261
+ So we also want to recognize additional attributes that should map to these canonical attributes:
262
+
263
+ We want to also support:
264
+
265
+ ```html
266
+ <div id=div>
267
+ <section class=hello
268
+ data-my-enhancement=greetings
269
+ data-my-enhancement-first-aspect=hello
270
+ data-my-enhancement-second-aspect=goodbye
271
+ data-my-enhancement-first-aspect-wow-this-is-deep
272
+ data-my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
273
+ ></section>
274
+ </div>
275
+ ```
276
+
277
+ 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-*).
278
+
279
+ 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?
280
+
281
+ So let's say we want to insist that on custom elements, we must have the data- prefix?
282
+
283
+ 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).
284
+
285
+ Here's what the api provides:
286
+
287
+ ## Option 1 -- The carpal syndrome syntax
288
+
289
+ ```JavaScript
290
+ import {MountObserver} from '../MountObserver.js';
291
+ const mo = new MountObserver({
292
+ on: '*',
293
+ whereAttr:{
294
+ isIn: [
295
+ 'data-my-enhancement',
296
+ 'data-my-enhancement-first-aspect',
297
+ 'data-my-enhancement-second-aspect',
298
+ 'enh-my-enhancement',
299
+ 'enh-my-enhancement-first-aspect',
300
+ 'enh-my-enhancement-second-aspect',
301
+ //...some ten more combinations not listed
206
302
  {
207
- names: ['test-1']
208
- }
303
+ name: 'my-enhancement',
304
+ builtIn: true
305
+ },
306
+ {
307
+ name: 'my-enhancement-first-attr',
308
+ builtIn: true
309
+ },
310
+ {
311
+ name: 'my-enhancement-second-aspect',
312
+ builtIn: true
313
+ },
314
+ ...
209
315
  ]
316
+
317
+ }
318
+ });
319
+ ```
320
+
321
+ ## Option 2 -- The DRY Way
322
+
323
+ ```JavaScript
324
+ import {MountObserver} from '../MountObserver.js';
325
+ const mo = new MountObserver({
326
+ on: '*',
327
+ whereAttr:{
328
+ hasRootIn: ['data', 'enh', 'data-enh'],
329
+ hasBase: 'my-enhancement',
330
+ hasBranchIn: ['first-aspect', 'second-aspect', ''],
331
+ hasLeafIn: {
332
+ 'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
333
+ }
334
+ }
335
+ });
336
+ ```
337
+
338
+ MountObserver provides a breakdown of the matching attribute when encountered:
339
+
340
+ ```html
341
+ <div id=div>
342
+ <section class=hello my-enhancement-first-attr-wow-this-is-deep="hello"></section>
343
+ </div>
344
+ <script type=module>
345
+ import {MountObserver} from '../MountObserver.js';
346
+ const mo = new MountObserver({
347
+ on: '*',
348
+ whereAttr:{
349
+ hasRootIn: ['data', 'enh', 'data-enh'],
350
+ hasBase: 'my-enhancement',
351
+ hasBranchIn: ['first-attr', 'second-attr', ''],
352
+ hasLeafIn: {
353
+ 'first-attr': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
354
+ }
355
+ }
210
356
  });
211
- mo.addEventListener('attr-change', e => {
357
+ mo.addEventListener('observed-attr-change', e => {
212
358
  console.log(e);
213
359
  // {
360
+ // matchingElement,
214
361
  // attrChangeInfo:{
215
- // name: 'test-1',
362
+ // name: 'data-my-enhancement-first-aspect-wow-this-is-deep'
363
+ // root: 'data',
364
+ // base: 'my-enhancement',
365
+ // branch: 'first-attr',
366
+ // leaf: 'wow-this-is-deep',
216
367
  // oldValue: null,
217
- // newValue: 'hello'
368
+ // newValue: 'good-bye'
218
369
  // idx: 0,
219
370
  // }
220
371
  // }
221
372
  });
222
373
  mo.observe(div);
223
374
  setTimeout(() => {
224
- span.setAttribute('test-1', 'hello')
375
+ const myCustomElement = document.querySelector('my-custom-element');
376
+ myCustomElement.setAttribute('data-my-enhancement-first-aspect-wow-this-is-deep', 'good-bye');
225
377
  }, 1000);
226
378
  </script>
227
379
  ```
228
380
 
381
+ Some libraries prefer to use the colon (:) rather than a dash to separate these levels of settings:
382
+
383
+ Possibly some libraries may prefer to mix it up a bit:
384
+
385
+
386
+ ```html
387
+ <div id=div>
388
+ <section class=hello
389
+ data-my-enhancement=greetings
390
+ data-my-enhancement:first-aspect=hello
391
+ data-my-enhancement:second-aspect=goodbye
392
+ data-my-enhancement:first-aspect--wow-this-is-deep
393
+ data-my-enhancement:first-aspect--have-you-considered-using-json-for-this=just-saying
394
+ ></section>
395
+ </div>
396
+ ```
397
+
398
+ To support, specify the delimiter thusly:
229
399
 
400
+ ```JavaScript
401
+ const mo = new MountObserver({
402
+ on: '*',
403
+ whereAttr:{
404
+ hasRootIn: ['data', 'enh', 'data-enh'],
405
+ hasBase: ['-', 'my-enhancement'],
406
+ hasBranchIn: [':', ['first-attr', 'second-attr', '']],
407
+ hasLeafIn: {
408
+ 'first-attr': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
409
+ }
410
+ }
411
+ });
412
+ ```
230
413
 
231
414
  ## Preemptive downloading
232
415
 
@@ -243,15 +426,14 @@ So for this we add option:
243
426
 
244
427
  ```JavaScript
245
428
  const observer = new MountObserver({
246
- match: 'my-element',
429
+ on: 'my-element',
247
430
  loading: 'eager',
248
431
  import: './my-element.js',
249
432
  do:{
250
- onMount: (matchingElement, {module}) => customElements.define(module.MyElement)
433
+ mount: (matchingElement, {module}) => customElements.define(module.MyElement)
251
434
  }
252
435
  })
253
436
  ```
254
437
 
255
438
  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.
256
439
 
257
-
package/doWhereAttr.js ADDED
@@ -0,0 +1,43 @@
1
+ import { AttrChangeEvent } from "./MountObserver.js";
2
+ export function doWhereAttr(whereAttr, attributeName, target, oldValue, mo) {
3
+ const { hasRootIn, hasBase, hasBranchIn } = whereAttr;
4
+ const name = attributeName;
5
+ let restOfName = name;
6
+ let root;
7
+ let branch;
8
+ let idx = 0;
9
+ const hasBaseIsString = typeof hasBase === 'string';
10
+ const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
11
+ const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
12
+ if (hasRootIn !== undefined) {
13
+ for (const rootTest in hasRootIn) {
14
+ if (restOfName.startsWith(rootTest)) {
15
+ root = rootTest;
16
+ restOfName = restOfName.substring(root.length + rootToBaseDelimiter.length);
17
+ break;
18
+ }
19
+ }
20
+ }
21
+ if (!restOfName.startsWith(baseSelector))
22
+ return;
23
+ restOfName = restOfName.substring(hasBase.length);
24
+ if (hasBranchIn) {
25
+ for (const branchTest in hasBranchIn) {
26
+ if (restOfName.startsWith(branchTest)) {
27
+ branch = branchTest;
28
+ break;
29
+ }
30
+ }
31
+ }
32
+ const newValue = target.getAttribute(attributeName);
33
+ const attrChangeInfo = {
34
+ name,
35
+ root,
36
+ base: baseSelector,
37
+ branch,
38
+ oldValue,
39
+ newValue,
40
+ idx
41
+ };
42
+ mo.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
43
+ }
@@ -0,0 +1,35 @@
1
+ export function getWhereAttrSelector(whereAttr, withoutAttrs) {
2
+ const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
3
+ const fullListOfAttrs = [];
4
+ //TODO: share this block with doWhereAttr?
5
+ const hasBaseIsString = typeof hasBase === 'string';
6
+ const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
7
+ const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
8
+ //end TODO
9
+ let prefixLessMatches = [baseSelector];
10
+ if (hasBranchIn !== undefined) {
11
+ let baseToBranchDelimiter = '-';
12
+ let branches;
13
+ if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
14
+ baseToBranchDelimiter = hasBranchIn[0];
15
+ branches = hasBranchIn[1];
16
+ }
17
+ else {
18
+ branches = hasBranchIn;
19
+ }
20
+ prefixLessMatches = branches.map(x => `${baseSelector}${baseToBranchDelimiter}x`);
21
+ }
22
+ const stems = hasRootIn || [''];
23
+ for (const stem of stems) {
24
+ const prefix = typeof stem === 'string' ? stem : stem.path;
25
+ for (const prefixLessMatch of prefixLessMatches) {
26
+ fullListOfAttrs.push(prefix.length === 0 ? prefixLessMatch : `${prefix}${rootToBaseDelimiter}${prefixLessMatch}`);
27
+ }
28
+ }
29
+ const listOfSelectors = fullListOfAttrs.map(s => `${withoutAttrs}[${s}]`);
30
+ const calculatedSelector = listOfSelectors.join(',');
31
+ return {
32
+ fullListOfAttrs,
33
+ calculatedSelector
34
+ };
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
package/types.d.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  export interface MountInit{
2
- readonly match?: CSSMatch,
3
- readonly attribMatches?: Array<AttribMatch>,
2
+ readonly on?: CSSMatch,
3
+ //readonly attribMatches?: Array<AttribMatch>,
4
+ readonly whereAttr?: WhereAttr,
4
5
  readonly whereElementIntersectsWith?: IntersectionObserverInit,
5
6
  readonly whereMediaMatches?: MediaQuery,
6
7
  readonly whereInstanceOf?: Array<typeof Node>, //[TODO] What's the best way to type this?,
7
8
  readonly whereSatisfies?: PipelineProcessor<boolean>,
8
9
  readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
9
10
  readonly do?: {
10
- readonly onMount?: PipelineProcessor,
11
- readonly onDismount?: PipelineProcessor,
12
- readonly onDisconnect?: PipelineProcessor,
13
- readonly onReconfirmed?: PipelineProcessor,
14
- readonly onOutsideRootNode?: PipelineProcessor,
11
+ readonly mount?: PipelineProcessor,
12
+ readonly dismount?: PipelineProcessor,
13
+ readonly disconnect?: PipelineProcessor,
14
+ readonly reconfirm?: PipelineProcessor,
15
+ readonly exit?: PipelineProcessor,
15
16
  }
16
17
  // /**
17
18
  // * Purpose -- there are scenarios where we may only want to affect changes that occur after the initial
@@ -19,6 +20,14 @@ export interface MountInit{
19
20
  // */
20
21
  // readonly ignoreInitialMatches?: boolean,
21
22
  }
23
+ export interface WhereAttr{
24
+ hasBase: string | [string, string],
25
+ hasBranchIn?: Array<string> | [string, Array<string>],
26
+ hasRootIn?: Array<string | {
27
+ path: string,
28
+ context: 'BuiltIn' | 'CustomElement' | 'Both'
29
+ }>,
30
+ }
22
31
  type CSSMatch = string;
23
32
  type ImportString = string;
24
33
  type MediaQuery = string;
@@ -62,6 +71,10 @@ export interface AddMutationEventListener {
62
71
 
63
72
  interface AttrChangeInfo{
64
73
  name: string,
74
+ root?: string,
75
+ base?: string,
76
+ branch?: string,
77
+ leaf?: string, //TODO
65
78
  oldValue: string | null,
66
79
  newValue: string | null,
67
80
  idx: number,