DOMClass.bindings Mixin VS Performance

If you remember my old “The DOM Is NOT Slow, Your Abstraction Is“, and you are aware about DOMClass potentials, you’d be happy to learn about its new “Best Bindings In Town“ mixin!

DOMClass DBMonster

Simpler than standards without perf-strings attached!

Tested against still sold but not modern at all devices, I’ve managed to reproduce the (in)famous DBMonster benchmark, this time basing it entirely on DOMClass abstraction, managing to create 1201 components per each instance of DBMonster, handling 2400 bindings per instnace plus 1000 bound callbacks too that will react to bindings.

This is not “jsut one class“, this is a component that uses sub related components to perform as close as possible to a pure DOM based version of the same benchmark (version I wrote myself a while ago).

So here the DOMClass based DBMonster benchmark component, which is basically less amount of cleaner and better organized lines of code proposed in the initial pure DOM comparison.

OK but … what about bindings?

Being based on es-class, the dom-class abstraction comes in the most lightweight possible way: a bit of extra magic, but not too much.

Here is where any sort of lightweight trait/mixin can play its role, using the widely compatible abstraction, together if needed with ES6/ES2015, ES5, or even ES3, to bring in any sort of extra behavior to the class: let’s see the bindings one!

Both one and two way bindings are supported, thanks to the bindings mixin. It is also possible to update partial text or html content using a different amount of curly brackets.

// bringing bindings in
var EasyBindings = DOMClass({
  // bring bindings in as a mixin
  with: [DOMClass.bindings],
  // bindings will look for an optional template
  template: `
        <div>
          This will be test: {{textContent}}.<br/>
          This will be html: {{{htmlContent}}}<br/>
          While this will call upper(textContent) each time textContent changes:
          {{upper(textContent)}}<br/>
          And this will call upper(htmlContent) each time it changes:
          {{{upper(htmlContent)}}}
        </div>
  `,
  // and an optional bindings property
  bindings: {
    // we are binding textContent and htmlContent
    // we can optionally provide their defaults
    textContent: 'just <strong>text</strong>, really',
    htmlContent: 'so <strong>bring it on</strong>, html!',
    // we are also binding a method invocation
    // that will update its bound "space" when one of its
    // parameters changes
    upper: function (textOrHTML) {
      return textOrHTML.toUpperCase();
    }
    // we could add any extra property or method, even if not bound
  }
});

In order to avoid any sort of conflicts with the component itself, the bindings mixin creates per each new instance a bindings property that will inherit from the one defined in the class so that defaults and methods can simply be shared.

We can use this property to update the text just like this:

var eb = document.body.appendChild(
  new EasyBindings
);

// how can we change those properties?
eb.bindings.textContent = 'is that it?';
// it will update only yhe text part of the node

// let's update the html too
eb.bindings.htmlContent = 'but this is <strong>awesome</strong>!';

The bindings property is not directly aware of its owner, which is in the previous example the new EasyBindings variable called eb. This peculiarity ensures that data bindings will be data related, and never directly UI one. It separates concerns between the component itself and the kind of data it needs to take care of.

Two way attributes binding

We can update some content, but we cannot be updated if the content is manually changed. This is where two way bindings become handy: inputs, selects, nodes and classes, others … these can be all managed via this mixin.

Inspired by KnockoutJS data-bind syntax, DOMClass.bindings mixin will also look for data-bind attribute and bind properties accordingly.

This time we’ll have any sort of property name on the left, and any sort of value on its right.

var InputName = DOMClass({
  with: DOMClass.bindings,
  template: `
    <input data-bind="value:name"><br>
    Hello {{name}}!
  `
});

var me = document.body.appendChild(new InputName);
me.bindings.name = 'Andrea';

We can now even change directly me.firstChild.value = "really?" or me.bindings.name = "no way!" or simply digit our name in the input.

Computed one way attributes

Like it is for text and html, we can compute those attributes that won’t be manually changed.

