Q:

How to use materialize autocomplete plugin with ajax?

I am trying to make work together the materializecss autocomplete plugin with my ajax call in order to dynamically load data according to what is typed on the input field.
My ajax request is called inside a keydown event. All data fetched are automatically pushed into a key/value object array.
Then, i put the autocomplete function in the ajax's success function and the value of the key "data" is the object array built right before.
It's seems i am on the good way but when i am testing in the browser, each time i type something, the suggestion dropdown shows up as expected with results, but rather than be renewed after each keydown, another dropdown list overlap the previous one and so one...
So this is my problem : How to do to avoid the dropdown suggestion list to overlap and rather make it renew each time i press a key ?
Thank you for helping.

var dat = {};
$("input").on("keydown", function(e) {
    var d = {
        query: {
            prefix: {
                body: e.target.value
            }
        }
    };

    $.ajax({
        url: "https://xxxxxxxxxxxxxxx.eu-west-1.es.amazonaws.com/xxxxxxxxxxxxx",
        type: "POST",
        contentType: "application/json",
        crossDomain: true,
        data: JSON.stringify(d),
        dataType: "JSON",
        async: true,
        success: function(da) {
            var c = da.hits.hits.length;
            for (var i = 0; i < c; i++) {
                dat[da.hits.hits[i]._source.body] = null;
            }

            $("input").autocomplete({
                data: dat
            });
        },
        error: function(jqXHR, errorStatus, errorThrown) {
            console.log(jqXHR);
            console.log(errorStatus);
            console.log(errorThrown);
        }
    });
});

A:

Here you go, a much cleaner example.

It's based on the Materialized.js original function
Cancels existing ajax requests as you type so you don't get double results
If you remove the 'timeout' commented lines, this will only call an ajax call after 'x' time has elapsed after a keypress. Might be useful when you're typing fast to avoid ajax calls on every single key press (even though they get cancelled).

See below:
initAutoComplete({inputId:'autocomplete-input',ajaxUrl:'/search/my-auto-complete-results'})

function initAutoComplete(options)
{  
  var defaults = {
    inputId:null,
    ajaxUrl:false,    
    data: {}
  };

  options = $.extend(defaults, options);
  var $input = $("#"+options.inputId);

  if (options.ajaxUrl !== false)
  {
    var $autocomplete = $('<ul id="myId" class="autocomplete-content dropdown-content"></ul>'),   
        $inputDiv = $input.closest('.input-field'),
        //timeout,
        runningRequest = false,
        request;

    if ($inputDiv.length) {
      $inputDiv.append($autocomplete); // Set ul in body
    } else {      
      $input.after($autocomplete);
    }

    var highlight = function(string, $el) {
      var img = $el.find('img');
      var matchStart = $el.text().toLowerCase().indexOf("" + string.toLowerCase() + ""),
          matchEnd = matchStart + string.length - 1,
          beforeMatch = $el.text().slice(0, matchStart),
          matchText = $el.text().slice(matchStart, matchEnd + 1),
          afterMatch = $el.text().slice(matchEnd + 1);
      $el.html("<span>" + beforeMatch + "<span class='highlight'>" + matchText + "</span>" + afterMatch + "</span>");
      if (img.length) {
        $el.prepend(img);
      }
    };

    $autocomplete.on('click', 'li', function () {
      $input.val($(this).text().trim());
      $autocomplete.empty();
    });

    $input.on('keyup', function (e) {

      //if(timeout){ clearTimeout(timeout);}
      if(runningRequest) request.abort();      

      if (e.which === 13) {
        $autocomplete.find('li').first().click();
        return;
      }

      var val = $input.val().toLowerCase();
      $autocomplete.empty();

      //timeout = setTimeout(function() {

        runningRequest=true;

        request = $.ajax({
          type: 'GET', // your request type
          url: options.ajaxUrl,        
          success: function (data) {
            if (!$.isEmptyObject(data)) {
              // Check if the input isn't empty
              if (val !== '') {
                for(var key in data) {
                  if (data.hasOwnProperty(key) &&
                      key.toLowerCase().indexOf(val) !== -1 &&
                      key.toLowerCase() !== val) {
                    var autocompleteOption = $('<li></li>');
                    if(!!data[key]) {
                      autocompleteOption.append('<img src="'+ data[key] +'" class="right circle"><span>'+ key +'</span>');
                    } else {
                      autocompleteOption.append('<span>'+ key +'</span>');
                    }
                    $autocomplete.append(autocompleteOption);

                    highlight(val, autocompleteOption);
                  }
                }
              }
            }                    
          },
          complete:function(){
            runningRequest = false;
          }        
        });
      //},250);
    });
  }
  else 
  {
    $input.autocomplete({
      data: options.data
    });
  }
}

A:

Building on top of @friek108's excellent answer, let's add the following features.

