Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create reusable document fragment from the DOM

I would like to have a document fragment/element on the shelf to which I've connected a bunch of other elements. Then whenever I want to add one of these element-systems to the DOM, I copy the fragment, add the unique DOM ID and attach it.

So, for example:

var doc = document,
    prototype = doc.createElement(),  // or fragment
    ra = doc.createElement("div"),
    rp = doc.createElement("div"),
    rp1 = doc.createElement("a"), 
    rp2 = doc.createElement("a"),
    rp3 = doc.createElement("a");

ra.appendChild(rp);
rp.appendChild(rp1);
rp.appendChild(rp2);
rp.appendChild(rp3);
rp1.className = "rp1";
rp2.className = "rp2";
rp3.className = "rp3";

prototype.appendChild(ra);

This creates the prototype. Then I want to be able to copy the prototype, add an id, and attach. Like so:

var fr = doc.createDocumentFragment(),
    to_use = prototype; // This step is illegal, but what I want!
                        // I want prototype to remain to be copied again.

to_use.id = "unique_id75";
fr.appendChild(to_use);
doc.getElementById("container").appendChild(fr);

I know it's not legal as it stands. I've done fiddles and researched and so on, but it ain't working. One SO post suggested el = doc.appendChild(el); returns el, but that didn't get me far.

So... is it possible? Can you create an on-the-shelf element which can be reused? Or do you have to build the DOM structure you want to add from scratch each time?

Essentially I'm looking for a performance boost 'cos I'm creating thousands of these suckers :)

Thanks.

like image 991
Nick Avatar asked Nov 28 '22 08:11

Nick


2 Answers

Use Node.cloneNode:

var container = document.getElementById('container');

var prototype = document.createElement('div');
prototype.innerHTML = "<p>Adding some <strong>arbitrary</strong> HTML in"
 +" here just to illustrate.</p> <p>Some <span>nesting</span> too.</p>"
 +"<p>CloneNode doesn't care how the initial nodes are created.</p>";

var prototype_copy = prototype.cloneNode(true);

prototype_copy.id = 'whatever'; //note--must be an Element!

container.appendChild(prototype_copy);

Speed Tips

There are three operations you want to minimize:

String Parsing

This occurs when you use innerHTML. innerHTML is fast when you use it in isolation. It's often faster than the equivalent manual-DOM construction because of the overhead of all those DOM method calls. However, you want to keep innerHTML out of inner loops and you don't want to use it for appending. element.innerHTML += 'more html' in particular has catastrophic run-time behavior as the element's contents get bigger and bigger. It also destroys any event or data binding because all those nodes are destroyed and recreated.

So use innerHTML to create your "prototype" nodes for convenience, but for inner loops use DOM manipulation. To clone your prototypes, use prototype.cloneNode(true) which does not invoke the parser. (Be careful with id attributes in cloned prototypes--you need to make sure yourself that they are unique when you append them to the document!)

Document tree modification (repeated appendChild calls)

Every time you modify the document tree you might trigger a repaint of the document window and update the document DOM node relationships, which can be slow. Instead, batch your appends up into a DocumentFragment and append that to the document DOM only once.

Node lookup

If you already have an in-memory prototype object and want to modify pieces of it, you will need to navigate the DOM to find and modify those pieces whether you use DOM traversal, getElement*, or querySelector*.

Keep these searches out of your inner loops by keeping a reference to the nodes you want to modify when you create the prototype. Then whenever you want to clone a near-identical copy of the prototype, modify the nodes you have references to already and then clone the modified prototype.

Sample Template object

For the heck of it, here is a basic (and probably fast) template object illustrating the use of cloneNode and cached node references (reducing the use of string parsing and Node lookups).

Supply it with a "prototype" node (or string) with class names and data-attr="slotname attributename" attributes. The class names become "slots" for text-content replacement; the elements with data-attr become slots for attribute name setting/replacement. You can then supply an object to the render() method with new values for the slots you have defined, and you will get back a clone of the node with the replacements done.

Example usage is at the bottom.

function Template(proto) {
    if (typeof proto === 'string') {
        this.proto = this.fromString(proto);
    } else {
        this.proto = proto.cloneNode(true);
    }
    this.slots = this.findSlots(this.proto);
}
Template.prototype.fromString = function(str) {
    var d = document.createDocumentFragment();
    var temp = document.createElement('div');
    temp.innerHTML = str;
    while (temp.firstChild) {
        d.appendChild(temp.firstChild);
    }
    return d;
};
Template.prototype.findSlots = function(proto) {
    // textContent slots
    var slots = {};
    var tokens = /^\s*(\w+)\s+(\w+)\s*$/;
    var classes = proto.querySelectorAll('[class]');
    Array.prototype.forEach.call(classes, function(e) {
        var command = ['setText', e];
        Array.prototype.forEach.call(e.classList, function(c) {
            slots[c] = command;
        });
    });
    var attributes = proto.querySelectorAll('[data-attr]');
    Array.prototype.forEach.call(attributes, function(e) {
        var matches = e.getAttribute('data-attr').match(tokens);
        if (matches) {
            slots[matches[1]] = ['setAttr', e, matches[2]];
        }
        e.removeAttribute('data-attr');
    });
    return slots;
};
Template.prototype.render = function(data) {
    Object.getOwnPropertyNames(data).forEach(function(name) {
        var cmd = this.slots[name];
        if (cmd) {
            this[cmd[0]].apply(this, cmd.slice(1).concat(data[name]));
        }
    }, this);
    return this.proto.cloneNode(true);
};
Template.prototype.setText = (function() {
    var d = document.createElement('div');
    var txtprop = (d.textContent === '') ? 'textContent' : 'innerText';
    d = null;
    return function(elem, val) {
        elem[txtprop] = val;
    };
}());
Template.prototype.setAttr = function(elem, attrname, val) {
    elem.setAttribute(attrname, val);
};



var tpl = new Template('<p data-attr="cloneid id">This is clone number <span class="clonenumber">one</span>!</p>');

var tpl_data = {
    cloneid: 0,
    clonenumber: 0
};
var df = document.createDocumentFragment();
for (var i = 0; i < 100; i++) {
    tpl_data.cloneid = 'id' + i;
    tpl_data.clonenumber = i;
    df.appendChild(tpl.render(tpl_data));
}
document.body.appendChild(df);
like image 181
Francis Avila Avatar answered Dec 04 '22 10:12

Francis Avila


I'd be shocked if innerHTML wasn't faster. Pre-compiled templates such as those provided by lo-dash or doT seem like a great way to go!

Check out this simple example: http://jsperf.com/lodash-template

It shows you can get 300,000 ops/sec for a fairly complex template with a loop using lo-dash's pre-compiled templates. Seems pretty fast to me and way cleaner JS.

Obviously, this is only one part of the problem. This generates the HTML, actually inserting the HTML is another problem, but once again, innerHTML seems to win over cloneNode and other DOM-based approaches and generally the code is way cleaner. http://jsperf.com/clonenode-vs-innerhtml-redo/2

Obviously you can take these benchmarks worth a grain of salt. What really matters is your actual app. But I'd recommend giving multiple approaches a try and benchmarking them yourself before making up your mind.

Note: A lot of the benchmarks about templates on JSPerf are doing it wrong. They're re-compiling the template on every iteration, which is obviously going to be way slow.

like image 32
Jamund Ferguson Avatar answered Dec 04 '22 10:12

Jamund Ferguson