var LogIn = DOMClass({
  with: DOMClass.bindings,
  template: `
    <div>
      <input placeholder="user" data-bind="value:user">
      <br>
      <input placeholder="pass" data-bind="value:pass" type="password">
      <br>
      <input
        type="submit"
        data-bind="disabled:unauthorized(user,pass)"
      >
    <div>
  `,
  bindings: {
    unauthorized: function (user, pass) {
      return !(
        /\S+/.test(user || '') &&
        /\S{8,}/.test(pass || '')
      );
    }
  }
});


var login = document.body.appendChild(new LogIn);
// try to use input fields directly or ...
login.bindings.user = 'this should be';
login.bindings.pass = 'safe-enough';

If any of the parameters in unauthorized method gets updated, its DOM counterpart will be updated. This works within the component or its bindings object, or from the outer world.

Same attribute and property name

The mixin is smart enough to understand same properties name. If the binding is called value, no need to value:value, just value would do.

{
  template: `
    <input data-bind="value">
  `,
  bindings: {value: 'default'}
}

Multiple attributes binding per node

Using comma separated value would do.

{
  template: `
    <span data-bind="key,data-wut:wut(some,prop),no:worries"></span>
  `,
  bindings: {
    key: 'some-key-name',
    wut: function (a, b) {
      return a +  b;
    },
    worries: 'nope'
  }
}

Dispatching changed bindings

If the dispatchBindings property is defined, and it’s either truthy or an integer greater than -1, the component will trigger a bindingChanged event each time something changed.

var ChangingInput = DOMClass({
  with: DOMClass.bindings,
  dispatchBindings: true,
  template: '<input data-bind="value">'
});

document.body.appendChild(
  new ChangingInput
).addEventListener(
  'bindingChanged',
  console.log.bind(console)
);

Every time we type a letter in that input its parent component will dispatch a CustomEvent with a detail property which will contain at least two properties: key, representing the component bindings[key] that triggered such notification, and value, which will be the newly assigned value to that bindings property.

Please note that by default, or better setting dispatchBindings just as true, there will be a delay between notifications.

This makes the mechanism less greedy by default but if needed, we can specify an arbitrary amount of milliseconds. Such amount will be used as setTimoeut delay or as requestAnimationFrame one in case it’s 0, where 0 in this case means ASAP.

Bear in mind if you have a listener and within such lister you change another binding in the same component you are luckily to put yourself into an infinite updating loop: don’t mix up UI changes and events notification with data related binding!

Going dirty with bindings

Of course it would be possible to assign eb.bindings.self = this within its constructor, but if we need to directly modify the DOM when data changes and from the inside of a bound method or setter (yes, setters in bindings are supported too) then we might rethink the logic. There are few sketchy attempts in the demo folder, the CelsiusToFahrenheitBindingChanged and the CelsiusToFahrenheitSeppuku. These are really not recommended approaches since everything could be solved easily and cleanly via the CelsiusToFahrenheitInput one.

As Summary

Like I’ve said in the very top comment of the bindings source file, it wasn’t easy to achieve this kind of stability where stress-benchmark could make it also scoring decent performance.

There are few features not even implemented in more popular Polymer or other WebComponents oriented libraries yet, plus some not so common but extremely powerful patterns that, specially in classes and composition land, makes perfect sence like traits or mixins do.

I do hope you’ll find some time to play around and propose either a new mixin, or show what you created in few lines of code.

All demos and examples should be already in place, live or a part … what are you waiting for? Play with DOMClas, I assure you it’s going to be fun, if not even mind blowing!

NO? Well, get this last explanation of what is all this about then!

