Kaynağa Gözat

feat: observeElementProperty() to observe element's JS object properties

Sv443 1 yıl önce
ebeveyn
işleme
885323d
4 değiştirilmiş dosya ile 103 ekleme ve 0 silme
  1. 5 0
      .changeset/famous-houses-clap.md
  2. 1 0
      README-summary.md
  3. 54 0
      README.md
  4. 43 0
      lib/dom.ts

+ 5 - 0
.changeset/famous-houses-clap.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": minor
+---
+
+Added function observeElementProperty to allow observing element property changes

+ 1 - 0
README-summary.md

@@ -31,6 +31,7 @@ Or view the documentation of previous major releases: [3.0.0](https://github.com
     - [interceptEvent()](https://github.com/Sv443-Network/UserUtils#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
     - [interceptWindowEvent()](https://github.com/Sv443-Network/UserUtils#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
     - [isScrollable()](https://github.com/Sv443-Network/UserUtils#isscrollable) - check if an element has a horizontal or vertical scroll bar
+    - [observeElementProperty()](https://github.com/Sv443-Network/UserUtils#observeelementproperty) - observe changes to an element's property that can't be observed with MutationObserver
 - Math:
     - [clamp()](https://github.com/Sv443-Network/UserUtils#clamp) - constrain a number between a min and max value
     - [mapRange()](https://github.com/Sv443-Network/UserUtils#maprange) - map a number from one range to the same spot in another range

+ 54 - 0
README.md

@@ -33,6 +33,7 @@ View the documentation of previous major releases: [3.0.0](https://github.com/Sv
     - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
     - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
     - [isScrollable()](#isscrollable) - check if an element has a horizontal or vertical scroll bar
+    - [observeElementProperty()](#observeelementproperty) - observe changes to an element's property that can't be observed with MutationObserver
   - [**Math:**](#math)
     - [clamp()](#clamp) - constrain a number between a min and max value
     - [mapRange()](#maprange) - map a number from one range to the same spot in another range
@@ -675,6 +676,59 @@ console.log("Element has a vertical scroll bar:", vertical);
 
 </details>
 
+<br>
+
+### observeElementProperty()
+Usage:  
+```ts
+observeElementProperty(
+  element: Element,
+  property: string,
+  callback: (oldValue: any, newValue: any) => void
+): void
+```
+  
+Observes changes to an element's property.  
+While regular attributes can be observed using a MutationObserver, this is not always possible for properties that are changed through setter functions and assignment.  
+This function shims the setter of the provided property and calls the callback function whenever it is changed through any means.  
+  
+When using TypeScript, the types for `element`, `property` and the arguments for `callback` will be automatically inferred.
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+import { observeElementProperty } from "@sv443-network/userutils";
+
+const myInput = document.querySelector("input#my-input");
+
+let value = 0;
+
+setInterval(() => {
+  value += 1;
+  myInput.value = String(value);
+}, 1000);
+
+
+const observer = new MutationObserver((mutations) => {
+  // will never be called:
+  console.log("MutationObserver mutation:", mutations);
+});
+
+// one would think this should work, but "value" is a JS object *property*, not a DOM *attribute*
+observer.observe(myInput, {
+  attributes: true,
+  attributeFilter: ["value"],
+});
+
+
+observeElementProperty(myInput, "value", (oldValue, newValue) => {
+  // will be called every time the value changes:
+  console.log("Value changed from", oldValue, "to", newValue);
+});
+```
+
+</details>
+
 <br><br>
 
 <!-- #SECTION Math -->

+ 43 - 0
lib/dom.ts

@@ -140,3 +140,46 @@ export function isScrollable(element: Element) {
     horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth,
   };
 }
+
+/**
+ * Executes the callback when the passed element's property changes.  
+ * Contrary to an element's attributes, properties can usually not be observed with a MutationObserver.  
+ * This function shims the getter and setter of the property to invoke the callback.  
+ *   
+ * [Source](https://stackoverflow.com/a/61975440)
+ * @param property The name of the property to observe
+ * @param callback Callback to execute when the value is changed
+ */
+export function observeElementProperty<
+  TElem extends Element = HTMLElement,
+  TProp extends keyof TElem = keyof TElem,
+>(
+  element: TElem,
+  property: TProp,
+  callback: (oldVal: TElem[TProp], newVal: TElem[TProp]) => void
+) {
+  const elementPrototype = Object.getPrototypeOf(element);
+  // eslint-disable-next-line no-prototype-builtins
+  if(elementPrototype.hasOwnProperty(property)) {
+    const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
+    Object.defineProperty(element, property, {
+      get: function() {
+        // @ts-ignore
+        // eslint-disable-next-line prefer-rest-params
+        return descriptor?.get?.apply(this, arguments);
+      },
+      set: function() {
+        const oldValue = this[property];
+        // @ts-ignore
+        // eslint-disable-next-line prefer-rest-params
+        descriptor?.set?.apply(this, arguments);
+        const newValue = this[property];
+        if(typeof callback === "function") {
+          // @ts-ignore
+          callback.bind(this, oldValue, newValue);
+        }
+        return newValue;
+      }
+    });
+  }
+}