Clone many rows

This is a technique that I’ve had to use a few times this week. It’s remarkably handy when you need to do a lot of DOM manipulation at once.

Think of the following scenario: your client (or boss) has asked you to build a form. Diligently you build it using the latest in form-building theories and jQuery-based validation. Then, right at the bottom of the brief, you see a spiteful “Add more applicants” button next to an input and a little note saying that they might want to add 200 applicants.

You laugh this scenario away, but just try cloning a few form fields 200 times. Clone all the mark-up that keeps it in place and tweak all the names and IDs so you can actually keep that data and interact with the form properly. Go ahead, I’ll wait. Need some markup to clone? Duplicate this definition list:

<fieldset id="applicants">
    <legend>Applicants</legend>
    <dl>
        <dt><label for="name-1">Name</label></dt>
        <dd><input type="text" name="name-1" id="name-1"></dd>

        <dt><label for="email-1">E-mail Address</label></dt>
        <dd><input type="email" name="email-1" id="email-1"></dd>

        <dt><label for="age-1">Age</label></dt>
        <dd><input type="number" min="13" name="age-1" id="age-1"></dd>

        <dt>Would you like to buy or sell?</dt>
        <dd>
            <ul>
                <li><label><input type="radio" name="buysell-1" id="buysell-buy-1">Buy</label></li>
                <li><label><input type="radio" name="buysell-1" id="buysell-sell-1">Sell</label></li>
            </ul>
        </dd>
    </dl>
</fieldset>

Your browser froze for a while, huh? Does your computer have enough RAM to actually complete the task or did you get a warning that said that the page wasn’t responsive?

Your browser stalled because JavaScript loops are “blocking” – everything stops while that loop happens. When you think about trying to loop over an ever-changing array, you can see why this would be necessary. But it does leave us with a problem: you still have a few form fields to clone 200 times and if your computer struggled, just think what your clients’ computer will do.

Luckily, this is not a new problem, the solution is more than 2 years old and was written by Nicholas C. Zakas. He called it timedChunk. Unlike a a standard JavaScript loop, timedChunk does as much as it can in a small amount of time before repeating. Today I’ll show you a general pattern to clone a lot of rows using timedChunk.

Instead of creating a jQuery plug-in, I prefer using a function. I always find that each situation is too different to justify a generic plug-in. Here’s the code:

var cloneRow = (function (undef) {

// A few variables. We need a copy of the row that we're cloning, the current
// number of times it's been cloned and the maximum number of clones to make.
    var jQsource,
        current,
        full;

// We create a function to handle the resetting. This way we can just call a
// single function to set everything back to the way it was.
    function endChunk() {
        jQsource = undef;
        current = undef;
        full = undef;
    }

// Word to the wise: never make a function inside a loop. We need a function to
// update attributes generally and to tweak the inputs.
    function getKey(str) {
        var key = str.split('-');
        key.pop();
        return key.join('-');
    }
    function updateAttr(i, a) {
        return a && getKey(a) + '-' + current;
    }
    function fixInputs() {
        var jQthis = $(this);
        jQthis.attr('id', updateAttr);
        jQthis.attr('for', updateAttr);
        jQthis.attr('name', updateAttr);
        jQthis.val('').removeAttr('checked');
    }

    return function chunk(jQs, total) {

// A few variables. We need to know the time right now because we'll only be
// working for 50 milliseconds. We also need a document fragment since that's
// the fastest way to add a lot of nodes to the DOM and the cloned node itself.
        var start = +new Date(),
            frag,
            jQclone;

// If our variables haven't been defined, we know that this is the start of the
// chunk. We store our source and set up the chunker. We set current to the
// current number of rows so that our numbers always line up.
        if (jQsource === undef) {
            jQsource = jQs;
            current = jQsource.find('dl').size();
            full = current + total;
        }

// The main magic happens here. The princible is that if more rows need cloning
// and 50 milliseconds haven't passed, repeat.
        while (current < full && (+new Date() - start < 50)) {

// Increase the current value and create a document fragment.
            current += 1;
            frag = document.createDocumentFragment();

// Clone the last DL tag, find all the LABELs and INPUTs and update them ready
// for appending. We need to use jQuery's .get() method to get the node itself
// and use the standard appendChild to add it to our fragment as (as far as I
// know) there is no way to add to a document fragment using jQuery yet.
            jQclone = jQsource.find('dl:last').clone();
            frag.appendChild(jQclone.find('label,input').each(fixInputs)
                .end().get(0));

            jQsource.append(frag);
        }

// Now another important part of the chunking. If there are more rows that need
// cloning, call this inner function again in 25 milliseconds. If not, call our
// endChunk function to re-enable the chunker again.
        if (current < full) {
            setTimeout(chunk, 25);
        } else {
            endChunk();
        }

    };

}());

Calling it is just as simple as this:

cloneRow($('#applicants'), 200);

The browser will sit back and chug through the cloning. It will succeed whether that second argument is 10, 200 or even 3000.

There are other things that you can add to this function, such as a progress meter or the ability to remove the cloned rows. But if you need to do a lot of DOM manipulation in a small amount of time, this is the kind of technique you'll need to rely on. Just remember a few basics:

  • Cache your selectors.
  • Don't make functions inside loops.
  • Rely on document fragments.
  • Do as much as you can in 50 milliseconds, rest briefly, repeat.

If you want to play around with this code, I built a fiddle. I hope this helps someone who suddenly comes up against this problem.

Leave a Reply

Your email address will not be published. Required fields are marked *