domJSON v0.1.2

Convert DOM trees into compact JSON objects, and vice versa, as fast as possible.

What is domJSON for?

The purpose of domJSON is to produce very accurate representations of the DOM as JSON objects, and to do it very quickly. Broadly speaking, the goals of this project are:

  • Provide as accurate a copy of a given node's DOM properties as possible, but allow optional filtering to remove useless information
  • Be able to rebuild JSON nodes into DOM nodes as performantly as possible
  • Speed, speed, speed
  • No frivolous data: produce JSON objects that are as compact as possible, removing all information not relevant to the developer
  • Keep the library lightweight, with no dependencies

DomJSON works in the following browsers (mobile and desktop versions supported):

  • Chrome 36+
  • Firefox 24+
  • Safari 7+
  • Internet Explorer 9+

While there are probably dozens of varied use cases for this project, two stick out in my mind. The first is as a front-end debugging or snapshotting tool, which would allow end users to send reports that include "snapshots" of the DOM at any given point in time. Imagine an "I Found a Bug!" button on your web app, which, when clicked, would send an easily rebuildable copy of the DOM in its current state (computed styles included!) to the dev team. DomJSON can do that - to take a peek, check out the first example. Another potential use case is as a poor man's virtual DOM, allowing domJSON to do batch updates on your DOM tree much quicker than normal. Check out the second example for a demonstration. I'm sure there are plenty of other use cases for domJSON that I'm not even considering, but these are the two that make the most sense at first glance.

Getting Started

Installing domJSON is easy. You can pull it from Bower...

								bower install domjson
							
								<script src="bower_components/domjson/dist/domjson.min.js"></script>
							

...or grab it from NPM and manually include it as a script tag.

								npm install domjson --save
							
								<script src="node_modules/domjson/dist/domjson.min.js"></script>
							

Of course, you can also do it the old fashioned way and just download it directly from Github, then include the script tag. Once you've got domJSON installed, working with it is super easy: use the .toJSON() method to create a JSON representation of the DOM tree:

								var someDOMElement = document.getElementById('sampleId');
								var jsonOutput = domJSON.toJSON(someDOMElement);
							

And then rebuild the DOM Node from that JSON using .toDOM():

								var DOMDocumentFragment = domJSON.toDOM(jsonOutput);
								someDOMElement.parentNode.replaceChild(someDOMElement, DOMDocumentFragment);
							

When creating the JSON object, there are many precise options available, ensuring that developers can produce very specific and compact outputs. For example, the following will produce a JSON copy of someDOMElement's DOM tree that is only two levels deep, contains no "offset," "client," or "scroll*" type DOM properties, only keeps the "id" attribute on each DOM Node, and outputs a string (rather than a JSON-friendly object):

								var jsonOutput = domJSON.toJSON(someDOMElement, {
								    attributes: ['id'],
								    domProperties: {
								        exclude: true,
								        values: ['clientHeight', 'clientLeft', 'clientTop', 'offsetWidth', 'offsetHeight', 'offsetLeft', 'offsetTop', 'offsetWidth', 'scrollHeight', 'scrollLeft', 'scrollTop', 'scrollWidth']
								    },
								    deep: 2,
								    stringify: true
								});
							

API Options

Below is a list of all the .toJSON() options, and an explanation of what each one does:

absolutePaths Boolean|FilterList

Default: ['action', 'data', 'href', 'src']

On most modern browsers, relative paths in DOM properties and CSS styles are converted to absolute paths during runtime. Put another way, is you write background: url('./something.png'); in a CSS file, you'll see background: url('http://www.example.com/something.png'); when you query it while the app is running. However, that same luxury is not provided for DOM attributes. So, if you want to make sure all of the paths in the DOM attributes are absolute, set this option to true, or provide a FilterList of attributes to try and convert. The default setup for this option should cover 99% of use cases.

attributes Boolean|FilterList

Default: true

Set true to copy all DOM attributes, false to not copy any, or provide a FilterList.

computedStyle Boolean|FilterList

Default: false

Set true to grab the computed styles for every DOM node in your tree, false to not copy any, or provide a FilterList. Please that setting this to anything but false will cause a significant performance lag, since window.getComputedStyle() is slow and forces a redraw. Use this option only if you absolutely need it!

cull Boolean

Default: true

Many, if not most, of the properties on any given DOM node are null or just empty strings. Setting this option to true removes, or culls, all of those empty and redundant DOM properties from the final output. It does not remove DOM properties whose value is false or 0, since that is still useful information.

deep Boolean|Integer

Default: true

By default, the entire DOM tree will be parsed and turned into a JSON object. But what if you only want to go two levels deep, or ignore child nodes completely? In that case, set this value either to an integer describing how deep you want to recurse into the DOM tree, or false to completely ignore all children.

domProperties Boolean|FilterList

Default: true

Which DOM properties you'd like to copy. Setting this true means that every DOM property will be copied, while false means that they will all be completely ignored. You can also set a FilterList enumerating exactly which properties to include or exclude. Remember, the properties nodeType, nodeValue, and tagName are copied no matter what (even if this options is set to false)!

