Data Binding in Vanilla JavaScript
2025-01-04 · edited 2026-08-31
Since I am using vanilla JavaScript for most of my work, I have stopped using frameworks like Vue.js.
I am quite content working this way. However, the one feature I miss very much when using vanilla JS is data binding via an HTML attribute e.g. data-colour="<value>" which updates automatically updates as well when element.colour = "<new value>" is set.
So I usually implement a very primitive version of data binding myself with the handy Object.defineProperty() on a DOM node.
const element = document.querySelector('.abcdef')
Object.defineProperty(element, "abcdef", {
set(value) {
element.setAttribute("data-abcdef", value)
},
get() {
return element.getAttribute("data-abcdef");
}
})
element.abcdef = 'xyz'
That’s really the essence of it.
I usually just create a function that does some extra housekeeping in handling the data type of attributes.
function defineReactiveAttribute({ element, propertyName, attributeName, setter, getter, integerValue = false, booleanValue = false }) {
Object.defineProperty(element, propertyName, {
set(value) {
if (setter) setter(value)
else element.setAttribute(attributeName, value)
},
get() {
if (getter) return getter()
else return integerValue ? parseInt(element.getAttribute(attributeName)) : booleanValue ? (element.getAttribute(attributeName) == 'true') : element.getAttribute(attributeName)
}
})
}
This is then used as such …
… for strings
defineReactiveAttribute({ element: mark, propertyName: "id", attributeName: "data-id" })
mark.id = 'u-123'
… for booleans
defineReactiveAttribute({ element: mark, propertyName: "visible", attributeName: "data-visible", booleanValue: true })
mark.visible = true
mark.visible = false
… for integers
defineReactiveAttribute({ element: mark, propertyName: "finding", attributeName: "data-finding", integerValue: true })
mark.finding = 12345
… for JSON data
defineReactiveAttribute({ element: mark, propertyName: "votes", attributeName: "data-votes",
setter(value) {
mark.setAttribute("data-votes", JSON.stringify(value));
},
getter() { return JSON.parse(mark.getAttribute("data-votes")); }
});
mark.votes = [{ vote: 1 }, { vote: 2 }]
Isn’t this beatiful? No more element.setAttribute() all over the place!
Bonus:
Sometimes a want to directly mirror a property to a CSS variable. In this case setting element.translationX would set the --translation-x variable and vice versa.
Object.defineProperty(element, 'translationX', {
set(value) {
element.style.setProperty('--translation-x', `${value}px`);
},
get() {
return parseFloat(getComputedStyle(element).getPropertyValue('--translation-x'));
}
}
);