wait for the function to finish before continuing to the next loop in a $.each loop

Question!

I'm not an expert in JS or jQuery by any means. I'm trying to write code for a chat app that does something like this:

  • Get conversation list from an AJAX call
  • Populate the left pane of the web page with the conversations returned by the API
  • Bind the chat list items to the right side of the pane after the left pane is populated with all the conversations.

Now the issue I'm having is this. After getting the conversation list from the API call, I iterate over the list and bind each item to the pane. .each function goes to the next iteration before the bind function is done executing it's code block. When the function that has the $.each iterator is done executing, I use ".done()' to populate the chat history of the first conversation item. But since the loop is finished before all conversations are binded properly, the chat list is populated and appears before I can see any conversations. Because of this, I can't bind any events to the conversation.

What I'm trying to achieve is to wait for the bind conversation to finish executing and then continue with the iteration.

My code is as below:

function (data) {
                    $.each(data, function (index, value) {

                        if (value.toUser == loggeduser) {
                            var isMe = false;
                            getUser(value.fromUserID, function (user) {
                                if (user.picturename == 'none') {
                                    bindConversationItem(value.chatMessages[0].message, user.username, 'nopic.png', value.conversationID);
                                }
                                else {
                                    bindConversationItem(value.chatMessages[0].message, user.username, user.picturename, value.conversationID);
                                }
                            });
                        }
                        else {
                            getUser(value.toUserID, function (user) {
                                var isMe = true;
                                if (user.picturename == 'none') {
                                    bindConversationItem(value.chatMessages[0].message, user.username, 'nopic.png', value.conversationID);
                                }
                                else {
                                    bindConversationItem(value.chatMessages[0].message, user.username, user.picturename, value.conversationID);
                                }

                            });
                        }
                    });
                }).done(function (data) {
                    populateFirstConversation(data);
                    bindEvents();

                });

Here's my bindConversationItem function

   function bindConversationItem(message, username, userimage, index) {
            var conv_item = '<li> <a href="#" conversation-index = '+index+' class="clearfix">'
                            + '<img src="http://localhost:1995/contents/member/' + userimage + '" alt="" class="img-circle">'
                            + '<div class="friend-name">'
                                + '<strong>' + username + '</strong>'
                            + '</div>'
                            + '<div class="last-message text-muted">' + message + '</div>'
                            + '<small class="time text-muted">Just now</small>'
                            + '<small class="chat-alert label label-danger">1</small>'
                        + '</a>'
                    + '</li>'


            $("ul.friend-list").append(conv_item);
        }

I tried wrapping the code inside a function and using a callback function but I can't seem to get it to work. Any solution here is appreciated.

Edit

I have the function "getUser' that is async in the for each loop. Here's what it looks like:

            function getUser(userid, callback) {
            $.ajax({
                url: 'http://localhost:1995//api/member/' + userid
            }).done(callback)
        }


Answers

You can use .queue(), $.map(), .promise() to return results from asynchronous calls in sequential order, perform tasks when all functions in queueName have completed

function processQueue (data) {
  return $({}).queue("ajax", $.map(data, function(id) {
    return function(next) {
      return getUser(id, function(val) {
        // do stuff when asynchronous function returns a value
        return $("<div>", {html:"id:" + id + ", value:" + val * Math.PI})
        .appendTo("body").after("<br>"); 
      }, next) // pass `next` function 
    }
  })).dequeue("ajax").promise("ajax")
}

function getUser(value, callback, next) {
  // do asynchronous stuff, e.g., `$.ajax()`
  return new $.Deferred(function(dfd) {
    setTimeout(function() {
      dfd.resolve(value * 10)
    }, Math.floor(Math.random() * 1500))
  })
  // chain `.then()` which calls `callback`, and `.then()`
  // which call next function in `"ajax"` queue
  .then(callback).then(next)
}

var arr = [1, 2, 3, 4, 5];

var queue = processQueue(arr);

// do stuff when queue completes
queue.then(function(data) {
  console.log("complete")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
</script>



The best solution of course is to use promises, but as you have reported you do not know very well about the language, this solution does not impact much your original code:

Instead $.each loop, execute every iteration at a time, then call the next when the callback fires.

function (data) {
    function done() {
        populateFirstConversation(data);
        bindEvents();
    }

    function getNextConversation(index) {
        if (index == data.length)
            return done();

        var value = data[index];

        if (value.toUser == ...) {
            getUser(..., function () {
                ...

                getNextConversation(index + 1);
            });
        } else {
           getUser(..., function () {
                ...

                getNextConversation(index + 1);
           });
        }
    }

    getNextConversation(0);
}


Nothing seems to have changed because I am able to achieve it even using Swift 3 as shown below.

import UIKit

class ViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()

        let temp: UIDatePicker = UIDatePicker(frame: self.view.frame)
        temp.setValue(UIColor.purple, forKey: "textColor")

        view.addSubview(temp)
    }

    override func didReceiveMemoryWarning()
    {
        super.didReceiveMemoryWarning()
    }
}


This video can help you solving your question :)
By: admin