Skip to content

Latest commit

 

History

History
594 lines (402 loc) · 15.8 KB

README.md

File metadata and controls

594 lines (402 loc) · 15.8 KB

<±/> diffHTML

The core diffHTML library that parses HTML, syncs changes, and patches the DOM.

Stable version: 1.0.0-beta.4

Inspired by React and motivated by the Web, this library is designed to help web developers write components and applications for the web. By focusing on the markup representing how your state should look, diffHTML will figure out how to modify the page with a minimal amount of operations.

Quick Jump

How to install

Back to quick jump...

The latest built version (but not necessarily the latest stable) is available for quick download from the master branch. Use this to test the bleeding edge.

Or you can use npm:

npm install diffhtml --save

or using yarn:

yarn add diffhtml

The module can be included natively in Node or browser environments. It is exported as a global named diff unless loaded as a module, in which case you determine the name diffHTML will be assigned to.

Include in HTML (browser)

Back to quick jump...

The path of least resistance to trying out diffHTML. Simply drop a script into your markup and diff will be available for your code to start utilizing its benefits.

<script src="node_modules/diffhtml/dist/diffhtml.js"></script>

<script>
  // Use a block to keep variables out of the global scope.
  {
    const { innerHTML } = diff;

    innerHTML(document.body, '<span>Hello world!</span>');
  }
</script>

Require with CommonJS (Node)

Back to quick jump...

Node is built using the CommonJS pattern as this predates ES Modules by years, if you are consuming diffHTML inside Node, it is recommended to use this method of importing.

const { innerHTML } = require('diffhtml');

innerHTML(document.body, '<span>Hello world!</span>');

You can import only what you need if you're using ES Modules:

Import using ES Modules syntax (advanced usage)

Back to quick jump...

Useful for those who are building applications using the next-generation syntax for defining modules. diffHTML is fully compatible with ES Modules and will continue to improve supporting techniques benefiting from this format, such as tree shaking.

import { innerHTML } from 'diffhtml';

innerHTML(document.body, '<span>Hello world!</span>');

Module format locations

Back to quick jump...

This library is authored in vanilla ES Modules with no experimental syntax enabled. The CJS build is compiled with Babel to reduce import calls to require. The UMD build is generated by Rollup to ES5.

