Michael Emerson
Michael Emerson

Reputation: 1813

jQuery each with ajax call will continue before it's finished

I have some jQuery which uses an each loop to go through values entered in a repeated form field on a Symfony 3 CRM. There is a $.post which sends the entered value to a function that checks for duplicates in the database, and if it's a duplicate it adds something to an array, otherwise it adds a blank value to indicate it's not a dupe. Once these have been done, it then checks the final array and adds any errors to the error block to display to the user.

However, it seems that the array is ALWAYS blank and I belivee it's because it's running the code that displays the errors BEFORE it's actually finished getting the response.

Here is my code:

$('#puppy_form').on('submit', function() {
    var bitch_errors = [];
    var dog_errors = [];
    // NOTE: Bitch and dog names need to be checked differently so we know which error is assigned to which input
    $('.check_bitch_name').each( function(i, obj) {
        // need to check each name for validity and duplication.
        var entered_bitch_name = obj.value;
        var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
        if(!pattern.test(entered_bitch_name)) {
            bitch_errors[i+1] = "invalid";
        } else {
            // now to check for duplicates
            $.post('/check-puppy-name', { name: entered_bitch_name }
            ).done(function (response) {
                if(response == 'duplicate') {
                    bitch_errors[i+1] = "duplicate";
                } else {
                    bitch_errors[i+1] = "";
                }
            });
        }
    });
    $('.check_dog_name').each( function(i, obj) {
        // need to check each name for validity and duplication.
        var entered_dog_name = obj.value;
        var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
        if(!pattern.test(entered_dog_name)) {
            dog_errors[i+1] = "invalid";
        } else {
            // now to check for duplicates
            $.post('/check-puppy-name', { name: entered_dog_name }
            ).done(function (response) {
                if(response == 'duplicate') {
                    dog_errors[i+1] = "duplicate";
                } else {
                    dog_errors[i+1] = "";
                }
            });
        }
    });


    if(count(bitch_errors) == 0 && count(dog_errors) == 0) {
        return true;
    }

    // loop through the errors and assign them to the correct input
    $.each( bitch_errors, function( key, value ) {
        if (value == "invalid") {
            $('input[name="bitch_name['+key+']"]').parent().addClass('has-error');
            $('input[name="bitch_name['+key+']"]').next('.error-message').html('Names must be at least two words, and only contain letters');
            return false;
        } else if(value == "duplicate") {
            $('input[name="bitch_name['+key+']"]').parent().addClass('has-error');
            $('input[name="bitch_name['+key+']"]').next('.error-message').html('Sorry, this name has already been taken');
            return false;
        }
    });
    $.each( dog_errors, function( key, value ) {
        if(value != "") {
            if (value == "invalid") {
                $('input[name="dog_name['+key+']"]').parent().addClass('has-error');
                $('input[name="dog_name['+key+']"]').next('.error-message').html('Names must be at least two words, and only contain letters');
                return false;
            } else if(value == "duplicate") {
                $('input[name="dog_name['+key+']"]').parent().addClass('has-error');
                $('input[name="dog_name['+key+']"]').next('.error-message').html('Sorry, this name has already been taken');
                return false;
            }
        }
    });

    return false;

});

Basically, it first checks that the inputted name is valid, then posts off and checks for dupes. The issue is, even though it does the validity check (and prints errors accordingly) it seems to ignore the dupe check and carry on before it's even called back the first response.

How can I make sure it's finished it's checking before going on and adding the errors to the form? I've tried other solutions including attempting to implement the $.when functionality in jQuery but I don't really understand how to make it work. Any help appreciated.

Upvotes: 1

Views: 230

Answers (3)

Alnitak
Alnitak

Reputation: 339856

First, write a function that returns an asynchronous promise to give you a value for one dog:

function checkDog(name) {
    var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
    if(!pattern.test(name)) {
        return $.Deferred().resolve("invalid");
    } else {
        return $.post('/check-puppy-name', { name: name } )
         .then(function (response) {
            if (response === 'duplicate') {
                return 'duplicate';
            } else {
                return '';
            }
        });
    }
}

