Be aware of DOM state: jQuery selector with variable

Recently we were working on some jQuery code that added several controls to the page in a loop, and performed some manipulations, like .hide() on them. Since we needed this functionality in 2 different places, we did some refactoring and moved it into a separate function. And that’s when the fun started.

Problem

Since you can’t change the readonly property once the element is created, you’ll have to work with a div with an input element in it and hide/show it according to whether you’re able to enter data or not. This is used quite often to simulate a regular and an edit state of some fields. For this post, I’ve done something similar, but used a label instead of an input element with readonly state.

$(document).ready(function(){
    fillContacts();
});

function fillContacts(){
    var lastRow = $('.persons tr:last-child');
    var names = new Array( 'Bart', 'Jan', 'Piet' );

    for(i=0; i<names.length; i++){
        try{
            var newRow = createNewContactRow(i, names[i], lastRow);
            $(newRow).insertAfter(lastRow);
            // maybe some extra manipulations on $(newRow)
            lastRow = newRow;
        }
        catch(e){}
    }
}

// create row
function createNewContactRow(i, name, lastRow){
    var newRow = $('<tr><td><div id="editname_' + i + '"><input id="inputname_' + i + '" value="' + name + '" /></div>'
    + '<div id="readonlyname_' + i + '"><label id="name_' + i + '">' + name + '</label></div></td><td>'
    + '<div id="edit_' + i + '"><a href="#" class="edit">Edit</a></div>'
    + '<div id="save_' + i + '"><a href="#" class="save">Save</a></div></td></tr>');

    $('#editname_' + i).hide();
    $('#save_' + i).hide();

    return newRow;
}

In the code above, we create several new rows with an input element to edit the name, a label with the name, an edit link and a save link. In the initial state the page should be in readonly state, so the input element and the save link have to be hidden. However if you try to run the code above, the input elements and save links are still shown on the page.

One easy cheat is to hide the elements by using the CSS class selector. But since in our case the initial state of each row could be different (according to the state of another element), we couldn’t fall back on this easy workaround.

$('.save').hide();

Solution

The solution is actually very simple: make sure that your newly created element is added to the DOM before you do any manipulations on it. This sounds logic of course, but it’s something that is overseen very easily. Like when you’re in the case shown above, you can start doubting the selector with the variable, instead of figuring out the real problem.

// create row and add directly to DOM before any further manipulations
function createNewContactRow(i, name, lastRow){
    var newRow = $('<tr><td><div id="editname_' + i + '"><input id="inputname_' + i + '" value="' + name + '" /></div>'
    + '<div id="readonlyname_' + i + '"><label id="name_' + i + '">' + name + '</label></div></td><td>'
    + '<div id="edit_' + i + '"><a href="#" class="edit">Edit</a></div>'
    + '<div id="save_' + i + '"><a href="#" class="save">Save</a></div></td></tr>');

    // first add to DOM, then manipulate
    $(newRow).insertAfter(lastRow);

    $('#editname_' + i).hide();
    $('#save_' + i).hide();

    // possibly return for further manipulation in the caller method
    return newRow;
}

Note that this problem not only occurs with .hide(), but with any DOM manipulation, like e.g. .val().

Licensed under CC BY-NC-SA 4.0; code samples licensed under MIT.
comments powered by Disqus
Built with Hugo - Based on Theme Stack designed by Jimmy