Format Specification Location
UMD (AMD/CJS/Browser) ES5 diffhtml/dist/diffhtml.js
CJS ES6 diffhtml/dist/cjs/*
ESM (ES Modules) ES6 diffhtml/dist/es/*

Quick start

Back to quick jump

Rendering HTML to a DOM Node

The primary purpose of diffHTML is to render markup to the DOM. The most basic way to apply this concept is by using simple strings.

import { innerHTML } from 'diffhtml';

innerHTML(document.body, '<strong>Hello world</strong>');

Multi-line strings

Back to quick jump

Multi-line strings can be achieved using the ES6 language feature template literal strings. These utilize the back-tick and may be spread over multiple lines. These are only useful if you interpolate primitive JavaScript types. diffHTML is smart enough to recognize the following example as a single <strong> instead of parsing as two text nodes and a span:

import { innerHTML } from 'diffhtml';

innerHTML(document.body, `
  <strong>Hello world</strong>
`);

These even have the ability to interpolate (use variables) values in the string. These values can be tag names, element attributes, and child nodes. So you could do:

import { innerHTML } from 'diffhtml';

const location = 'world';

innerHTML(document.body, `
  <strong>Hello ${location}</strong>
`);

Although, if you try and pass an object or function it will be serialized to a string, which is generally not desirable. For example this would not work correctly, it would flatten the style value to a string instead of passing the reference:

import { innerHTML } from 'diffhtml';

const style = { fontSize: '11px' };

innerHTML(document.body, `
  <strong style=${style}>Hello world</strong>
`);

To overcome this limitation, if you need it, seek out the HTML tagged template literal. This is a simple function you import and prepend to the template strings. You can find more information in the next section.

Tagged template literals

Back to quick jump

This upgrades the template literal to use a fast HTML parser which builds a Virtual Tree of our markup and assigns dynamic references to properties, child nodes, and even facilitates interpolating React components.

To use the tagged template feature, simply import html:

import { html, innerHTML } from 'diffhtml';

const style = { fontSize: '11px' };

innerHTML(document.body, html`
  <strong style=${style}>Hello world</strong>
`);

Writing a component

Back to quick jump

Components are provided by an optional package diffhtml-components which contains a Web Component and React Like interface. Both are designed to maintain as much cross-compatibility as possible.

The React-like API supports all major browsers, you can import and render a component like so:

import { html, innerHTML } from 'diffhtml';
import { Component } from 'diffhtml-components';

class HelloWorld extends Component {
  render() {
    return html`
      <strong>Hello world</strong>
    `;
  }
}

innerHTML(document.body, html`<${HelloWorld} />`);

For more information about this, check out the docs for diffhtml-components

Documentation

Back to quick jump...

Lifecycle overview

Back to quick jump...

The following outlines the lifecycle flow of diffHTML rendering. The tasks that follow occur after something triggers an innerHTML or outerHTML call.

Schedule

The first task that runs is scheduling or deferring the transaction that was created by the innerHTML or outerHTML call. diffHTML is a shared namespace and will only allow one render at a time. If a render is happening during this time, then the transaction will be deferred by scheduling it for later processing.

Should update

This looks at the markup passed in and checks if it matches with the markup passed before. If nothing has changed, then it will abort the transaction.

Reconcile trees

Synchronize trees

Patch node

End as Promise

Virtual Tree Abstraction

Back to quick jump...

Middleware

Back to quick jump...

Writing

Back to quick jump...

Consuming

Back to quick jump...

Building the input Virtual Tree

Back to quick jump...

Invoking Middleware

Back to quick jump...

Synchronizing the input tree into the original tree

Back to quick jump...

Patching the DOM Node

Back to quick jump...

Asynchronous transitions

Back to quick jump...

Completing the render transaction

Back to quick jump...

API

Back to quick jump...

The follow error types are exposed so you can test exceptions:

  • TransitionStateError - Happens when errors occur during transitions.
  • DOMException - Happens whenever a DOM manipulation fails.

Options

Back to quick jump...

This is an optional argument that can be passed to any diff method. The inner property can only be used with the element method.

  • inner - Boolean that determines if innerHTML is used.

Diff an element with markup

Back to quick jump...

This method will take in a string of markup that matches the element root you are diffing against. This allows you to change attributes and text on the main element. This also allows you to change the document.documentElement.

You cannot override the inner options property here.

diff.outerHTML(document.body, '<body class="test"><h1>Hello world!</h1></body>');

Diff an element's children with markup

Back to quick jump...

This method also takes in a string of markup, but unlike outerHTML this is children-only markup that will be nested inside the element passed.

You cannot override the inner options property here.

diff.innerHTML(document.body, '<h1>Hello world!</h1>');

Diff an element to another element

Back to quick jump...

Unlike the previous two methods, this will take in two elements and diff them together.

The inner options property can be set here to change between inner/outerHTML.

var newBody = document.createElement('body');

newBody.innerHTML = '<h1>Hello world!</h1>';
newBody.setAttribute('class', 'test');

diff.element(document.body, newBody);

With inner set:

var h1 = document.createElement('h1');

h1.innerHTML = 'Hello world!';

diff.element(document.body, h1, { inner: true });

Release element

Use this method if you need to clean up memory allocations and anything else internal to diffHTML associated with your element. This is very useful for unit testing and general cleanup when you're done with an element.

var h1 = document.createElement('h1');

h1.innerHTML = 'Hello world!';

diff.element(document.body, h1, { inner: true });
diff.release(document.body);

Add a transition state callback

Adds a global transition listener. With many elements this could be an expensive operation, so try to limit the amount of listeners added if you're concerned about performance.

Since the callback triggers with various elements, most of which you probably don't care about, you'll want to filter. A good way of filtering is to use the DOM matches method. It's fairly well supported (http://caniuse.com/#feat=matchesselector) and may suit many projects. If you need backwards compatibility, consider using jQuery's is.

You can do fun, highly specific, filters:

addTransitionState('attached', function(element) {
  // Fade in the main container after it's attached into the DOM.
  if (element.matches('body main.container')) {
    $(element).stop(true, true).fadeIn();
  }
});

If you like these transitions and want to declaratively assign them in tagged templates, check out the diffhtml-inline-transitions plugin.

Available states

Format is: name[callbackArgs]

  • attached[element] For when an element is attached to the DOM.
  • detached[element] For when an element leaves the DOM.
  • replaced[oldElement, newElement] For when elements are swapped
  • attributeChanged[element, attributeName, oldValue, newValue] For when attributes are changed.
  • textChanged[element, oldValue, newValue] For when text has changed in either TextNodes or SVG text elements.

A note about detached/replaced element accuracy

When rendering Nodes that contain lists of identical elements, you may not receive the elements you expect in the detached and replaced transition state hooks. This is a known limitation of string diffing and allows for better performance. By default if no key is specified, the last element will be removed and the subsequent elements from the one that was removed will be mutated via replace.

This isn't really ideal. At all.

What you should do here is add a key attribute with a unique value that persists between renders.

For example, when the following markup...

<ul>
  <li>Test</li>
  <li>This</li>
  <li>Out</li>
</ul>

...is changed into...

<ul>
  <li>Test</li>
  <li>Out</li>
</ul>

The transformative operations are:

  1. Remove the last element
  2. Replace the text of the second element to 'out'

What we intended, however, was to simply remove the second item. And to achieve that, decorate your markup like so...

<ul>
  <li key="1">Test</li>
  <li key="2">This</li>
  <li key="3">Out</li>
</ul>

...and update with matching attributes...

<ul>
  <li key="1">Test</li>
  <li key="3">Out</li>
</ul>

Now the transformative operations are:

  1. Remove the second element

Remove a transition state callback

Removes a global transition listener.

When invoked with no arguments, this method will remove all transition callbacks. When invoked with the name argument it will remove all transition state callbacks matching the name, and so on for the callback.

// Removes all registered transition states.
diff.removeTransitionState();

// Removes states by name.
diff.removeTransitionState('attached');

// Removes states by name and callback reference.
diff.removeTransitionState('attached', callbackReference);

HTML

You can use the diff.html tagged template helper to build up dynamic trees in a way that looks very similar to JSX.

For instance the following example:

const fixture = document.createElement('div');

function showUnixTime() {
  fixture.querySelector('span').innerHTML = Date.now();
}

diff.outerHTML(fixture, `
  <div>
    <button>Show current unix time</button>
    <span>${Date.now()}</span>
  </div>
`);

fixture.addEventListener('click', showUnixTime);

Could be rewritten with the helper as:

const fixture = document.createElement('div');

function showUnixTime() {
  fixture.querySelector('span').innerHTML = Date.now();
}

diff.outerHTML(fixture, html`
  <div onclick=${showUnixTime}>
    <button>Show current unix time</button>
    <span>${Date.now()}</span>
  </div>
`);

So this feature allows for inline binding of any DOM event, and sending dynamic property data to any element.

Tagged templates also have no problem consuming other tagged templates (even from arrays), so you will be able to do:

const fixture = document.createElement('div');

const listItems = ['diff', 'HTML', '♥'];

diff.outerHtml(fixture, html`
  <ul>
    ${listItems.map(item => html`<li>${item.text}</li>`)}
  </ul>
`);

Middleware

Back to quick jump...

More information and a demo are available on http://www.diffhtml.org/