Skip to content

Commit

Permalink
feat: observeElementProperty() to observe element's JS object properties
Browse files Browse the repository at this point in the history
  • Loading branch information
Sv443 committed Jan 5, 2024
1 parent fa51d70 commit 885323d
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-houses-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sv443-network/userutils": minor
---

Added function observeElementProperty to allow observing element property changes
1 change: 1 addition & 0 deletions README-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -->
Expand Down
43 changes: 43 additions & 0 deletions lib/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
}
}

0 comments on commit 885323d

Please sign in to comment.