mount-observer 0.1.1 → 0.1.2
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 +76 -77
- package/MountObserver.ts +89 -89
- package/README.md +344 -24
- package/attrChanges.js +70 -0
- package/attrChanges.ts +90 -0
- package/emitEvents.js +103 -0
- package/emitEvents.ts +126 -0
- package/index.ts +4 -0
- package/mediaQuery.js +14 -11
- package/mediaQuery.ts +16 -13
- package/package.json +13 -1
- package/types.d.ts +17 -0
- package/whereOutside.js +19 -0
- package/whereOutside.ts +25 -0
package/README.md
CHANGED
|
@@ -5,6 +5,44 @@
|
|
|
5
5
|
|
|
6
6
|
Note that much of what is described below has not yet been polyfilled.
|
|
7
7
|
|
|
8
|
+
## Implementation Status
|
|
9
|
+
|
|
10
|
+
The following features have been implemented and tested:
|
|
11
|
+
|
|
12
|
+
### Core Functionality
|
|
13
|
+
- ✅ **whereElementMatches**: CSS selector-based element matching
|
|
14
|
+
- ✅ **whereAttr**: Complex attribute-based matching with:
|
|
15
|
+
- Built-in vs custom element distinction
|
|
16
|
+
- Attribute prefix variations (data-, enh-, data-enh-)
|
|
17
|
+
- Hierarchical attribute branches with customizable delimiters
|
|
18
|
+
- Coordinate system for attribute mapping
|
|
19
|
+
- ✅ **whereInstanceOf**: Constructor-based element filtering (single or array)
|
|
20
|
+
- ✅ **whereMediaMatches**: Media query-based conditional mounting (string or MediaQueryList)
|
|
21
|
+
- ✅ **whereOutside**: Donut hole scoping (exclude elements inside matching ancestors)
|
|
22
|
+
|
|
23
|
+
### Lifecycle & Events
|
|
24
|
+
- ✅ **mount/dismount/disconnect events**: Element lifecycle tracking
|
|
25
|
+
- ✅ **attrchange event**: Attribute change notifications with batching
|
|
26
|
+
- ✅ **mediamatch/mediaunmatch events**: Media query state change notifications (with `getPlayByPlay` option)
|
|
27
|
+
- ✅ **load event**: Import completion notification
|
|
28
|
+
|
|
29
|
+
### Advanced Features
|
|
30
|
+
- ✅ **Dynamic imports**: Lazy loading of JavaScript modules
|
|
31
|
+
- ✅ **assignGingerly**: Property assignment on mount
|
|
32
|
+
- ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
|
|
33
|
+
- ✅ **map configuration**: Metadata mapping for attribute coordinates
|
|
34
|
+
- ✅ **once option**: Fire attrchange event only once per attribute
|
|
35
|
+
- ✅ **Shared MutationObserver**: Efficient observer sharing across instances
|
|
36
|
+
- ✅ **Code splitting**: Conditional features loaded on-demand
|
|
37
|
+
- ✅ **Memory management**: WeakRef usage for DOM node references
|
|
38
|
+
|
|
39
|
+
### Not Yet Implemented
|
|
40
|
+
- ❌ Intersection observer integration
|
|
41
|
+
- ❌ Container query support
|
|
42
|
+
- ❌ Shadow DOM traversal utilities
|
|
43
|
+
- ❌ Reconnect event handling
|
|
44
|
+
- ❌ Multiple import types (CSS, JSON, HTML)
|
|
45
|
+
|
|
8
46
|
# The MountObserver api.
|
|
9
47
|
|
|
10
48
|
Author: Bruce B. Anderson (with valuable feedback from @doeixd )
|
|
@@ -102,7 +140,7 @@ The "observer" constant above is a class instance that inherits from EventTarget
|
|
|
102
140
|
|
|
103
141
|
In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but has to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable.
|
|
104
142
|
|
|
105
|
-
If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "
|
|
143
|
+
If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "select" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support, maybe even after the browser vendors every provide a selector-observer (if ever).
|
|
106
144
|
|
|
107
145
|
So the developer could use:
|
|
108
146
|
|
|
@@ -110,8 +148,8 @@ So the developer could use:
|
|
|
110
148
|
|
|
111
149
|
```JavaScript
|
|
112
150
|
const observer = new MountObserver({
|
|
113
|
-
import: './my-element.js',
|
|
114
151
|
whereElementMatches:'my-element',
|
|
152
|
+
import: './my-element.js',
|
|
115
153
|
do: ({localName}, {modules, observer, observeInfo}) => {
|
|
116
154
|
if(!customElements.get(localName)) {
|
|
117
155
|
customElements.define(localName, modules[0].MyElement);
|
|
@@ -123,10 +161,12 @@ const observer = new MountObserver({
|
|
|
123
161
|
observer.observe(document);
|
|
124
162
|
```
|
|
125
163
|
|
|
126
|
-
and could perhaps expect faster binding as a result of the more limited supported expressions. Since "select" is not specified, it is assumed to be "*"
|
|
164
|
+
and could perhaps expect faster binding as a result of the more limited supported expressions. Since "select" is not specified, it is assumed to be "*".
|
|
127
165
|
|
|
128
166
|
This polyfill in fact only supports this latter option ("whreElementMatches"), and leaves "select" for such a time as when a selector observer is available in the platform.
|
|
129
167
|
|
|
168
|
+
[Implemented as Requirement 1](requirements/Requirement1.md).
|
|
169
|
+
|
|
130
170
|
## The import key
|
|
131
171
|
|
|
132
172
|
This proposal has been amended to support multiple imports, including of different types:
|
|
@@ -154,7 +194,9 @@ The do function won't be invoked until all the imports have been successfully co
|
|
|
154
194
|
|
|
155
195
|
Previously, this proposal called for allowing arrow functions as well, thinking that could be a good interim way to support bundlers, as well as multiple imports. But the valuable input provided by [doeixd](https://github.com/doeixd) makes me think that that interim support could more effectively be done by the developer in the do methods.
|
|
156
196
|
|
|
157
|
-
This proposal would also include support for JSON and HTML module imports (really, all types).
|
|
197
|
+
This proposal would also include support for JSON and HTML module imports (really, all types).
|
|
198
|
+
|
|
199
|
+
[Implemented as Requirement 1](requirements/Requirement1.md).
|
|
158
200
|
|
|
159
201
|
## Preemptive downloading
|
|
160
202
|
|
|
@@ -301,7 +343,7 @@ This would allow developers to create "stylesheet" like capabilities.
|
|
|
301
343
|
|
|
302
344
|
## Applying properties with assignGingerly
|
|
303
345
|
|
|
304
|
-
For the common use case of setting properties on matching elements, MountObserver provides built-in support for the [assignGingerly](https://github.com/bahrus/assign-gingerly) library. This allows
|
|
346
|
+
For the common use case of setting properties on matching elements, MountObserver provides built-in support for the [assignGingerly](https://github.com/bahrus/assign-gingerly) library. This allows us to declaratively specify properties to apply to elements without writing custom mount callbacks:
|
|
305
347
|
|
|
306
348
|
```JavaScript
|
|
307
349
|
const observer = new MountObserver({
|
|
@@ -317,23 +359,29 @@ observer.observe(document);
|
|
|
317
359
|
|
|
318
360
|
This will automatically apply the specified properties to all matching input elements, both existing ones and those added dynamically.
|
|
319
361
|
|
|
362
|
+
[Implemented as [Requirement2](requirements/Requirement2.md)]
|
|
363
|
+
|
|
320
364
|
### Nested properties with dataset
|
|
321
365
|
|
|
322
|
-
The `assignGingerly` library supports nested property assignment using the `?.` notation. This is particularly useful for setting data attributes:
|
|
366
|
+
The `assignGingerly` library supports nested property assignment using the `?.` notation. This is particularly useful for setting data attributes and style:
|
|
323
367
|
|
|
324
368
|
```JavaScript
|
|
325
369
|
const observer = new MountObserver({
|
|
326
370
|
whereElementMatches: 'button',
|
|
327
371
|
assignGingerly: {
|
|
328
372
|
disabled: false,
|
|
329
|
-
'?.dataset
|
|
330
|
-
'?.dataset
|
|
373
|
+
'?.dataset?.action': 'submit',
|
|
374
|
+
'?.dataset?.trackingId': '12345',
|
|
375
|
+
'?.style': {
|
|
376
|
+
color: 'white',
|
|
377
|
+
height: '25px',
|
|
378
|
+
}
|
|
331
379
|
}
|
|
332
380
|
});
|
|
333
381
|
observer.observe(document);
|
|
334
382
|
```
|
|
335
383
|
|
|
336
|
-
The `?.` prefix tells assignGingerly to create nested properties if they don't exist. In this example, `?.dataset
|
|
384
|
+
The `?.` prefix tells assignGingerly to create nested properties if they don't exist. In this example, `?.dataset?.action` will set the `data-action` attribute on the button elements.
|
|
337
385
|
|
|
338
386
|
### Combining with imports
|
|
339
387
|
|
|
@@ -345,7 +393,7 @@ const observer = new MountObserver({
|
|
|
345
393
|
import: './my-element.js',
|
|
346
394
|
assignGingerly: {
|
|
347
395
|
theme: 'dark',
|
|
348
|
-
'?.dataset
|
|
396
|
+
'?.dataset?.initialized': 'true'
|
|
349
397
|
},
|
|
350
398
|
do: ({localName}, {modules}) => {
|
|
351
399
|
if(!customElements.get(localName)) {
|
|
@@ -367,6 +415,254 @@ Using `assignGingerly` provides several benefits:
|
|
|
367
415
|
3. **Declarative**: No need to write custom mount callbacks for simple property assignments
|
|
368
416
|
4. **Consistent**: The same property values are applied uniformly across all matching elements
|
|
369
417
|
|
|
418
|
+
### Dynamically updating assignGingerly configuration
|
|
419
|
+
|
|
420
|
+
The `MountObserver` class provides a public `assignGingerly()` method that allows you to merge new updates into the observer. This is useful for responding to user actions or application state changes:
|
|
421
|
+
|
|
422
|
+
```JavaScript
|
|
423
|
+
const observer = new MountObserver({
|
|
424
|
+
whereElementMatches: 'input',
|
|
425
|
+
assignGingerly: {
|
|
426
|
+
disabled: true,
|
|
427
|
+
value: 'Initial value'
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
observer.observe(document);
|
|
431
|
+
|
|
432
|
+
// Later, update the configuration
|
|
433
|
+
await observer.assignGingerly({
|
|
434
|
+
title: 'Updated tooltip',
|
|
435
|
+
placeholder: 'New placeholder'
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Key behaviors:**
|
|
440
|
+
|
|
441
|
+
1. **Merging**: New properties are merged with existing configuration. In the example above, future elements will receive all properties: `disabled`, `value`, `title`, and `placeholder`.
|
|
442
|
+
|
|
443
|
+
2. **Applies to existing elements**: The new properties are immediately applied to all currently mounted elements.
|
|
444
|
+
|
|
445
|
+
3. **Applies to future elements**: Future elements that mount will receive the merged configuration.
|
|
446
|
+
|
|
447
|
+
4. **Starting without initial config**: You can call the method even if no `assignGingerly` was specified in the constructor:
|
|
448
|
+
|
|
449
|
+
```JavaScript
|
|
450
|
+
const observer = new MountObserver({
|
|
451
|
+
whereElementMatches: 'input'
|
|
452
|
+
});
|
|
453
|
+
observer.observe(document);
|
|
454
|
+
|
|
455
|
+
// Set configuration later
|
|
456
|
+
await observer.assignGingerly({
|
|
457
|
+
disabled: true,
|
|
458
|
+
value: 'Set via method'
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
5. **Clearing configuration**: Pass `undefined` to clear the configuration for future elements (already-mounted elements keep their properties):
|
|
463
|
+
|
|
464
|
+
```JavaScript
|
|
465
|
+
await observer.assignGingerly(undefined);
|
|
466
|
+
// Future elements will not have properties applied
|
|
467
|
+
// Existing elements retain their current properties
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Method signature:**
|
|
471
|
+
```TypeScript
|
|
472
|
+
async assignGingerly(config: Record<string, any> | undefined): Promise<void>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The method is async because the assign-gingerly library is loaded dynamically when needed.
|
|
476
|
+
|
|
477
|
+
[Implemented as [Requirement9](requirements/Requirement9.md)]
|
|
478
|
+
|
|
479
|
+
## Emitting events from mounted elements
|
|
480
|
+
|
|
481
|
+
MountObserver can automatically dispatch custom events from elements when they mount. This is useful for:
|
|
482
|
+
|
|
483
|
+
1. **Signaling readiness**: Notify parent components or listeners that an element is ready
|
|
484
|
+
2. **Initialization events**: Trigger workflows when elements appear in the DOM
|
|
485
|
+
3. **Decoupled communication**: Allow elements to announce their presence without tight coupling
|
|
486
|
+
|
|
487
|
+
### Basic event emission
|
|
488
|
+
|
|
489
|
+
```JavaScript
|
|
490
|
+
const observer = new MountObserver({
|
|
491
|
+
whereElementMatches: 'button[data-action]',
|
|
492
|
+
mountedElemEmits: {
|
|
493
|
+
event: 'Event',
|
|
494
|
+
args: 'custom-ready'
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
observer.observe(document);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
This dispatches a `custom-ready` event from each matching button element when it mounts. Events bubble by default, so you can listen at the document level:
|
|
501
|
+
|
|
502
|
+
```JavaScript
|
|
503
|
+
document.addEventListener('custom-ready', (e) => {
|
|
504
|
+
console.log('Button ready:', e.target);
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Event constructors
|
|
509
|
+
|
|
510
|
+
You can specify any event constructor available in `globalThis`:
|
|
511
|
+
|
|
512
|
+
```JavaScript
|
|
513
|
+
mountedElemEmits: {
|
|
514
|
+
event: 'CustomEvent',
|
|
515
|
+
args: ['element-ready', { detail: { timestamp: Date.now() } }]
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Or pass a constructor directly:
|
|
520
|
+
|
|
521
|
+
```JavaScript
|
|
522
|
+
mountedElemEmits: {
|
|
523
|
+
event: CustomEvent,
|
|
524
|
+
args: ['element-ready', { detail: { timestamp: Date.now() } }]
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Magic string substitution
|
|
529
|
+
|
|
530
|
+
Use magic strings to inject dynamic values into event data:
|
|
531
|
+
|
|
532
|
+
- `{{mountedElement}}` - The element that just mounted
|
|
533
|
+
- `{{mountInit}}` - The MountInit configuration object
|
|
534
|
+
|
|
535
|
+
```JavaScript
|
|
536
|
+
const observer = new MountObserver({
|
|
537
|
+
whereElementMatches: 'button[data-test]',
|
|
538
|
+
mountedElemEmits: {
|
|
539
|
+
event: 'CustomEvent',
|
|
540
|
+
args: ['element-mounted', {
|
|
541
|
+
detail: {
|
|
542
|
+
element: '{{mountedElement}}',
|
|
543
|
+
config: '{{mountInit}}'
|
|
544
|
+
}
|
|
545
|
+
}]
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Magic strings work at any depth in nested objects and arrays:
|
|
551
|
+
|
|
552
|
+
```JavaScript
|
|
553
|
+
mountedElemEmits: {
|
|
554
|
+
event: 'CustomEvent',
|
|
555
|
+
args: ['data-ready', {
|
|
556
|
+
detail: {
|
|
557
|
+
nested: {
|
|
558
|
+
deep: {
|
|
559
|
+
element: '{{mountedElement}}'
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}]
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Multiple events
|
|
568
|
+
|
|
569
|
+
Emit multiple events in sequence by providing an array:
|
|
570
|
+
|
|
571
|
+
```JavaScript
|
|
572
|
+
const observer = new MountObserver({
|
|
573
|
+
whereElementMatches: 'my-component',
|
|
574
|
+
mountedElemEmits: [
|
|
575
|
+
{ event: 'Event', args: 'component-loading' },
|
|
576
|
+
{ event: 'Event', args: 'component-ready' },
|
|
577
|
+
{ event: 'CustomEvent', args: ['component-initialized', { detail: { version: '1.0' } }] }
|
|
578
|
+
]
|
|
579
|
+
});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Events are dispatched in the order specified.
|
|
583
|
+
|
|
584
|
+
### Event properties with eventProps
|
|
585
|
+
|
|
586
|
+
Apply additional properties to the event object using `eventProps`:
|
|
587
|
+
|
|
588
|
+
```JavaScript
|
|
589
|
+
mountedElemEmits: {
|
|
590
|
+
event: 'CustomEvent',
|
|
591
|
+
args: ['ready', { detail: {} }],
|
|
592
|
+
eventProps: {
|
|
593
|
+
timestamp: Date.now(),
|
|
594
|
+
source: 'mount-observer',
|
|
595
|
+
element: '{{mountedElement}}'
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Properties are applied using the [assignGingerly](https://github.com/bahrus/assign-gingerly) library, which supports nested property assignment with the `?.` notation.
|
|
601
|
+
|
|
602
|
+
### Fire once per element
|
|
603
|
+
|
|
604
|
+
Use `oncePerMountedElement` to ensure an event only fires the first time an element mounts:
|
|
605
|
+
|
|
606
|
+
```JavaScript
|
|
607
|
+
const observer = new MountObserver({
|
|
608
|
+
whereElementMatches: 'button[data-once]',
|
|
609
|
+
mountedElemEmits: {
|
|
610
|
+
event: 'Event',
|
|
611
|
+
args: 'initialized',
|
|
612
|
+
oncePerMountedElement: true
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
If the element is removed and re-added to the DOM, the event will not fire again. This is useful for initialization events that should only happen once per element instance.
|
|
618
|
+
|
|
619
|
+
### Performance considerations
|
|
620
|
+
|
|
621
|
+
The event emission logic is code-split into a separate module (`emitEvents.js`) that is only loaded when `mountedElemEmits` is configured. This keeps the core MountObserver lean for users who don't need this feature.
|
|
622
|
+
|
|
623
|
+
### Complete example
|
|
624
|
+
|
|
625
|
+
```JavaScript
|
|
626
|
+
const observer = new MountObserver({
|
|
627
|
+
whereElementMatches: 'my-widget',
|
|
628
|
+
import: './my-widget.js',
|
|
629
|
+
mountedElemEmits: [
|
|
630
|
+
{
|
|
631
|
+
event: 'CustomEvent',
|
|
632
|
+
args: ['widget-loading', {
|
|
633
|
+
detail: {
|
|
634
|
+
element: '{{mountedElement}}',
|
|
635
|
+
timestamp: Date.now()
|
|
636
|
+
}
|
|
637
|
+
}],
|
|
638
|
+
oncePerMountedElement: true
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
event: 'Event',
|
|
642
|
+
args: 'widget-ready'
|
|
643
|
+
}
|
|
644
|
+
],
|
|
645
|
+
do: ({localName}, {modules}) => {
|
|
646
|
+
if(!customElements.get(localName)) {
|
|
647
|
+
customElements.define(localName, modules[0].MyWidget);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Listen for events
|
|
653
|
+
document.addEventListener('widget-loading', (e) => {
|
|
654
|
+
console.log('Widget loading:', e.detail.element);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
document.addEventListener('widget-ready', (e) => {
|
|
658
|
+
console.log('Widget ready:', e.target);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
observer.observe(document);
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
[Implemented as [Requirement10](requirements/Requirement10.md)]
|
|
665
|
+
|
|
370
666
|
|
|
371
667
|
## Extra lazy loading
|
|
372
668
|
|
|
@@ -376,7 +672,7 @@ However, we could make the loading even more lazy by specifying intersection opt
|
|
|
376
672
|
|
|
377
673
|
```JavaScript
|
|
378
674
|
const observer = new MountObserver({
|
|
379
|
-
select: 'my-element',
|
|
675
|
+
select: 'my-element', //not supported by polyfill
|
|
380
676
|
whereElementIntersectsWith:{
|
|
381
677
|
rootMargin: "0px",
|
|
382
678
|
threshold: 1.0,
|
|
@@ -397,7 +693,7 @@ const observer = new MountObserver({
|
|
|
397
693
|
whereContainerHas: '[itemprop=isActive][value="true"]',
|
|
398
694
|
whereInstanceOf: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
|
|
399
695
|
whereLangIn: ['en-GB'],
|
|
400
|
-
|
|
696
|
+
whereConnectionHas:{
|
|
401
697
|
effectiveTypeIn: ["slow-2g"],
|
|
402
698
|
},
|
|
403
699
|
import: ['./my-element-small.css', {type: 'css'}],
|
|
@@ -421,9 +717,13 @@ const observer = new MountObserver({
|
|
|
421
717
|
});
|
|
422
718
|
```
|
|
423
719
|
|
|
720
|
+
[whereInstanceOf implemented as [Requirement5](requirements/Requirement5.md)]
|
|
721
|
+
|
|
722
|
+
[whereMediaMatches implemented as [Requirement6](requirements/Requirement6.md)]
|
|
723
|
+
|
|
424
724
|
## InstanceOf checks in detail
|
|
425
725
|
|
|
426
|
-
Carving out the special "whereInstanceOf" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "confirm" callback logic. For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
|
|
726
|
+
Carving out the special "whereInstanceOf" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "confirm" callback logic (discussed later). For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
|
|
427
727
|
|
|
428
728
|
The picture becomes murkier for custom elements. The best solution in that case seems to be to utilize customElements.getName(...) as a basis for the match, but at first glance, that could preclude being able to use base classes which a family of custom elements subclass, if that superclass isn't itself a custom element. I suppose the solution to this conundrum, when warranted, is simply to burden the developer with defining a custom element for the superclass, and thus assigning it a name, applicable within ShadowDOM scopes as needed, even though it isn't actually necessarily used for any live custom elements. This would require already having imported the base class, only benefitting from lazy loading the code needed for each sub class, which might not always be all that high as a percentage, compared to the base class.
|
|
429
729
|
|
|
@@ -473,6 +773,8 @@ observer.addEventListener('forget', e => {
|
|
|
473
773
|
});
|
|
474
774
|
```
|
|
475
775
|
|
|
776
|
+
[mount, dismount, disconnect] events implemented
|
|
777
|
+
|
|
476
778
|
## Explanation of all states / events
|
|
477
779
|
|
|
478
780
|
Normally, an element stays in its place in the DOM tree, but the conditions that the MountObserver instance is monitoring for can change for the element, based on modifications to the attributes of the element itself, or its custom state, or to other peer elements within the shadowRoot, if any, or window resizing, etc. As the element meets or doesn't meet all the conditions, the mountObserver will first call the corresponding mount/dismount callback, and then dispatch event "mount" or "dismount" according to whether the criteria are all met or not.
|
|
@@ -506,6 +808,8 @@ I'm on the fence on that one. I think the benefits either way to DX are so sma
|
|
|
506
808
|
|
|
507
809
|
## Dismounting
|
|
508
810
|
|
|
811
|
+
[TODO] This section is out of date
|
|
812
|
+
|
|
509
813
|
In many cases, it will be critical to inform the developer **why** the element no longer satisfies all the criteria. For example, we may be using an intersection observer, and when we've scrolled away from view, we can "shut down" until the element is (nearly) scrolled back into view. We may also be displaying things differently depending on the network speed. How we should respond when one of the original conditions, but not the other, no longer applies, is of paramount importance.
|
|
510
814
|
|
|
511
815
|
So the dismount event should provide a "checklist" of all the conditions, and their current value:
|
|
@@ -538,6 +842,8 @@ So I believe the prudent thing to do is wait for all the conditions to be satisf
|
|
|
538
842
|
|
|
539
843
|
The alternative to providing this feature, which I'm leaning towards, is to just ask the developer to create "specialized" mountObserver construction arguments, that turn on and off precisely when the developer needs to know.
|
|
540
844
|
|
|
845
|
+
[Implemented with [Requirement6](requirements/Requirement6.md)]
|
|
846
|
+
|
|
541
847
|
|
|
542
848
|
## Support for "donut hole scoping"
|
|
543
849
|
|
|
@@ -550,14 +856,19 @@ For the polyfill, we need to support it as follows:
|
|
|
550
856
|
```html
|
|
551
857
|
<div id=myTest itemscope>
|
|
552
858
|
<span itemprop=name>
|
|
859
|
+
<div itemscope>
|
|
860
|
+
<data itemprop=ssn>
|
|
861
|
+
</div>
|
|
553
862
|
</div>
|
|
554
863
|
```
|
|
555
864
|
|
|
865
|
+
We want to find all elements with attribute itemprop outside any itemscope, so the span and not the data element.
|
|
866
|
+
|
|
556
867
|
```JavaScript
|
|
557
|
-
const
|
|
868
|
+
const oContainerNode = document.getElementById('myTest');
|
|
558
869
|
const observer = new MountObserver({
|
|
559
|
-
|
|
560
|
-
|
|
870
|
+
whereElementMatches:'[itemprop]',
|
|
871
|
+
whereOutside: '[itemscope]'
|
|
561
872
|
do: {
|
|
562
873
|
mount: ({localName}, {modules, observer}) => {
|
|
563
874
|
...
|
|
@@ -565,21 +876,30 @@ const observer = new MountObserver({
|
|
|
565
876
|
},
|
|
566
877
|
disconnectedSignal: new AbortController().signal
|
|
567
878
|
});
|
|
568
|
-
observer.observe(
|
|
879
|
+
observer.observe(oContainerNode);
|
|
569
880
|
```
|
|
570
881
|
|
|
571
|
-
The check for "
|
|
882
|
+
The check for "whereOutside" is done via script:
|
|
572
883
|
|
|
573
884
|
```JavaScript
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
885
|
+
import {whereOutside} from 'mount-observer/whereOutside.js';
|
|
886
|
+
whereOutside(oContainerNode: Node, matchCandidate: Element, outside: string){
|
|
887
|
+
let current = matchCandidate.parentElement;
|
|
888
|
+
|
|
889
|
+
while (current && current !== oContainerNode) {
|
|
890
|
+
if (current.matches(outside)) {
|
|
891
|
+
return false; // Found an excluding ancestor
|
|
892
|
+
}
|
|
893
|
+
current = current.parentElement;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return true; // No excluding ancestors found
|
|
580
897
|
}
|
|
898
|
+
|
|
581
899
|
```
|
|
582
900
|
|
|
901
|
+
[Implemented as [Requirement7](requirements/Requirement7.md)]
|
|
902
|
+
|
|
583
903
|
## A tribute to attributes
|
|
584
904
|
|
|
585
905
|
Attributes of DOM elements are tricky. They've been around since the get-go of the Web, and they've survived multiple eras of web development, where different philosophies have prevailed, so prepare yourself for some esoteric discussions in what follows.
|
package/attrChanges.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks for attribute changes on a mounted element.
|
|
3
|
+
* This module is dynamically loaded only when whereAttr is configured.
|
|
4
|
+
*/
|
|
5
|
+
export function checkAttrChanges(element, mountInit, buildAttrCoordinateMapFn, elementAttrStates, elementOnceAttrs) {
|
|
6
|
+
if (!mountInit.whereAttr || !buildAttrCoordinateMapFn) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const isCustomElement = element.tagName.toLowerCase().includes('-');
|
|
10
|
+
const attrCoordMap = buildAttrCoordinateMapFn(mountInit.whereAttr, isCustomElement);
|
|
11
|
+
// Get or create the attribute state for this element
|
|
12
|
+
let attrState = elementAttrStates.get(element);
|
|
13
|
+
if (!attrState) {
|
|
14
|
+
attrState = new Map();
|
|
15
|
+
elementAttrStates.set(element, attrState);
|
|
16
|
+
}
|
|
17
|
+
const changes = [];
|
|
18
|
+
const currentAttrs = new Set();
|
|
19
|
+
// Check all possible attributes from the coordinate map
|
|
20
|
+
for (const attrName of Object.keys(attrCoordMap)) {
|
|
21
|
+
const coordinate = attrCoordMap[attrName];
|
|
22
|
+
const currentValue = element.getAttribute(attrName);
|
|
23
|
+
const previousValue = attrState.get(attrName);
|
|
24
|
+
if (currentValue !== null) {
|
|
25
|
+
currentAttrs.add(attrName);
|
|
26
|
+
}
|
|
27
|
+
// Check if this attribute has "once: true" in its map entry
|
|
28
|
+
const mapEntry = mountInit.map?.[coordinate] || null;
|
|
29
|
+
const isOnce = mapEntry?.once === true;
|
|
30
|
+
// If "once" is true, check if we've already seen this attribute
|
|
31
|
+
if (isOnce) {
|
|
32
|
+
let onceAttrs = elementOnceAttrs.get(element);
|
|
33
|
+
if (!onceAttrs) {
|
|
34
|
+
onceAttrs = new Set();
|
|
35
|
+
elementOnceAttrs.set(element, onceAttrs);
|
|
36
|
+
}
|
|
37
|
+
// If we've already seen this attribute, skip it
|
|
38
|
+
if (onceAttrs.has(attrName)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Mark this attribute as seen if it currently has a value
|
|
42
|
+
if (currentValue !== null) {
|
|
43
|
+
onceAttrs.add(attrName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Include if: currently has value OR previously had value but now removed
|
|
47
|
+
if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
|
|
48
|
+
// Check if value changed
|
|
49
|
+
if (currentValue !== previousValue) {
|
|
50
|
+
const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
|
|
51
|
+
changes.push({
|
|
52
|
+
value: currentValue,
|
|
53
|
+
attrNode,
|
|
54
|
+
mapEntry,
|
|
55
|
+
attrName,
|
|
56
|
+
coordinate,
|
|
57
|
+
element
|
|
58
|
+
});
|
|
59
|
+
// Update state
|
|
60
|
+
if (currentValue !== null) {
|
|
61
|
+
attrState.set(attrName, currentValue);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
attrState.delete(attrName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return changes;
|
|
70
|
+
}
|
package/attrChanges.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { AttrChange, MountInit } from './types.d.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks for attribute changes on a mounted element.
|
|
5
|
+
* This module is dynamically loaded only when whereAttr is configured.
|
|
6
|
+
*/
|
|
7
|
+
export function checkAttrChanges(
|
|
8
|
+
element: Element,
|
|
9
|
+
mountInit: MountInit,
|
|
10
|
+
buildAttrCoordinateMapFn: (whereAttr: any, isCustomElement: boolean) => any,
|
|
11
|
+
elementAttrStates: WeakMap<Element, Map<string, string | null>>,
|
|
12
|
+
elementOnceAttrs: WeakMap<Element, Set<string>>
|
|
13
|
+
): AttrChange[] {
|
|
14
|
+
if (!mountInit.whereAttr || !buildAttrCoordinateMapFn) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const isCustomElement = element.tagName.toLowerCase().includes('-');
|
|
19
|
+
const attrCoordMap = buildAttrCoordinateMapFn(mountInit.whereAttr, isCustomElement);
|
|
20
|
+
|
|
21
|
+
// Get or create the attribute state for this element
|
|
22
|
+
let attrState = elementAttrStates.get(element);
|
|
23
|
+
if (!attrState) {
|
|
24
|
+
attrState = new Map<string, string | null>();
|
|
25
|
+
elementAttrStates.set(element, attrState);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const changes: AttrChange[] = [];
|
|
29
|
+
const currentAttrs = new Set<string>();
|
|
30
|
+
|
|
31
|
+
// Check all possible attributes from the coordinate map
|
|
32
|
+
for (const attrName of Object.keys(attrCoordMap)) {
|
|
33
|
+
const coordinate = attrCoordMap[attrName];
|
|
34
|
+
const currentValue = element.getAttribute(attrName);
|
|
35
|
+
const previousValue = attrState.get(attrName);
|
|
36
|
+
|
|
37
|
+
if (currentValue !== null) {
|
|
38
|
+
currentAttrs.add(attrName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if this attribute has "once: true" in its map entry
|
|
42
|
+
const mapEntry = mountInit.map?.[coordinate] || null;
|
|
43
|
+
const isOnce = mapEntry?.once === true;
|
|
44
|
+
|
|
45
|
+
// If "once" is true, check if we've already seen this attribute
|
|
46
|
+
if (isOnce) {
|
|
47
|
+
let onceAttrs = elementOnceAttrs.get(element);
|
|
48
|
+
if (!onceAttrs) {
|
|
49
|
+
onceAttrs = new Set<string>();
|
|
50
|
+
elementOnceAttrs.set(element, onceAttrs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If we've already seen this attribute, skip it
|
|
54
|
+
if (onceAttrs.has(attrName)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Mark this attribute as seen if it currently has a value
|
|
59
|
+
if (currentValue !== null) {
|
|
60
|
+
onceAttrs.add(attrName);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Include if: currently has value OR previously had value but now removed
|
|
65
|
+
if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
|
|
66
|
+
// Check if value changed
|
|
67
|
+
if (currentValue !== previousValue) {
|
|
68
|
+
const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
|
|
69
|
+
|
|
70
|
+
changes.push({
|
|
71
|
+
value: currentValue,
|
|
72
|
+
attrNode,
|
|
73
|
+
mapEntry,
|
|
74
|
+
attrName,
|
|
75
|
+
coordinate,
|
|
76
|
+
element
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Update state
|
|
80
|
+
if (currentValue !== null) {
|
|
81
|
+
attrState.set(attrName, currentValue);
|
|
82
|
+
} else {
|
|
83
|
+
attrState.delete(attrName);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return changes;
|
|
90
|
+
}
|