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);
}
});
});
Here you go, a much cleaner example.
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
});
}
}
Building on top of @friek108's excellent answer, let's add the following features.
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).
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>
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With