Then you can write one that handles multiple dogs, also returning a promise (which won't itself be resolved until every dog has been checked):

function checkDogs(array) {
    return $.when.apply($, array.map(checkDog));
}

Note that there's no DOM-related code yet. You can now write a function that gets the values from a bunch of DOM inputs and returns them in an array:

function getInputValues($selector) {
    return $selector.get().map(function(el) {
        return el.value;
    });
}

So now (on submit) you can check your two sets of inputs and then finally when both of these are available, you can examine the results and update the DOM:

$('#puppy_form').on('submit', function() {

    var bitch_names = getInputValues($('.check_bitch_name'));
    var dog_names = getInputValues($('.check_dog_name'));

    var bitch_promises = checkDogs(bitch_names);
    var dog_promises = checkDogs(dog_names);

    $.when(bitch_promises, dog_promises).then(function(bitch_errors, dog_errors) {
        // update the DOM based on the passed arrays
        ...
    });
});

Upvotes: 2

Stuart
Stuart

Reputation: 201

You could use the async lib to manage these requests and collect the results which will then be passed into the final callback where you can process them.

I haven't tried to run this code but hopefully it will get you close enough if not already there.

async.parallel({
    bitch_errors: function(callback) {
       var bitch_errors = [];

       async.forEachOf($('.check_bitch_name'), function(obj, i, cb) {
           // need to check each name for validity and duplication.
           var entered_bitch_name = obj.value;
           var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
           if(!pattern.test(entered_bitch_name)) {
               bitch_errors[i+1] = "invalid";
               cb();
           } else {
               // now to check for duplicates
               $.post('/check-puppy-name', { name: entered_bitch_name }
               ).done(function (response) {
                  if(response == 'duplicate') {
                     bitch_errors[i+1] = "duplicate";
                  } else {
                     bitch_errors[i+1] = "";
                 }
                 cb();
              });
          }
       }, function () {
           callback(null, bitch_errors);
       });
    },
    dog_errors: function(callback) {
       var dog_errors = [];

       async.forEachOf($('.check_dog_name'), function(obj, i, cb) {
           // need to check each name for validity and duplication.
           var entered_dog_name = obj.value;
           var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
           if(!pattern.test(entered_dog_name)) {
               dog_errors[i+1] = "invalid";
               cb();
           } else {
               // now to check for duplicates
               $.post('/check-puppy-name', { name: entered_dog_name }
               ).done(function (response) {
                   if(response == 'duplicate') {
                       dog_errors[i+1] = "duplicate";
                   } else {
                       dog_errors[i+1] = "";
                   }
                   cb();
              });
          }
       }, function () {
           callback(null, dog_errors);
       });
    }
}, function(err, results) {
    // you can now access your results like so

    if(count(results.bitch_errors) == 0 && count(results.dog_errors) == 0) {
    // ... rest of your code
});

Upvotes: 0

Johannes Stadler
Johannes Stadler

Reputation: 787

You are right, ajax calls are like their name says asynchronous. Therefor you can only rely on the .done function. A simple solution would be to initialize a counter variable at the beginning for bitches and dogs and in the according done function you decrement it until it reaches zero. Then, also in the done function, you put an if that calls validation of the error arrays. Here is UNTESTED code to show what I mean:

$('#puppy_form').on('submit', function() {

    /* 
       here you get the initial count for bitches and dogs 
    */
    var bitch_count = $('.check_bitch_name').length;
    var dog_count = $('.check_dog_name').length;

    var bitch_errors = [];
    var dog_errors = [];
    // NOTE: Bitch and dog names need to be checked differently so we know which error is assigned to which input
    $('.check_bitch_name').each( function(i, obj) {
        // need to check each name for validity and duplication.
        var entered_bitch_name = obj.value;
        var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
        if(!pattern.test(entered_bitch_name)) {
            bitch_errors[i+1] = "invalid";
        } else {
            // now to check for duplicates
            $.post('/check-puppy-name', { name: entered_bitch_name }
            ).done(function (response) {
                if(response == 'duplicate') {
                    bitch_errors[i+1] = "duplicate";
                } else {
                    bitch_errors[i+1] = "";
                }

                /*
                    now on every checked name you decrement the counter
                    and if both counters reach zero you can be sure you
                    checked all and only now you call your validation
                */
                bitch_count--;
                if(bitch_count === 0 && dog_count === 0) {
                    return validateErrors();
                }

            });
        }
    });
    $('.check_dog_name').each( function(i, obj) {
        // need to check each name for validity and duplication.
        var entered_dog_name = obj.value;
        var pattern = /^[a-zA-Z,.]+\s[a-zA-Z,.]+(\s[a-zA-Z,.]+){0,}$/;
        if(!pattern.test(entered_dog_name)) {
            dog_errors[i+1] = "invalid";
        } else {
            // now to check for duplicates
            $.post('/check-puppy-name', { name: entered_dog_name }
            ).done(function (response) {
                if(response == 'duplicate') {
                    dog_errors[i+1] = "duplicate";
                } else {
                    dog_errors[i+1] = "";
                }

                /*
                    same here
                */                    
                dog_count--;
                if(bitch_count === 0 && dog_count === 0) {
                    return validateErrors();
                }

            });
        }
    });
}

/*
    ...and finally all code that should be processed after the ajax calls
*/
function validateErrors() {
    if(count(bitch_errors) == 0 && count(dog_errors) == 0) {
        return true;
    }

    // loop through the errors and assign them to the correct input
    $.each( bitch_errors, function( key, value ) {
        if (value == "invalid") {
            $('input[name="bitch_name['+key+']"]').parent().addClass('has-error');
            $('input[name="bitch_name['+key+']"]').next('.error-message').html('Names must be at least two words, and only contain letters');
            return false;
        } else if(value == "duplicate") {
            $('input[name="bitch_name['+key+']"]').parent().addClass('has-error');
            $('input[name="bitch_name['+key+']"]').next('.error-message').html('Sorry, this name has already been taken');
            return false;
        }
    });
    $.each( dog_errors, function( key, value ) {
        if(value != "") {
            if (value == "invalid") {
                $('input[name="dog_name['+key+']"]').parent().addClass('has-error');
                $('input[name="dog_name['+key+']"]').next('.error-message').html('Names must be at least two words, and only contain letters');
                return false;
            } else if(value == "duplicate") {
                $('input[name="dog_name['+key+']"]').parent().addClass('has-error');
                $('input[name="dog_name['+key+']"]').next('.error-message').html('Sorry, this name has already been taken');
                return false;
            }
        }
    });

    return false;

});

Upvotes: 0

Related Questions