htmlOnly Boolean

Default: true

When this is true, only DOM Nodes of type 1 (HTML Elements) are copied. This means that all comments AND all text content is ignored!

metadata Boolean

Default: true

Controls whether or not to export metadata with the result. It is strongly recommended that you leave this set to true, as it is possible that the output format will change slightly in future versions of domJSON, and it would be a real shame to you couldn't rebuild your JSON objects into DOM Nodes because you left out the metadata...

serialProperties Boolean|FilterList

Default: false

Some DOM Nodes contain a few properties, notably innertHTML and outerHTML, simply contain serialized copies of the node's contents. Copying this information essentially amounts to copying every DOM Node's contents twice over, and is very wasteful in terms of speed and JSON output size. By setting this option to false, fields that are merely serializations of the DOM Node are ignored. Conversely, true will copy all serialized properties, while a FilterList enumerates which serialized properties to include or exclude. It is strongly recommended that you not change the default for this option unless you really need to keep the serializations, as including them has a seriously negative effect on performance.

stringify Boolean

Default: false

When this is true, the output will be a string. All it does is execute JSON.stringify() for you, so the following two statements will have an identical output:

									domJSON.toJSON(someNode, {stringify: true});
									JSON.stringify( domJSON.toJSON(someNode, {stringify: false}) );
								

Sample: Turning the DOM into JSON

This is a simple example of how domJSON can take a DOM Tree and turn it into a usable and representative JSON object. When you click the "Go!" button, the output box will show a JSON representation of the div containing this sample. Mess around with the option toggles, then check out the output in the blue area.

absolutePaths:
attributes:
computedStyle:
cull:
deep:
domProperties:
htmlOnly:
metadata:
serialProperties:

Sample: Speeding Up Batch DOM Updates with Web Workers

In theory, rather than updating the DOM piecemeal, developers can grab a branch of the DOM tree and convert it to JSON. They could make all of the changes they want in a JavaScript Web Worker, then re-build a Document Fragment from this modified JSON. Finally, this new fragment could easily inserted into the DOM, replacing the old branch. This should be much more performant than repeatedly updating the DOM.

The reasons for this are twofold. First, the browser can hang during large JavaScript operations, due to JavaScript's single-threaded nature. By using a Web Worker, we can cut down on that "browser freezing" significantly, offering a vastly improved user experience. Secondly, repeated jQuery updates means we have to do many DOM redraws, which can take forever. Instead, we can just convert the DOM to JSON, do all of our updating in JavaScript, and then replace the existing DOM Tree with a new Document Fragment. That's only one redraw - way faster!

For this example, we have a 2500 row table of numbered entries that can be incremented by a specified amount. So what will be quicker: iterating over the table cells using jQuery:

								var increment = parseInt($('[name="increment"]').first().val().trim());
									var frameDoc = $( $('#webworkersFrame').get(0).contentDocument.activeElement );
									var timer = new Date().getTime();

									//Update each row
									$('div', frameDoc).each(function(i,v){
										    $(this).html( parseInt(v.innerText, 10) + increment );
									});
									$('#webworkersResults').prepend('<div class="webworkers-red">Using jQuery ieration, the incrementation took: '+ ((new Date().getTime() - timer)/1000) +' seconds!</div>');
							

or using domJSON with a Web Worker?

								var worker = new Worker('./worker.js');

								var increment = parseInt($('[name="increment"]').first().val().trim());
									var frameDoc = $( $('#webworkersFrame').get(0).contentDocument.activeElement );;
									var timer = new Date().getTime();

									//Update each row using a web worker and domJSON
									worker.postMessage( [domJSON.toJSON($('section', frameDoc).get(0), {
									    attributes: false,
									    domProperties: false,
									}), increment] );
									worker.onmessage = function(e){
										    var x = domJSON.toDOM(e.data);
										    frameDoc.html(x);
										    $('#webworkersResults').prepend('<div class="webworkers-green">Using domJSON and Web Workers, the incrementation took: '+ ((new Date().getTime() - timer)/1000) +' seconds!</div>');
									};
								
									//In a separate file located at './worker.js'
									onmessage = function(e){
										    var increment = e.data[1];
										    var rows = e.data[0].node.childNodes;
										    var length = rows.length;
										    for (var i = 0; i < length; i++){
											        rows[i].childNodes[0].nodeValue = parseInt(rows[i].childNodes[0].nodeValue, 10) + increment;
										    }
										    postMessage(e.data[0]);
									};
									

Note: this demo can take up to 20 seconds per run - please be patient! Also, on Gecko based browsers, the jQuery method will actually be a bit faster...


Contributing

If you'd like to contribute, check out the Github repo, but please make sure to check the contribution guidelines before you make pull requests.

Are you curious about compatibility with a browser not list above? You can run the test suite using the instructions here to see if it passes - if it does, you're (probably) in the clear. Please note that Internet Explorer 8 and lower is explicitly not supported.

The domJSON library is published under the standard MIT License.