A lightweight, cross browser, simplification of WebComponents.
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
build
demo
src
template
test
utils
.gitignore
.npmignore
.travis.yml
LICENSE.txt
Makefile
README.md
index.html
package.json
testrunner.js

README.md

dom-class build status

Built on top of es-class, and integrating handy restyle features, DOMClass is a lightweight, cross browser, simplification of WebComponents.

Please read the related blog post to know extra details about this project.

Demo

In case of doubts, some demo could quickly clarify DOMClass potentials so here they are:

  • the (in)famouse DBMonster benchmark, a component split in rows and cells that performs like its pure DOM counter part
  • few classic basic components for forms and playful interaction, or runtime creation
  • a one to one comparison VS Knockout JS, showing main differences between these two approaches. TL;DR is that DOMClass doesn't use eval or Function and it's based on DRY components rather than magic HTML parsing (which is not necessarily a bad thing but it's surely a different paradigm)

Finally a quoted comment from DOMClass.bindings Mixin VS Performance post

This is seriously cool ... so much functionality via so little framework code.It begs the question ... "What the heck are those other frameworks doing that involves so much code?". I especially like that it is based on the existing web platform as opposed to what is promised in V.Next (i.e. ES5,6, etc, etc). Outstanding work.

How To

All es-class features are included, and the constructor will be automatically used once the component has been initialized.

var MyComponent = new DOMClass({
  constructor: function () {
    this.textContent = 'Hello World!';
  }
});

document.body.append(new MyComponent);

While by default new components will be created name-conflict free, it is possible to specify the desired node name via the name property.

var XAMe = new DOMClass({
  name: 'x-a-me',
  constructor: function () {
    console.log(this.textContent);
  }
});

// we can create via new DOMClass
// or we can use the name
document.body.innerHTML = '<x-a-me>Hello Again!</x-a-me>';

Extending natives

Since version 0.4.0 it is also possible to extend directly extend native and all we need is basically the following:

var MyInput = DOMClass({
  extends: HTMLInputElement,
  // just for demo purpose
  constructor: function (value) {
    this.value = value;
  }
});

document.query('form').appendChild(
  new MyInput('hello')
);

If a native constructor is used, and it's an instance of HTMLElement, there's no need to use the is property, stick with a name and DOMClass will take care of the rest.

DOMClass.bindings Mixin

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.

Custom Elements callbacks

All famous and verbose CustomElement callbacks are aliased in a simpler, yet semantic, way.

var MyCE = new DOMClass({

  // equivalent of createdCallback
  // will be invoked after the component will get initialized
  // in case there are mixins or super extends
  constructor: function () {},

  // equivalent of attachedCallback
  onAttached: function () {},

  // equivalent of detachedCallback
  onDetached: function () {},

  // equivalent of attributeChangedCallback
  onChanged: function () {}

});

It is possible to style elements per Component, which will eventually also create a css property we can use to apply own specific styles.

var XSquare = new DOMClass({
  name: 'x-square',
  css: {
    // to self reference the component
    // it is also possible to use just empty selector
    // handy specially when the name is unknown
    'x-square': {
      display: 'block',
      padding: 0,
      width: 32,
      height: 32,
      border: '1px solid black'
    },
    // to reference any element within the component
    // simply use regular CSS selectors
    'span': {
      display: 'inline-block',
      width: '100%',
      lineHeight: 32,
      textAlign: 'center',
      font: {
        size: 26,
        weight: 'bold'
      }
    }
    // mediq queries, animations, and everything else
    // are also supported. Please see `restyle` documentation
  },
  // Yes! DOMClass accepts arguments too \o/
  constructor: function (text) {
    this.innerHTML = '<span>' + text + '</span>';
    // if there is a css in the prototype,
    // we can use the css property to overwrite/set the inherited one
    // following is the equivalent of this.css = { ... }
    this.css.set({
      // empty selector, same as using 'x-square'
      // to reference itself
      '': {
        backgroundColor: 'rgb(' + [
          Math.random() * 256 | 0,
          Math.random() * 256 | 0,
          Math.random() * 256 | 0
        ].join(',') + ')'
      }
    });
  }
});

// example
document.body.append(new XSquare('!'));
document.body.append(new XSquare('A'));
document.body.append(new XSquare('Z'));

Please note there is no ShadowDOM, template, or HTMLImport polyfill, everything works with regular, well supported, HTML5 standards and on top of document.registerElement.

Live demos and benchmarks

In the following demo page it's possible to interact with components. Read its source code to know more.

The famous DBMonster benchmark is also available for fun and comparison. All compatible devices can also be tested against this page and the component file is just this little.

Compatibility

The following list of desktop browsers has been successfully tested:

  • Chrome
  • Firefox
  • IE9 or greater
  • Safari
  • Opera

The following list of mobile OS has been successfully tested:

  • iOS 5.1 or greater
  • Android 2.2 or greater
  • FirefoxOS 1.1 or greater
  • KindleFire 3 or greater
  • Windows Phone 7 or greater
  • Opera Mobile 12 or greater
  • Blackberry OS 10
  • webOS 2 or LG TV
  • Samsung Bada OS 2 or greater

If you'd like to know if your browser is supported please check the live test page and let me know if something is not green, thank you.

Which file for what ?

The build folder contains all variants, following explained:

What is Vitamer JS ?

Directly from Wikipedia:

Typically, the vitamin activity of multiple vitamers is due to the body's (limited) ability to convert one vitamer to another, or many vitamers to the same enzymatic cofactor(s), which is active in the body as the important form of the vitamin.

As part of the definition of vitamin, the body cannot completely synthesize an optimal amount of vitamin activity from simple foodstuffs, without some minimal amount of a vitamer molecule as a basis.

In this case it's the minimum amount of packages required in order to obtain a modern, comfortable, and cross browser environment based on latest DOM standards and proposals.

The "all in one shot" file contains the following modules:

From vanilla JS world, above package might be truly everything you need in order to create amazing apps, forgetting about cross platform issues or performance gotchas (greedy RAM or CPU operations).

Since the total package amount, once minified and gzipped, is less than 9KB, I thought Vitamer, as opposite to the well known Polymer, would have worked as file name. Let me know if you have better name ideas :-)

License

The MIT Style License

Copyright (C) 2015 by Andrea Giammarchi - @WebReflection

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.