Programmatically assigning a new value to an <input> element bypasses the native change event listener. This occurs because the browser's implementation of onchange enforces strict activation criteria. Specifically, the event only fires when three conditions are met sequentially: the element receives focus, its content is modified, and it subsequently loses focus. Crucially, this sequence must originate from direct user interaction. Methods like focus(), direct property assigment, and blur() executed via JavaScript do not satisfy the user-interaction requirement, rendering the change listener inactive.
While continuous polling using setInterval can detect value mutations, it introduces performance overhead and latency. A more efficient approach involves intercepting the property assignment itself by modifying the prototype definition.
To understand how to intercept the assignment, we first examine how the value property is defined on the input prototype:
const field = document.querySelector('input[type="text"]');
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(field), 'value');
console.log(descriptor);
The output reveals that value is not an own property but a getter/setter pair defined on HTMLInputElement.prototype. Since it is configurable, we can overide the setter to inject custom logic before delegating to the original implementation.
const targetElement = document.querySelector('input[type="text"]');
const prototype = Object.getPrototypeOf(targetElement);
const originalDescriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
const baseSetter = originalDescriptor.set;
const baseGetter = originalDescriptor.get;
Object.defineProperty(prototype, 'value', {
configurable: true,
enumerable: true,
get() {
return baseGetter.call(this);
},
set(newValue) {
const previousValue = this.value;
baseSetter.call(this, newValue);
if (previousValue !== newValue) {
this.dispatchEvent(new CustomEvent('valueMutated', {
detail: { oldValue: previousValue, newValue },
bubbles: true
}));
}
}
});
This implementation retrieves the oriignal accessor functions, redefines the property on the prototype, and ensures the native behavior remains intact. Inside the new setter, the script captures the prior state, applies the incoming value via the original setter, and then emits a custom valueMutated event if a modification actually occurred. Listeners can now attach to this custom event to react to programmatic updates without relying on browser-driven focus/blur cycles.
The technique mirrors how reactive frameworks intercept method calls to trigger DOM updates. By leveraging JavaScript's prototype chain and property descriptors, developers can establish a reliable notification mechanism for any programmatic value assignment.