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.
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.
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>
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:
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>');
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/* |
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 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.
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>
`);
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
The following outlines the lifecycle flow of diffHTML rendering. The tasks that
follow occur after something triggers an innerHTML
or outerHTML
call.
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.
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.
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.
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 ifinnerHTML
is used.
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>');
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>');
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 });
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);
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 swappedattributeChanged[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.
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:
- Remove the last element
- 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:
- Remove the second element
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);
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>
`);
More information and a demo are available on http://www.diffhtml.org/