var XGreeter = DOMClass({

  // use bindings mixin
  'with': [DOMClass.bindings],

  // style any internal node with a class strong
  css: {'.strong':{fontWeight: 'bold'}},

  // template, it could be ES6 back-tick multi line string as well
  template: ''.concat(
    //
    // data-bind accepts one or more comma separated bindings.
    // Each data binding is a key:value pair
    //  - the key is the node property or attribute
    //  - the value is the property name in the bindings object
    // The value could also be method invocation.
    // In latter case, every time one of its parameters changes
    // the method will be re-executed and its return assigned
    // on the element as property or attribute.
    // example:
    '<p data-bind="class:changeClass(bold)">',
    //               ↑         ↑      ↑
    //               └─────────┤      └─── every time the 'bold'
    //                         │            property changes ┐
    // and the p 'class'       │                             │
    // attribute changes       └── changeClass gets invoked ←┘
    //
    // ---------------------------------------------------------
    //
    // When it comes to text nodes we don't have
    // properties or attributes like we do on elements.
    // In this case we can either bind a property by name
    // or just a method which will update the text node
    // content every time one of its parameters changes.
    //
      'Hello {{greeter(selectedIndex)}}, and Welcome!',
    //             ↑         ↑
    //             │         └─── every time the property
    // it's return │              selectedIndex changes ┐
    // is the text │                                    │
    //             └── the method greeter gets invoked ←┘
    '</p>',
    '<p><label>',
      '<small>How would like to be called?</small>', ' ',
      '<select data-bind="selectedIndex"></select>',
    //                          ↑
    // ┌────────────────────────┘
    // └─ if the element property is the same
    //    used in the bindings we can write propName
    //    instead of writing propName:propName.
    //
    //    If the select changes, or the bindings.selectedIndex
    //    is manually set to a different index
    //      - it invokes greeter(selectedIndex) writing new value
    //      - if it was a manual change it will update the select
    //      - if it was a UI user change it will update bindings
    //
    '</label></p>',
    '<p><label>',
      '<small>Would you like bold greetings?</small>', ' ',
      '<input type="checkbox" data-bind="checked:bold">',
    //                                      ↑     ↑
    // ┌────────────────────────────────────┴─────┘
    // └─ in this case we are binding the property bold
    //    of the bindings object with the checked status
    //    of the input and vice-versa, if the checked
    //    status changes for any reason, the bold property
    //    will update accordingly. This change will:
    //      - invoke the initial changeClass(bold)
    //      - it will update the class attribute on
    //        the first p node.
    //
    '</label></p>'
  ),

  // the binding object, it's inherited per each instance
  // but its bound properties are created on top
  // whenever this.createBindings(this.bindings) is called
  // The DOMClass.bindings mixin has aumatic mixin:init()
  // so that this will be understood and correctly analyzed
  // whenever a new instance is created
  bindings: {
    // reflects the initial <select> selectedIndex value
    selectedIndex: 0,
    // reflects the initial input checkbox status
    bold: 0,
    // not listened anywhere, it will be used in the constructor
    options: ['', 'Mrs', 'Miss', 'Mr', 'Jr', 'Chap'],
    // the greeter method with the index to show
    greeter: function (i) {
      // it's a method, its context is the current
      // instance of XGreeter bindings object
      return this.options[i] || 'user';
    },
    // the changeClass method returning the 'bold' or '' class
    changeClass: function (bold) {
      return bold ? 'strong' : '';
    }
  },

  // if true (or an integer greater than -1)
  // it will dispatch an event every time
  // something changes. When it's an integer
  // it will be used to debounce multiple calls.
  // If it's value is 0 it will assume ASAP invocation
  // and it will use requestAnimationFrame
  dispatchBindings: false,

  // it will be invoked per each new instance
  constructor: function () {
    // per each bindings.options
    this.bindings.options.forEach(function (option) {
      // create an option and append it to the select
      var el = this.appendChild(document.createElement('option'));
      // setting also value and content
      el.value = option;
      el.textContent = option;
    }, this.query('select'));
    // DOM4 has query and queryAll methods which look
    // for nested nodes only and DOMClass uses DOM4 features
  }
});

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor