Skip to content

Chaining Javascript Style Calls with Proxies

August 27, 2022

7 min read

479 views

Mixed feelings about jQuery aside, it's hard to argue against the ease it brings to some tasks compared with vanilla JavaScript. One such feature is its ability to chain method calls, meaning instead of referencing a DOM element repeatedly, you can keep chaining instructions without interruption.

Here's a concrete example: if you're looking to adjust the color and width of .someClass and want a fade effect:

$('.someClass') .css('color', '#fafafa') .css('width', '100%') .fadeIn();

Doing this in plain JavaScript would require a bit more code and doesn't feel quite as smooth:

const someObject = document.querySelector('.someClass'); someObject.style.color = '#fafafa'; someObject.style.width = '100%'; someObject.style.transition = 'opacity 250ms ease'; someObject.style.opacity = '1.0';

But, as an example in using JavaScript Proxies, we can make the process cleaner and allow chained calls to element.style:

style('.someObject') .color('#fafafa') .width('100%') .transition('opacity 250ms ease') .opacity('1.0');

What are Proxies?

In JavaScript, a Proxy is an object that sits between a target object and the external code trying to interact with it. When you create a Proxy, you specify what operations (like gets or sets) it should intercept. The Proxy then intercepts any of those operations made to the target object.

Here's a very simple example which intercepts all gets:

const target = {}; const handler = { get: (target, name) { return `Hello, ${name}!`; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // => "Hello, name!" console.log(proxy.otherName); // => "Hello, otherName!"

The proxy is acting as an intermediary for the target; the handler defining its behaviour, in this case, overriding the getter for any property and returning a string containing the property name.

Method

Given this foundation, we can set up a Proxy to allow chained style changes, much like what jQuery offers:

  1. Get the element with a given selector.
  2. Use a Proxy to watch all interactions.
  3. Update the value of any accessed property.
  4. Return the Proxy for further chaining.

By doing this, you can keep chaining methods without needing a temporary variable to hold the element. It remains in memory.

Since this particular method involves repurposing the getter to act as a setter, we can keep its functionality by returning the value of a specified property if no additional arguments are passed (also similar to how jQuery functions). For example, you can check the value of backgroundColor with style('.someDomObject').backgroundColor().

const proxy = { get: (obj, prop) => { return (value) => { if (value) { obj[prop] = value; return new Proxy(obj, proxy); } return obj[prop]; } } }; export const style = (selector) => { const element = document.querySelector(selector); return element ? new Proxy(element.style, proxy) : undefined; };

And that's it! We can now import this style function and chain it for changing DOM element styles.

More Use-cases

Proxies offer a lot more flexibility and functionality beyond just chaining style changes:

  • Logging: By monitoring method calls on objects, Proxies allow keeping closer track of details. This includes identifying which methods are being called, understanding the arguments being passed, and analyzing return values. This capability can be immensely powerful when debugging, as it gives a more transparent view of interactions.
  • Data Checks: Data integrity is crucial in many applications. With Proxies, you can set up barriers to validate data inputs, ensuring they meet criteria before being applied to the target.
  • Control and Storage: Proxies allow for a fine level of control over object interactions. By using tools like WeakMap alongside Proxies, you can manage data changes in a more efficient manner - This could mean buffering changes and then applying them all at once, leading to performance benefits in specific scenarios, or allowing rollbacks of changes.

Potential Issues

While Proxies bring many advantages to the table, it's good to be aware of their limitations:

  1. Speed: Every layer of abstraction has a cost, and Proxies are no exception. Intensive use of Proxies, especially in performance-critical scenarios, can introduce noticeable overhead.
  2. Learning Curve: For those unfamiliar with the concept, understanding and working with Proxies can present a challenge. They introduce a level of indirection that isn't intuitive.
  3. Type Issues: When working with TypeScript, altering the behaviour of the underlying object with Proxies can lead to type inconsistencies. TypeScript relies on static types, and if a Proxy modifies an object's behaviour in a way that isn't reflected in its type definitions, it can be a pain to deal with.
  4. Issues with Inheritance: Proxies can be tricky when dealing with inherited properties. They may not always behave as expected, especially if the properties are deep within the prototype chain.

More resources