Making Your HTML Inputs Reactive, Because We Can

When dealing with HTML <input> in JavaScript, we usually want to handle one use case: user I/O via the input, change, and other similar events.

This can easily be done with the following code:

// Suppose we have an <input type="text"> element

const inputElem = document.querySelector('input');

inputElem.addEventListener('input', function(event) {
  // Do something on user I/O
});

But what happens when the input is being updated by other JavaScript code we have little control over? Perhaps via some legacy JS library with sparse or non-existent documentation? Or some two-way data bindings with complicated spaghetti logic all over the application?

Then it might be time to go directly to the source: the value property on the HTMLInputElement itself.

value is a simple property that can be used via JavaScript to get and set the value of the HTML input element. It represents the DOM interface for JavaScript I/O to and from the <input>.

Since it is easy to use and well-documented we don’t just have to stop at getting the value, we can make it reactive using the Object.defineProperty() API with just a few lines of code:

const rxDom = (elem, propertyName) => {
  let innerValue = elem[propertyName], subscribers = [];
  
  Object.defineProperty(elem, propertyName, {
    get() {
      return innerValue;
    },
    set(value) {
      const newValue = value, oldValue = innerValue;
      innerValue = newValue;

      if (newValue === oldValue) return;
      subscribers.forEach(s => s(newValue, oldValue));
    },
    configurable: true,
    enumerable: true
  });
  
  const subscribe = callback => {
    subscribers.push(callback);
  };
  
  const unsubscribe = callback => {
    const index = subscribers.indexOf(callback);
    if (index < 0) return;
    subscribers.splice(index, 1);
  };
  
  return {
    elem,
    subscribe,
    unsubscribe
  };
};

I call this bit of code rxDom and what it does is simple: it adds a getter and a setter pair to a HTML element and allows us to react to changes on the value (or any) property as they happen in real-time.

We can pass in a function as a callback via the subscribe method that has access to the newValue and the oldValue and can execute code when they change.

let rxInput = rxDom(inputElem, 'value');

function handleChange(newValue, oldValue) {
  console.log('value has changed to:', newValue);
}

rxInput.subscribe(handleChange);

// Change the value of the input element via JS
inputElem.value = 'Hello New World!';

Running this code will result in the following output to the console:

value has changed to: Hello New World!

Pretty easy, right?

We can also unsubscribe in order to stop reacting to changes or when we are done doing one particular thing:

rxInput.unsubscribe(handleChange);

Interestingly enough, you can only define one pair of getters and setters for a DOM element. So using rxDom multiple times on a single element will only work with the last pair that was defined for the property.

Here’s a short example with Object.defineProperty() to illustrate:

const test = { a: 1 };

let innerVal = test.a;

Object.defineProperty(test, 'a', {
  get() {
    return innerVal;
  },
  set(value) {
    console.log('Setter 1:', value);
    innerVal = value;
  }
});

Object.defineProperty(test, 'a', {
  get() {
    return innerVal;
  },
  set(value) {
    console.log('Setter 2:', value);
    innerVal = value;
  }
});

Object.defineProperty(test, 'a', {
  get() {
    return innerVal;
  },
  set(value) {
    console.log('Setter 3:', value);
    innerVal = value;
  }
});

test.a = 2;
test.a = 3;
test.a = 4;
test.a = 5;

Running this code will result in the following output to the console:

Setter 3: 2
Setter 3: 3
Setter 3: 4
Setter 3: 5

We can observe that only the last pair of getters and setters are called when we change the test.a object property.


Besides using rxDom as a fun little proof-of-concept and for exploring JavaScript property descriptors, I wouldn’t really recommend using it due to how it mutates DOM elements, introduces side-effects into your code, and makes it harder to keep track of how things are changing.

If you have to resort to this type of pattern, it’s likely that there is a better solution or it might be time to seriously refactor your code.

…Your Scientists JavaScript Developers Were So Preoccupied With Whether Or Not They Could, They Didn’t Stop To Think If They Should.

Ian Malcolm

Just because we can do it, doesn’t mean we should.

2 thoughts on “Making Your HTML Inputs Reactive, Because We Can

  1. You can do it without mutating the DOM element using Proxy (doesn’t work in IE).

    const rxDom = (element, propertyName) => {
    const subscribers = [];

    const proxy = new Proxy(element, {
    get(obj, prop) {
    return obj[prop];
    },
    set(obj, prop, value) {
    if (prop === propertyName) {
    const oldValue = obj[prop];
    if (value === oldValue) return true;
    subscribers.forEach(s => s(value, oldValue));
    }
    obj[prop] = value;
    return true;
    }
    });

    const subscribe = (callback) => {
    subscribers.push(callback);
    };

    const unsubscribe = (callback) => {
    const index = subscribers.indexOf(callback);
    if (index console.log(`Changed from ${oldVal} to ${newVal}`);

    rxInput.subscribe(handleChange);
    console.log(rxInput.element.value);
    rxInput.element.value = “world”;
    console.log(rxInput.element.value);

    Like

    1. Yes, absolutely. Proxies are very powerful and make dealing with reactivity in JavaScript much easier.

      One interesting detail is that they don’t mutate the target object, which is great for writing cleaner code. In this example, however, Object.defineProperty() can be used for reacting to changes that are already taking place vs. introducing a new Proxy object and re-writing existing code to support it.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s