#development

Data Binding in Vanilla JavaScript

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'));
   }
 }
);