mount-observer 0.0.7 → 0.0.9

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
@@ -3,13 +3,14 @@ const mutationObserverLookup = new WeakMap();
3
3
  const refCount = new WeakMap();
4
4
  export class MountObserver extends EventTarget {
5
5
  #mountInit;
6
- #rootMutObs;
6
+ //#rootMutObs: RootMutObs | undefined;
7
7
  #abortController;
8
8
  #mounted;
9
9
  #mountedList;
10
10
  #disconnected;
11
11
  //#unmounted: WeakSet<Element>;
12
12
  #isComplex;
13
+ #observe;
13
14
  constructor(init) {
14
15
  super();
15
16
  const { on, whereElementIntersectsWith, whereMediaMatches } = init;
@@ -28,26 +29,107 @@ export class MountObserver extends EventTarget {
28
29
  //this.#unmounted = new WeakSet();
29
30
  }
30
31
  #calculatedSelector;
31
- get #selector() {
32
+ #fullListOfAttrs;
33
+ //get #attrVals
34
+ async #selector() {
32
35
  if (this.#calculatedSelector !== undefined)
33
36
  return this.#calculatedSelector;
34
37
  const { on, whereAttr } = this.#mountInit;
35
- const base = on || '*';
38
+ const withoutAttrs = on || '*';
36
39
  if (whereAttr === undefined)
37
- return base;
38
- const { withFirstName, andQualifiers, withStemsIn } = whereAttr;
39
- const matches = [];
40
- const prefixLessMatches = andQualifiers === undefined ? [withFirstName]
41
- : andQualifiers.map(x => `${withFirstName}-${x}`);
42
- const stems = withStemsIn || ['data', 'enh', 'data-enh'];
43
- for (const stem of stems) {
44
- const prefix = typeof stem === 'string' ? stem : stem.stem;
45
- for (const prefixLessMatch of prefixLessMatches) {
46
- matches.push(`${prefix}-${prefixLessMatch}`);
40
+ return withoutAttrs;
41
+ const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
42
+ const info = getWhereAttrSelector(whereAttr, withoutAttrs);
43
+ const { fullListOfAttrs, calculatedSelector } = info;
44
+ this.#fullListOfAttrs = fullListOfAttrs;
45
+ this.#calculatedSelector = calculatedSelector;
46
+ return this.#calculatedSelector;
47
+ }
48
+ async #birtualizeFragment(fragment, level) {
49
+ const bis = Array.from(fragment.querySelectorAll(biQry));
50
+ for (const bi of bis) {
51
+ await this.#birtalizeMatch(bi, level);
52
+ }
53
+ }
54
+ async #birtalizeMatch(el, level) {
55
+ const href = el.getAttribute('href');
56
+ el.removeAttribute('href');
57
+ const templID = href.substring(1);
58
+ const fragment = this.#observe?.deref();
59
+ if (fragment === undefined)
60
+ return;
61
+ const templ = this.#findByID(templID, fragment);
62
+ if (!(templ instanceof HTMLTemplateElement))
63
+ throw 404;
64
+ const clone = templ.content.cloneNode(true);
65
+ const slots = el.querySelectorAll(`[slot]`);
66
+ for (const slot of slots) {
67
+ const name = slot.getAttribute('slot');
68
+ const targets = Array.from(clone.querySelectorAll(`slot[name="${name}"]`));
69
+ for (const target of targets) {
70
+ const slotClone = slot.cloneNode(true);
71
+ target.after(slotClone);
72
+ target.remove();
47
73
  }
48
74
  }
49
- this.#calculatedSelector = matches.join(',');
50
- return this.#calculatedSelector;
75
+ this.#birtualizeFragment(clone, level + 1);
76
+ if (level === 0) {
77
+ const slotMap = el.getAttribute('slotmap');
78
+ let map = slotMap === null ? undefined : JSON.parse(slotMap);
79
+ const slots = Array.from(clone.querySelectorAll('[slot]'));
80
+ for (const slot of slots) {
81
+ if (map !== undefined) {
82
+ const slotName = slot.slot;
83
+ for (const key in map) {
84
+ if (slot.matches(key)) {
85
+ const targetAttSymbols = map[key];
86
+ for (const sym of targetAttSymbols) {
87
+ switch (sym) {
88
+ case '|':
89
+ slot.setAttribute('itemprop', slotName);
90
+ break;
91
+ case '$':
92
+ slot.setAttribute('itemscope', '');
93
+ slot.setAttribute('itemprop', slotName);
94
+ break;
95
+ case '@':
96
+ slot.setAttribute('name', slotName);
97
+ break;
98
+ case '.':
99
+ slot.classList.add(slotName);
100
+ break;
101
+ case '%':
102
+ slot.part.add(slotName);
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ slot.removeAttribute('slot');
110
+ }
111
+ el.dispatchEvent(new LoadEvent(clone));
112
+ //console.log('dispatched')
113
+ }
114
+ el.before(clone);
115
+ el.remove();
116
+ }
117
+ #templLookUp = new Map();
118
+ #findByID(id, fragment) {
119
+ if (this.#templLookUp.has(id))
120
+ return this.#templLookUp.get(id);
121
+ let templ = fragment.getElementById(id);
122
+ if (templ === null) {
123
+ let rootToSearchOutwardFrom = ((fragment.isConnected ? fragment.getRootNode() : this.#mountInit.withTargetShadowRoot) || document);
124
+ templ = rootToSearchOutwardFrom.getElementById(id);
125
+ while (templ === null && rootToSearchOutwardFrom !== document) {
126
+ rootToSearchOutwardFrom = (rootToSearchOutwardFrom.host || rootToSearchOutwardFrom).getRootNode();
127
+ templ = rootToSearchOutwardFrom.getElementById(id);
128
+ }
129
+ }
130
+ if (templ !== null)
131
+ this.#templLookUp.set(id, templ);
132
+ return templ;
51
133
  }
52
134
  unobserve(within) {
53
135
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
@@ -75,6 +157,7 @@ export class MountObserver extends EventTarget {
75
157
  }
76
158
  }
77
159
  async observe(within) {
160
+ this.#observe = new WeakRef(within);
78
161
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
79
162
  if (!mutationObserverLookup.has(nodeToMonitor)) {
80
163
  mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
@@ -90,8 +173,9 @@ export class MountObserver extends EventTarget {
90
173
  }
91
174
  }
92
175
  const rootMutObs = mutationObserverLookup.get(within);
93
- const { attribMatches } = this.#mountInit;
94
- rootMutObs.addEventListener('mutation-event', (e) => {
176
+ //const {whereAttr} = this.#mountInit;
177
+ const fullListOfAttrs = this.#fullListOfAttrs;
178
+ rootMutObs.addEventListener('mutation-event', async (e) => {
95
179
  //TODO: disconnected
96
180
  if (this.#isComplex) {
97
181
  this.#inspectWithin(within, false);
@@ -108,22 +192,11 @@ export class MountObserver extends EventTarget {
108
192
  addedElements.forEach(x => elsToInspect.push(x));
109
193
  if (type === 'attributes') {
110
194
  const { target, attributeName, oldValue } = mutationRecord;
111
- if (target instanceof Element && attributeName !== null && attribMatches !== undefined && this.#mounted.has(target)) {
112
- let idx = 0;
113
- for (const attrMatch of attribMatches) {
114
- const { names } = attrMatch;
115
- if (names.includes(attributeName)) {
195
+ if (target instanceof Element && attributeName !== null && this.#mounted.has(target)) {
196
+ if (fullListOfAttrs !== undefined) {
197
+ const idx = fullListOfAttrs.indexOf(attributeName);
198
+ if (idx > -1) {
116
199
  const newValue = target.getAttribute(attributeName);
117
- // let parsedNewValue = undefined;
118
- // switch(type){
119
- // case 'boolean':
120
- // parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
121
- // break;
122
- // case 'date':
123
- // parsedNewValue = newValue === null ? null : new Date(newValue);
124
- // break;
125
- // case ''
126
- // }
127
200
  const attrChangeInfo = {
128
201
  name: attributeName,
129
202
  oldValue,
@@ -132,7 +205,13 @@ export class MountObserver extends EventTarget {
132
205
  };
133
206
  this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
134
207
  }
135
- idx++;
208
+ }
209
+ else {
210
+ const { whereAttr } = this.#mountInit;
211
+ if (whereAttr !== undefined) {
212
+ const { doWhereAttr } = await import('./doWhereAttr.js');
213
+ doWhereAttr(whereAttr, attributeName, target, oldValue, this);
214
+ }
136
215
  }
137
216
  }
138
217
  elsToInspect.push(target);
@@ -164,9 +243,10 @@ export class MountObserver extends EventTarget {
164
243
  }
165
244
  async #mount(matching, initializing) {
166
245
  //first unmount non matching
167
- const alreadyMounted = this.#filterAndDismount();
246
+ const alreadyMounted = await this.#filterAndDismount();
168
247
  const mount = this.#mountInit.do?.mount;
169
- const { import: imp, attribMatches } = this.#mountInit;
248
+ const { import: imp } = this.#mountInit;
249
+ const fullListOfAttrs = this.#fullListOfAttrs;
170
250
  for (const match of matching) {
171
251
  if (alreadyMounted.has(match))
172
252
  continue;
@@ -197,27 +277,33 @@ export class MountObserver extends EventTarget {
197
277
  });
198
278
  }
199
279
  this.dispatchEvent(new MountEvent(match, initializing));
200
- if (attribMatches !== undefined) {
201
- let idx = 0;
202
- for (const attribMatch of attribMatches) {
203
- let newValue = null;
204
- const { names } = attribMatch;
205
- let nonNullName = names[0];
206
- for (const name of names) {
207
- const attrVal = match.getAttribute(name);
208
- if (attrVal !== null)
209
- nonNullName = name;
210
- newValue = newValue || attrVal;
280
+ if (fullListOfAttrs !== undefined) {
281
+ const { whereAttr } = this.#mountInit;
282
+ for (const name of fullListOfAttrs) {
283
+ if (whereAttr !== undefined) {
284
+ const { doWhereAttr } = await import('./doWhereAttr.js');
285
+ doWhereAttr(whereAttr, name, match, null, this);
211
286
  }
212
- const attribInfo = {
213
- oldValue: null,
214
- newValue,
215
- idx,
216
- name: nonNullName
217
- };
218
- this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
219
- idx++;
220
287
  }
288
+ // let idx = 0;
289
+ // for(const attribMatch of attribMatches){
290
+ // let newValue = null;
291
+ // const {names} = attribMatch;
292
+ // let nonNullName = names[0];
293
+ // for(const name of names){
294
+ // const attrVal = match.getAttribute(name);
295
+ // if(attrVal !== null) nonNullName = name;
296
+ // newValue = newValue || attrVal;
297
+ // }
298
+ // const attribInfo: AttrChangeInfo = {
299
+ // oldValue: null,
300
+ // newValue,
301
+ // idx,
302
+ // name: nonNullName
303
+ // };
304
+ // this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
305
+ // idx++;
306
+ // }
221
307
  }
222
308
  this.#mountedList?.push(new WeakRef(match));
223
309
  //if(this.#unmounted.has(match)) this.#unmounted.delete(match);
@@ -232,12 +318,12 @@ export class MountObserver extends EventTarget {
232
318
  this.dispatchEvent(new DismountEvent(unmatch));
233
319
  }
234
320
  }
235
- #filterAndDismount() {
321
+ async #filterAndDismount() {
236
322
  const returnSet = new Set();
237
323
  if (this.#mountedList !== undefined) {
238
324
  const previouslyMounted = this.#mountedList.map(x => x.deref());
239
325
  const { whereSatisfies, whereInstanceOf } = this.#mountInit;
240
- const match = this.#selector;
326
+ const match = await this.#selector();
241
327
  const elsToUnMount = previouslyMounted.filter(x => {
242
328
  if (x === undefined)
243
329
  return false;
@@ -257,7 +343,7 @@ export class MountObserver extends EventTarget {
257
343
  }
258
344
  async #filterAndMount(els, checkMatch, initializing) {
259
345
  const { whereSatisfies, whereInstanceOf } = this.#mountInit;
260
- const match = this.#selector;
346
+ const match = await this.#selector();
261
347
  const elsToMount = els.filter(x => {
262
348
  if (checkMatch) {
263
349
  if (!x.matches(match))
@@ -273,14 +359,21 @@ export class MountObserver extends EventTarget {
273
359
  }
274
360
  return true;
275
361
  });
362
+ for (const elToMount of elsToMount) {
363
+ if (elToMount.matches(biQry)) {
364
+ await this.#birtalizeMatch(elToMount, 0);
365
+ }
366
+ }
276
367
  this.#mount(elsToMount, initializing);
277
368
  }
278
369
  async #inspectWithin(within, initializing) {
279
- const els = Array.from(within.querySelectorAll(this.#selector));
370
+ await this.#birtualizeFragment(within, 0);
371
+ const els = Array.from(within.querySelectorAll(await this.#selector()));
280
372
  this.#filterAndMount(els, false, initializing);
281
373
  }
282
374
  }
283
375
  const refCountErr = 'mount-observer ref count mismatch';
376
+ const biQry = 'b-i[href^="#"]:not([disabled])';
284
377
  // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
285
378
  /**
286
379
  * The `mutation-event` event represents something that happened.
@@ -322,3 +415,12 @@ export class AttrChangeEvent extends Event {
322
415
  this.attrChangeInfo = attrChangeInfo;
323
416
  }
324
417
  }
418
+ export class LoadEvent extends Event {
419
+ clone;
420
+ static eventName = 'load';
421
+ constructor(clone) {
422
+ super(LoadEvent.eventName);
423
+ this.clone = clone;
424
+ }
425
+ }
426
+ //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,7 +11,7 @@ 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: 2024-2-11
14
+ Last Update: 2024-2-17
14
15
 
15
16
  ## Benefits of this API
16
17
 
@@ -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
 
@@ -38,7 +39,7 @@ The amount of code necessary to accomplish these common tasks designed to improv
38
39
  1. Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
39
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?
40
41
  3. Supporting "progressive enhancement" more effectively.
41
- 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 surfacing up the developer "found one!".
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!".
42
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?
43
44
 
44
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.
@@ -100,6 +101,8 @@ const observer = new MountObserver({
100
101
  })
101
102
  ```
102
103
 
104
+ ... would work.
105
+
103
106
  This would allow developers to create "stylesheet" like capabilities.
104
107
 
105
108
 
@@ -150,7 +153,9 @@ const observer = new MountObserver({
150
153
  })
151
154
  ```
152
155
 
153
- 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:
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.
157
+
158
+ 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:
154
159
 
155
160
  ## Subscribing
156
161
 
@@ -206,7 +211,7 @@ Being that for both custom elements, as well as (hopefully) [custom enhancements
206
211
 
207
212
  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.
208
213
 
209
- ### Scenario 1 -- Custom Element integration with ObserveObservableAttributes API [WIP]
214
+ ### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
210
215
 
211
216
  Example:
212
217
 
@@ -247,6 +252,8 @@ To make this discussion concrete, let's suppose the "canonical" names of those a
247
252
  my-enhancement=greetings
248
253
  my-enhancement-first-aspect=hello
249
254
  my-enhancement-second-aspect=goodbye
255
+ my-enhancement-first-aspect-wow-this-is-deep
256
+ my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
250
257
  ></section>
251
258
  </div>
252
259
  ```
@@ -263,17 +270,19 @@ We want to also support:
263
270
  data-my-enhancement=greetings
264
271
  data-my-enhancement-first-aspect=hello
265
272
  data-my-enhancement-second-aspect=goodbye
273
+ data-my-enhancement-first-aspect-wow-this-is-deep
274
+ data-my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
266
275
  ></section>
267
276
  </div>
268
277
  ```
269
278
 
270
279
  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-*).
271
280
 
272
- 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-first-aspect? (Okay, with this particular example, the names are so long and generic it's unlikely, but who would ever use such a long, generic name in practice?)
281
+ 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?
273
282
 
274
- So let's say we want to insist that on custom elements, we must have the dat- prefix?
283
+ So let's say we want to insist that on custom elements, we must have the data- prefix?
275
284
 
276
- And we want to support an alternative prefix to data, say enh-*, endorsed by [this proposal](https://github.com/WICG/webcomponents/issues/1000).
285
+ 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).
277
286
 
278
287
  Here's what the api provides:
279
288
 
@@ -291,18 +300,20 @@ const mo = new MountObserver({
291
300
  'enh-my-enhancement',
292
301
  'enh-my-enhancement-first-aspect',
293
302
  'enh-my-enhancement-second-aspect',
303
+ //...some ten more combinations not listed
294
304
  {
295
305
  name: 'my-enhancement',
296
306
  builtIn: true
297
307
  },
298
308
  {
299
- name: 'my-enhancement-first-attr',
309
+ name: 'my-enhancement-first-aspect',
300
310
  builtIn: true
301
311
  },
302
312
  {
303
313
  name: 'my-enhancement-second-aspect',
304
314
  builtIn: true
305
- }
315
+ },
316
+ ...
306
317
  ]
307
318
 
308
319
  }
@@ -316,30 +327,33 @@ import {MountObserver} from '../MountObserver.js';
316
327
  const mo = new MountObserver({
317
328
  on: '*',
318
329
  whereAttr:{
319
- withFirstName: 'my-enhancement',
320
- andQualifiers: ['first-attr', 'second-attr', ''],
321
- withStemsIn: ['data', 'enh', 'data-enh'],
330
+ hasRootIn: ['data', 'enh', 'data-enh'],
331
+ hasBase: 'my-enhancement',
332
+ hasBranchIn: ['first-aspect', 'second-aspect', ''],
333
+ hasLeafIn: {
334
+ 'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
335
+ }
322
336
  }
323
337
  });
324
338
  ```
325
339
 
326
- MountObserver supports both approaches
340
+ MountObserver provides a breakdown of the matching attribute when encountered:
327
341
 
328
342
  ```html
329
343
  <div id=div>
330
- <section class=hello my-enhancement-first-attr="hello"></section>
344
+ <section class=hello my-enhancement-first-aspect-wow-this-is-deep="hello"></section>
331
345
  </div>
332
346
  <script type=module>
333
347
  import {MountObserver} from '../MountObserver.js';
334
348
  const mo = new MountObserver({
335
349
  on: '*',
336
350
  whereAttr:{
337
- hasPrefixesIn: ['enh-', 'data-enh-'],
338
- hasSuffixesIn:[
339
- 'my-enhancement-first-attr',
340
- 'my-enhancement-second-aspect'
341
- ],
342
-
351
+ hasRootIn: ['data', 'enh', 'data-enh'],
352
+ hasBase: 'my-enhancement',
353
+ hasBranchIn: ['first-aspect', 'second-aspect', ''],
354
+ hasLeafIn: {
355
+ 'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
356
+ }
343
357
  }
344
358
  });
345
359
  mo.addEventListener('observed-attr-change', e => {
@@ -347,10 +361,13 @@ MountObserver supports both approaches
347
361
  // {
348
362
  // matchingElement,
349
363
  // attrChangeInfo:{
350
- // fullName: 'data-enh-my-first-enhancement-attr',
351
- // suffix: 'my-first-enhancement-attr',
364
+ // name: 'data-my-enhancement-first-aspect-wow-this-is-deep'
365
+ // root: 'data',
366
+ // base: 'my-enhancement',
367
+ // branch: 'first-aspect',
368
+ // leaf: 'wow-this-is-deep',
352
369
  // oldValue: null,
353
- // newValue: 'hello'
370
+ // newValue: 'good-bye'
354
371
  // idx: 0,
355
372
  // }
356
373
  // }
@@ -358,11 +375,44 @@ MountObserver supports both approaches
358
375
  mo.observe(div);
359
376
  setTimeout(() => {
360
377
  const myCustomElement = document.querySelector('my-custom-element');
361
- myCustomElement.setAttribute('my-first-observed-attribute', 'good-bye');
378
+ myCustomElement.setAttribute('data-my-enhancement-first-aspect-wow-this-is-deep', 'good-bye');
362
379
  }, 1000);
363
380
  </script>
364
381
  ```
365
382
 
383
+ Some libraries prefer to use the colon (:) rather than a dash to separate these levels of settings:
384
+
385
+ Possibly some libraries may prefer to mix it up a bit:
386
+
387
+
388
+ ```html
389
+ <div id=div>
390
+ <section class=hello
391
+ data-my-enhancement=greetings
392
+ data-my-enhancement:first-aspect=hello
393
+ data-my-enhancement:second-aspect=goodbye
394
+ data-my-enhancement:first-aspect--wow-this-is-deep
395
+ data-my-enhancement:first-aspect--have-you-considered-using-json-for-this=just-saying
396
+ ></section>
397
+ </div>
398
+ ```
399
+
400
+ To support such syntax, specify the delimiter thusly:
401
+
402
+ ```JavaScript
403
+ const mo = new MountObserver({
404
+ on: '*',
405
+ whereAttr:{
406
+ hasRootIn: ['data', 'enh', 'data-enh'],
407
+ hasBase: ['-', 'my-enhancement'],
408
+ hasBranchIn: [':', ['first-aspect', 'second-aspect', '']],
409
+ hasLeafIn: {
410
+ 'first-aspect': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
411
+ }
412
+ }
413
+ });
414
+ ```
415
+
366
416
  ## Preemptive downloading
367
417
 
368
418
  There are two significant steps to imports, each of which imposes a cost:
@@ -389,4 +439,47 @@ const observer = new MountObserver({
389
439
 
390
440
  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.
391
441
 
442
+ ## Birtual Inclusions
443
+
444
+ 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.
445
+
446
+ The mount-observer is always on the lookout for a special tag that takes the form:
447
+
448
+ ```html
449
+ <div>Some prior stuff</div>
450
+ <b-i href=#my-snippet-of-html>
451
+ <div slot=slot1>hello</div>
452
+ <div slot=slot2>goodbye<div>
453
+ </b-i>
454
+ <div>Some additional stuff</div>
455
+ ```
456
+
457
+ "b-i" stands for "birtual inclusion" where birtual is a made up word that is a portmanteau of birth and virtual.
458
+
459
+ When it encounters such a thing, it searches "upwardly" through the chain of ShadowRoots for a template with id=my-kinda-sorta-custom-element (in this case), and caches them as it finds them.
460
+
461
+ Let's say the template looks as follows:
462
+
463
+ ```html
464
+ <template id=my-snippet-of-html>
465
+ This is an example of a snippet of HTML that appears repeatedly.
466
+ <slot name=slot1></slot>
467
+ <slot name=slot2></slot>
468
+ </template>
469
+ ```
470
+
471
+ What we would end up with is:
472
+
473
+
474
+ ```html
475
+ <div>Some prior stuff</div>
476
+ This is an example of a snippet of HTML that appears repeatedly.
477
+ <div>hello</div>
478
+ <div>goodbye</div>
479
+ <div>Some additional stuff</div>
480
+ ```
481
+
482
+ Some significant differences with genuine slot support as used with (ShadowDOM'd) custom elements
392
483
 
484
+ 1. There is no mechanism for updating the slots. That is something under investigation with this userland [custom enhancement](https://github.com/bahrus/be-inclusive), that could possibly lead to a future implementation request tied to template instantiation.
485
+ 2. ShadowDOM's slots act on a "many to one" basis. Multiple light children with identical slot identifiers all get merged into a single (first?) matching slot within the Shadow DOM. These birtual inclusions, instead, follow the opposite approach -- a single element with a slot identifier can get cloned into multiple slot targets as it weaves itself into the templates as they get merged together.
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.7",
3
+ "version": "0.0.9",
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,14 +1,8 @@
1
1
  export interface MountInit{
2
2
  readonly on?: CSSMatch,
3
- readonly attribMatches?: Array<AttribMatch>,
4
- readonly whereAttr?: {
5
- withFirstName: string,
6
- andQualifiers?: Array<string>,
7
- withStemsIn?: Array<string | {
8
- stem: string,
9
- context: 'BuiltIn' | 'CustomElement'
10
- }>,
11
- },
3
+ //readonly attribMatches?: Array<AttribMatch>,
4
+ readonly withTargetShadowRoot?: ShadowRoot,
5
+ readonly whereAttr?: WhereAttr,
12
6
  readonly whereElementIntersectsWith?: IntersectionObserverInit,
13
7
  readonly whereMediaMatches?: MediaQuery,
14
8
  readonly whereInstanceOf?: Array<typeof Node>, //[TODO] What's the best way to type this?,
@@ -27,6 +21,14 @@ export interface MountInit{
27
21
  // */
28
22
  // readonly ignoreInitialMatches?: boolean,
29
23
  }
24
+ export interface WhereAttr{
25
+ hasBase: string | [string, string],
26
+ hasBranchIn?: Array<string> | [string, Array<string>],
27
+ hasRootIn?: Array<string | {
28
+ path: string,
29
+ context: 'BuiltIn' | 'CustomElement' | 'Both'
30
+ }>,
31
+ }
30
32
  type CSSMatch = string;
31
33
  type ImportString = string;
32
34
  type MediaQuery = string;
@@ -70,6 +72,10 @@ export interface AddMutationEventListener {
70
72
 
71
73
  interface AttrChangeInfo{
72
74
  name: string,
75
+ root?: string,
76
+ base?: string,
77
+ branch?: string,
78
+ leaf?: string, //TODO
73
79
  oldValue: string | null,
74
80
  newValue: string | null,
75
81
  idx: number,
@@ -119,9 +125,20 @@ export type attrChangeEventName = 'attr-change';
119
125
  export interface IAttrChangeEvent extends IMountEvent {
120
126
  attrChangeInfo: AttrChangeInfo,
121
127
  }
122
- export type attrChangeEventHander = (e: IAttrChangeEvent) => void;
123
- export interface AddAttrChangeEventistener{
124
- addEventListener(eventName: attrChangeEventName, handler: attrChangeEventHander, options?: AddEventListenerOptions): void;
128
+ export type attrChangeEventHandler = (e: IAttrChangeEvent) => void;
129
+ export interface AddAttrChangeEventListener{
130
+ addEventListener(eventName: attrChangeEventName, handler: attrChangeEventHandler, options?: AddEventListenerOptions): void;
131
+ }
132
+ //#endregion
133
+
134
+ //#region load event
135
+ export type loadEventName = 'load';
136
+ export interface ILoadEvent {
137
+ clone: DocumentFragment
138
+ }
139
+ export type loadEventHandler = (e: ILoadEvent) => void;
140
+ export interface AddLoadEventListener{
141
+ addEventListener(eventName: loadEventName, handler: loadEventHandler, options?: AddEventListenerOptions): void
125
142
  }
126
143
  //#endregion
127
144