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/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 "on" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support.
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 you to declaratively specify properties to apply to elements without writing custom mount callbacks:
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.action': 'submit',
330
- '?.dataset.trackingId': '12345'
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.action` will set the `data-action` attribute on the button elements.
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.initialized': 'true'
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
- whereConnectiselect:{
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 oElement = document.getElementById('myTest');
868
+ const oContainerNode = document.getElementById('myTest');
558
869
  const observer = new MountObserver({
559
- select:'[itemprop]',
560
- outside: '[itemscope]'
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(oElement);
879
+ observer.observe(oContainerNode);
569
880
  ```
570
881
 
571
- The check for "outside" is done via script:
882
+ The check for "whereOutside" is done via script:
572
883
 
573
884
  ```JavaScript
574
- outsideCheck(oElement: Element, matchCandidate: Element, outside: string){
575
- const elementsToExclude = Array.from(oElement.querySelectorAll(outside));
576
- for(const elementToExclude of elementsToExclude){
577
- if(elementToExclude === matchCandidate || elementToExclude.contains(matchCandidate)) return false;
578
- }
579
- return true;
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
+ }