'Adapting the HTMLElement class to override specific properties
I want to override the functionality of an HTMLElement so that the scrollTop property, on get or set, will do some extra logic before touching the actual value. The only way, as far as I have found, is to delete the original property and then use Object.defineProperty():
delete element.scrollTop;
Object.defineProperty(element, "scrollTop", {
get: function() : number {
...
},
set: function(value: number) {
...
}
});
However, this removes all access to the original scrollTop, so the new logic can't do something like return base.scrollTop. The comments on this older, similar question claim that getting access to the original value is not possible when overriding with Object.defineProperty().
I'm wondering if a possible alternative is to create an adapter class that implements the HTMLElement interface and wraps the HTMLElement in question. All implemented properties delegate to the wrapped element's properties, but its scrollTop would do the extra work I need.
I'm quite new to Typescript, but is the alternative possible? If so, is there a lightweight way of defining all other properties on the adapter that we're not touching to automatically delegate to the wrapped element?
Solution 1:[1]
Here is my solution to the problem (which is a bit more tricky than might seem)
I implemented some utility for that and usage look like:
Let's say we want to replace some div 'scrollTop' method to return 50 if scrollTop is bigger than 50 and return original value if it is less than 50:
const div = document.createElement("div")
replaceOriginalPropertyDescriptor(div, "scrollTop", (originalDescriptor, div) => {
return {
// Keep rest of the descriptor (like setter) original
...originalDescriptor,
get() {
const originalScrollTop = originalDescriptor.get!.apply(div);
if (originalScrollTop > 50) return 50;
return originalScrollTop
},
}
})
Here is implementation with bunch of comments:
/**
* Will replace property descriptor of target while allowing us to access original one.
*
* Example:
*
* Will replace div.scrollTop to return 50, if original scrollTop is bigger than 50. Otherwise returns original scrollTop
*
* const div = document.createElement("div")
*
* replaceOriginalPropertyDescriptor(div, "scrollTop", (originalDescriptor, div) => {
* return {
* get() {
* const originalScrollTop = originalDescriptor.get.apply(div);
*
* if (originalScrollTop > 50) return 50;
*
* return originalScrollTop
* },
* // Keep rest of the descriptor (like setter) original
* ...originalDescriptor,
* }
* })
*/
function replaceOriginalPropertyDescriptor<T extends object>(
input: T,
property: keyof T,
newDescriptorCreator: (originalDescriptor: PropertyDescriptor, target: T) => PropertyDescriptor
) {
const originalDescriptor = findPropertyDescriptor(input, property);
if (!originalDescriptor) {
throw new Error(`Cannot replace original descriptor ${String(property)}. Target has no such property`);
}
const newDescriptor = newDescriptorCreator(originalDescriptor, input);
Reflect.defineProperty(input, property, newDescriptor);
}
What was my use case:
I am using drag and drop library that is reading scroll positions like 500 times a second. I wanted to cache this value for lifetime of a single frame. As I cannot control source code of the library itself, I am kinda injecting this cache to HTMLElements itself so they keep previous scroll position values for 1 frame.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Adam Pietrasiak |