Close the autocomplete widget when clicking outside it.
Allow scrolling results with arrow keys and selecting with enter key.
Make AJAX calls only after a predefined minimum number of characters are entered.
Stop certain keys from triggering AJAX calls.

This adopts the timeout & ajax call cancelling features from @friek108's answer. You might want to check it first.
ajaxAutoComplete({inputId:'autocomplete-input',ajaxUrl:'/search/my-auto-complete-results'})

function ajaxAutoComplete(options)
{

    var defaults = {
        inputId:null,
        ajaxUrl:false,    
        data: {},
        minLength: 3
    };

    options = $.extend(defaults, options);
    var $input = $("#" + options.inputId);

    if (options.ajaxUrl){

        var $autocomplete = $('<ul id="ac" class="autocomplete-content dropdown-content"'
            + 'style="position:absolute"></ul>'),
        $inputDiv = $input.closest('.input-field'),
        request,
        runningRequest = false,
        timeout,
        liSelected;

        if ($inputDiv.length) {
            $inputDiv.append($autocomplete); // Set ul in body
        } else {
            $input.after($autocomplete);
        }

        var highlight = function (string, match) {
            var matchStart = string.toLowerCase().indexOf("" + match.toLowerCase() + ""),
            matchEnd = matchStart + match.length - 1,
            beforeMatch = string.slice(0, matchStart),
            matchText = string.slice(matchStart, matchEnd + 1),
            afterMatch = string.slice(matchEnd + 1);
            string = "<span>" + beforeMatch + "<span class='highlight'>" + 
            matchText + "</span>" + afterMatch + "</span>";
            return string;

        };

        $autocomplete.on('click', 'li', function () {
            $input.val($(this).text().trim());
            $autocomplete.empty();
        });

        $input.on('keyup', function (e) {

            if (timeout) { // comment to remove timeout
                clearTimeout(timeout);
            }

            if (runningRequest) {
                request.abort();
            }

            if (e.which === 13) { // select element with enter key
                liSelected[0].click();
                return;
            }

            // scroll ul with arrow keys
            if (e.which === 40) {   // down arrow
                if (liSelected) {
                    liSelected.removeClass('selected');
                    next = liSelected.next();
                    if (next.length > 0) {
                        liSelected = next.addClass('selected');
                    } else {
                        liSelected = $autocomplete.find('li').eq(0).addClass('selected');
                    }
                } else {
                    liSelected = $autocomplete.find('li').eq(0).addClass('selected');
                }
                return; // stop new AJAX call
            } else if (e.which === 38) { // up arrow
                if (liSelected) {
                    liSelected.removeClass('selected');
                    next = liSelected.prev();
                    if (next.length > 0) {
                        liSelected = next.addClass('selected');
                    } else {
                        liSelected = $autocomplete.find('li').last().addClass('selected');
                    }
                } else {
                    liSelected = $autocomplete.find('li').last().addClass('selected');
                }
                return;
            } 

            // escape these keys
            if (e.which === 9 ||        // tab
                e.which === 16 ||       // shift
                e.which === 17 ||       // ctrl
                e.which === 18 ||       // alt
                e.which === 20 ||       // caps lock
                e.which === 35 ||       // end
                e.which === 36 ||       // home
                e.which === 37 ||       // left arrow
                e.which === 39) {       // right arrow
                return;
            } else if (e.which === 27) { // Esc. Close ul
                $autocomplete.empty();
                return;
            }

            var val = $input.val().toLowerCase();
            $autocomplete.empty();

            if (val.length > options.minLength) {

                timeout = setTimeout(function () { // comment this line to remove timeout
                    runningRequest = true;

                    request = $.ajax({
                        type: 'GET',
                        url: options.ajaxUrl + val,
                        success: function (data) {
                            if (!$.isEmptyObject(data)) { // (or other) check for empty result
                                var appendList = '';
                                for (var key in data) {
                                    if (data.hasOwnProperty(key)) {
                                        var li = '';
                                        if (!!data[key]) { // if image exists as in official docs
                                            li += '<li><img src="' + data[key] + '" class="left">';
                                            li += "<span>" + highlight(key, val) + "</span></li>";
                                        } else {
                                            li += '<li><span>' + highlight(key, val) + '</span></li>';
                                        }
                                        appendList += li;
                                    }
                                }
                                $autocomplete.append(appendList);
                            }else{
                                $autocomplete.append($('<li>No matches</li>'));
                            }
                        },
                        complete: function () {
                            runningRequest = false;
                        }
                    });
                }, 250);        // comment this line to remove timeout
            }
        });

        $(document).click(function () { // close ul if clicked outside
            if (!$(event.target).closest($autocomplete).length) {
                $autocomplete.empty();
            }
        });
    }
}

Instead of appending results to the autocomplete widget one by one, I've appended all of them together with one single, long string to make the process faster. (Read a wonderful analysis of jQuery .append() method here).

