0

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?

Tom
  • 11
  • First of all, have you made sure you're not solving [an XY problem](https://meta.stackexchange.com/questions/346503/what-is-the-opposite-of-the-xy-problem)? – VLAZ Apr 27 '21 at 18:40
  • Ah, thank you for that, I'll provide more context. The root of the issue is that I want to cache the scrollTop of this element and not unnecessarily read from it directly to prevent unnecessary DOM thrash. I have already a kind of adapter class with a public ScrollTop property that does some extra work, but the element itself is also used in external libraries that touch scrollTop directly, so any attempt to cache the value in the aformentioned class is useless. I want to try pushing the caching logic down into the element itself. – Tom Apr 27 '21 at 18:56

1 Answers1

0

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.

Adam Pietrasiak
  • 12,773
  • 9
  • 78
  • 91