How to use materialize autocomplete plugin with aj

2019-05-04 18:30发布

问题:

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);
        }

       }) 
		

回答1:

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
    });
  }
}


回答2:

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).



回答3:

A little late to the party but thought this might help some people struggling with the same issue.

One way I found was to make a copy of the object returned by autocomplete() then using the built in data() function on the copy you iterate through results and add them in. A new copy is needed otherwise it just adds the extra values into the object (I'm sure there is some way to cleanup the object but all the usual methods failed for me).

var data = [{
  key : 'One'
}, {
  key : 'Two'
}, {
  key : 'Three'
}, {
  key: 'Twenty One'
}, {
  key: 'Thirty Five'
}, {
  key: 'Three Thousand'
}]; // Dummy data to emulate data from ajax request

function autocompleteData(field) {
  window.acSearch = $.ajax({
      url: 'somescript.php',
      type: 'GET',
      data: {
        key: function() {
          return $(field).val().trim();
        }
      },
      success: function(data) {
        $('.autocomplete-content').remove(); // Clear the old elements
        var newData = $.extend({}, $(field).autocomplete()); // Create copy of autocomplete object
        for (var i = 0; i < 20 && i < data.length; i++) {
          newData.data((data[i]["key"]), null); // Iterate through results and add to the copied autocomplete object (I set the limit to 20 as this is the limit I set below for the autocomplete)
      	}

        $(field).autocomplete({
        data: newData.data(),
        limit: 20, // Limit the number of results
      });
      $(field).keyup(); // This is just to get it to show the updated autocomplete results
    },
    error: function(){ // Ajax request will error as the URL is invalid so we will use the dummy data var created earlier and process the same function on error as we would on success - THIS IS NOT NEEDED (it's just for demonstrative purposed)
      $('.autocomplete-content').remove();
        var newData = $.extend({}, $(field).autocomplete());
        for (var i = 0; i < 20 && i < data.length; i++) {
          newData.data(data[i]["key"], null);
      	}

        $(field).autocomplete({
        data: newData.data(),
        limit: 20,
      });
      $(field).keyup();
    },
    complete: function(data) {
      setTimeout(function() {
        $(field).keyup()
      }, 250);

    }
  });
}

// Event handler on input field to trigger our function above and to clear any pending ajax requests
$('#autocompleteInput').on('input', function(e) {
  if (typeof acSearch != 'undefined') {
    acSearch.abort();
  }
  autocompleteData(this);
});
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/css/materialize.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/js/materialize.min.js"></script>
<head>
<body>
<div class="container">
  <label for="autocompleteInput">Example Autocomplete</label>
  <div class="input-field">
    <input id="autocompleteInput" class="autocomplete">
  </div>
</div>
</body>



回答4:

I was able to get https://github.com/devbridge/jQuery-Autocomplete working with Materialize CSS. It seems to be actively maintained and has some nice features such as client-side caching of searches and it's quite fast.

The implementation was simpler and smoother than the other libraries I tried.

HTML element:

<div class="row">
  <div class="input-field col s12">
    <i class="material-icons prefix">textsms</i>
    <input id="autocomplete-input" type="text" class="validate">
    <label for="autocomplete-input">Autocomplete</label>
  </div>
</div>

In a <script></script> element at the bottom of your HTML file:

$("#autocomplete-input").devbridgeAutocomplete({
  serviceUrl:"/api/url",
  // Called when an item is selected from the list of results
  onSelect: function (selected) {
    console.log('You selected: ' + selected.value + ', ' + selected.data);
  },
  showNoSuggestionNotice: true,
  noSuggestionNotice: 'Sorry, no matching results',
});

By default the request is a GET, the querystring is query=<value of input field>, and your API should return an array of data in a JSON object as the value of an element named suggestions:

{
  "suggestions": ["Java", "Javascript"]
}

Full documentation: https://www.devbridge.com/sourcery/components/jquery-autocomplete/



回答5:

Your autocomplete() function is missing the closing bracket for the option list object, the closing paren for the function itself, and the semicolon that needs to follow that. Try fixing those and see what you get:

    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
        });  <----HERE
    },

Ok. You've fixed the brackets, and still have the problem. First I'll say that it isn't clear to me why you are trying to reinitialize the autocomplete widget with new data every time the user presses a key. I would initialize it when I open the page, and then if there are changes to the data I would reinitialize it. The available selections aren't going to change as a consequence of the user pressing a key.

But that aside: looking at the doc, it's pretty sketchy, and you've done what it tells you to do. It doesn't address how to reinitialize an existing widget with new data. You probably have to find a way to remove the previous list before you reinitialize it. Here's something that they say about their Select widget:

If you want to update the items inside the select, just rerun the initialization code from above after editing the original select. Or you can destroy the material select with this function below, and create a new select altogether.

Maybe that applies to the autocomplete widget, too, and they just failed to document it. So, I'd try this first:

        $("input").autocomplete("destroy")
        .autocomplete({
            data : dat
        });

destroying the existing autocomplete and then reinitializing it with your new data. If that doesn't work (they don't say that they support a destroy() method for the autocomplete, but you never know until you try) then you'll need to dig down into the DOM, find the element that holds the data, and write code to remove it before you call the autocomplete() function. If that fails, then you might consider using the jquery-ui autocomplete, since a lot more people are using it than the one you're using and you can get better help.



回答6:

I worked around the issue doing this.

<script src="jquery/2.1.4/jquery.js" type="application/javascript"></script>
<script src="/js/default.js" type="application/javascript"></script>
<script src="materializecss/0.97.7/js/materialize.js" type="application/javascript"></script>

note the file "default.js" in the middle of the libraries.

then inside "default.js"

$.fn.alterAutocomplete = $.fn.autocomplete;

and wherever I needed to use the autocomplete plugin i did this.

 $('#autocomplete-input').alterAutocomplete ({
    source: function (request, response) {

    },
    response: function (event, ui) {
        if (ui.content.length <= 0) {
            ui.content.push({label: "No results"});
        }
    },
    select: function (event, ui) {
    }
});