5

I'm doing a webapp with html+jquery and a java rest-service backend. I have a textfield, with typeahead suggestions, so every character the user types in the field will trigger a server-round trip and update the list of typeahead suggestions.

Essential parts of the code:

    var showTypeaheadSuggestions = function(data) {
        // update ui-element ...
    }

    var displayFailure = function() {
        // update ui-element ...
    }

    var searchText = $("#searchText");
    var searchTextKeyup = function() {
        var txt = searchText.val();
        $.ajax({
            url : typeaheadUrl(txt),
            type : 'GET',
            dataType : 'json',
        }).done(showTypeaheadSuggestions).fail(displayFailure);
    }
    searchText.on('keyup', searchTextKeyup);

It's basically working. But I was thinking abt what happens if you type, for example, 2 letters "ab" (that will trigger first a request for "a" and then a request for "ab")...

Then, what happens if the "a" response takes a bit longer to process, and arrives after the "ab" response? Do I need to detect this in my code, to throw away the "a" response?

In http://api.jquery.com/jquery.ajax/ it does says:

Promise callbacks — .done(), .fail(), .always(), and .then() — are invoked, in the order they are registered.

What does that mean exactly? I was hoping this means $.ajax() would automatically handle the above scenario correct.

But when I do a small test (on the server-side I simply injected a 2 secs sleep-delay, only when the search-string is exactly "a"), it turns out it does not behave as I expected.

The typeahead list will first get updated with the "ab" response, and then when the "a" response arrives, it also updates, so the typeahead list gets the wrong suggestions.

What is the established way to handle this correctly?

Rop
  • 3,043
  • 3
  • 32
  • 54

4 Answers4

1

One of the ways I approached this problem was to assign an ID for each time you call it, and pass it as an ID to server side. When your server is done processing it, it then sends the result back along with it's id.

Then, every time the client side code runs, the ID will increment. For example:

var callback_id = 0;

var searchText = $("#searchText");
var searchTextKeyup = function() {
    callback_id ++;
    var txt = searchText.val();
    $.ajax({
        url : typeaheadUrl(txt),
        data : callback_id,
        type : 'GET',
        dataType : 'json',
    }).done(showTypeaheadSuggestions).fail(displayFailure);
}
searchText.on('keyup', searchTextKeyup);

Then, when you receive the response back, you check if the id is the current one. In the event that the user fires two events at once, your ajax event will be triggered twice, once with callback_id = 0, and one with callback_id = 1.

The last thing you have to then do is an if statement only updating your TypeaheadSuggestions if the callback_id is the most current one by comparing the id sent back from your server response.

David Li
  • 1,122
  • 6
  • 16
1

You must compare new input text with text you sent, and if it what user wants to find - you will show it, otherwise do nothing with response.

For example :

var searchText = $("input").val()
$.ajax({
        ....
        data: {searchText : searchText}
        success: funtion(){
        if($("input").val()==searchText){
           //SHOW RESULTS
        }
     }
})
  • This method is better than giving time id, because if you write "a", than "ab", than "a" and if your get answer on "a" it will show it (and you make 2 calls instead of 3 for time id). – Roman Romanovsky Oct 12 '15 at 00:43
1

There's another approach if you want to keep the server side code without changes. You can actually wrap the return functions inside a class and create instances for each request, then store the latest instance in a global scoped variable and check if the owner of the invoked method does match the latest instance:

var lastRequest;
var searchText = $("#searchText");

function requestClass()
{
    var that = this;

    this.showTypeaheadSuggestions = function(data) {
        //Update ui-element
        if (lastRequest == that)
            console.log('suggestions retrieved: ' + data);
        else
            console.log('this response (' + data + ') is ignored');
    };

    this.displayFailure = function() {
        //Update ui-element
        console.log('failure');
    };
}

var searchTextKeyup = function() {
    var request = new requestClass();
    lastRequest = request;

    var txt = searchText.val();
    $.ajax({
        url : typeaheadUrl(txt),
        type : 'GET',
        dataType : 'json',
    }).done(request.showTypeaheadSuggestions).fail(request.displayFailure);
}

searchText.on('keyup', searchTextKeyup);

I have tested this with the small-test you proposed in the question (adding a 2 seconds delay when the search string does match the 'a' character) and the result is the following:

suggestions retrieved: ab
this response (a) is ignored
Ivan De Paz Centeno
  • 2,815
  • 1
  • 14
  • 20
  • Interesting... though I'm struggling to grasp how it really works :) --- What does the expression "new requestClass()" actually evaluate to? ---- requestClass() is a call to function with no return stmt, so should then evaluate to (if I remember right) the last expression in the function... which here is what? :) – Rop Oct 12 '15 at 22:21
  • The `new` keyword instantiates an object, sets its constructor function to `requestClass()` and calls it. Since the function is not returning any result, the `new requestClass()` will return the reference to the instantiated object. Take a look [here](http://stackoverflow.com/questions/1646698/what-is-the-new-keyword-in-javascript#answer-3658673) to know more about the `new` keyword. – Ivan De Paz Centeno Oct 13 '15 at 00:27
1

The Promises interface returns to you a "Promise object" immediately so that you can use a different syntax for the callback.

Instead of:

asyncCall(callback);

You can use:

asyncCall()
  .then(callback);

And you can chain these:

authorizeAccount()
  .then(getNames)
  .then(processNames);

The callbacks will be executed in the order you register them -- processNames will wait for getNames to resolve first.

The "established" way to handle this "correctly" is probably to add some client-side debouncing so that only the whole request ('query' instead of 'q', 'qu' 'que'...) is processed when you are typing the word: http://underscorejs.org/#debounce with a timeout that expires a response if it takes too long to come back.

David Rosson
  • 844
  • 1
  • 8
  • 13