How do you refresh an HTML5 datalist using JavaScript? - javascript

I'm loading options into an HTML5 datalist element dynamically. However, the browser attempts to show the datalist before the options have loaded. This results in the list not being shown or sometimes a partial list being shown. Is there any way to refresh the list via JavaScript once the options have loaded?
HTML
<input type="text" id="ingredient" list="ingredients">
<datalist id="ingredients"></datalist>
JavaScript
$("#ingredient").on("keyup", function(event) {
var value = $(this).val();
$.ajax({
url: "/api/ingredients",
data: {search: value.length > 0 ? value + "*" : ""},
success: function(ingredients) {
$("#ingredients").empty();
for (var i in ingredients) {
$("<option/>").html(ingredients[i].name).appendTo("#ingredients");
}
// Trigger a refresh of the rendered datalist
}
});
});
Note: In Chrome and Opera, the entire list is only shown if the user clicks on the input after entering text. However, I'd like the entire list to appear as the user types. Firefox is not a problem, as it appears to refresh the list automatically when the options are updated.
UPDATE
I'm not sure this question has a satisfactory answer, as I believe this is simply a shortcoming of certain browsers. If a datalist is updated, the browser should refresh the list, but some browsers (including Chrome and Opera) simply do not do this. Hacks?

Quite a long time after question but I found a workaround for IE and Chrome (not tested on Opera and already OK for Firefox).
The solution is to focus the input at the end of success (or done) function like this :
$("#ingredient").on("keyup", function(event) {
var _this = $(this);
var value = _this.val();
$.ajax({
url: "/api/ingredients",
data: { search: value.length > 0 ? value + "*" : "" },
success: function(ingredients) {
$("#ingredients").empty();
for (var i in ingredients) {
$("<option/>").html(ingredients[i].name).appendTo("#ingredients");
}
// Trigger a refresh of the rendered datalist
// Workaround using focus()
_this.focus();
}
});
It works on Firefox, Chrome and IE 11+ (perhaps 10).

I had the same problem when updating datalist.
The new values would not show until new input event.
I tried every suggested solutions but nothing using Firefox and
updating datalist via AJAX.
However, I solved the problem (for simplicity, I'll use your example):
<input type="text" id="ingredient" list="ingredients" **autocomplete="off"**>
<datalist id="ingredients"></datalist>
$("#ingredient").on("**input**", function(event) { ....}
Autocomplete and input is the couple that solve my problems and it works with Chrome too.

You can probably eliminate the problem if you don't make AJAX request on every key stroke. You can try throttle technique using set/cleatTimeout to issue request after 500ms after the last char typed:
$("#ingredient").on("keyup", function(event) {
clearTimeout($(this).data('timeout'));
$(this).data('timeout', setTimeout($.proxy(function() {
var value = $(this).val();
$.ajax({
url: "/api/ingredients",
data: {search: value.length > 0 ? value + "*" : ""},
success: function(ingredients) {
$("#ingredients").empty();
for (var i = 0; i < ingredients.length; i++) {
$("<option/>").html(ingredients[i].name).appendTo("#ingredients");
}
}
});
}, this), 500));
});

Yoyo gave the correct solution, but here's a better way to structure your inserts into the DOM.
$("#ingredient").on("keyup", function(event) {
var _this = $(this);
var value = _this.val();
$.ajax({
url: "/api/ingredients",
data: { search: value.length > 0 ? value + "*" : "" },
success: function(ingredients) {
var options = ingredients.map(function(ingredient) {
var option = document.createElement('option');
option.value = ingredient.name;
return option;
});
$("#ingredients")
.empty()
.append(options);
// Trigger a refresh of the rendered datalist
// Workaround using focus()
_this.focus();
}
});
Less DOM manipulation
With this refinement, I'm only inserting into the DOM a single time per each successful callback. This cuts down on the browser needing to re-render, and will help improve any "blips" in the view.
Functional Programming and Less Idiomatic jQuery
Here we are using the Array.prototype.map to clean up some of the jQuery and make things a bit less idiomatic. You can see from the ECMA Chart that this function will work in all browsers you are targeting.
Not Hacky
This by no means is hacky. IE appears to be the only browser that doesn't automatically refresh the input to display the new list options. focus() is just a way to ensure the input is refocused which forces a refresh of the view.
This solution works very well in all of the browsers that my company has to support internally, IE10+ Chrome and Firefox.

Place your #ingredients element is inside #container and try this code:
$.ajax({
url: "/api/ingredients",
data: {search: value.length > 0 ? value + "*" : ""},
success: function(ingredients) {
$("#ingredients").remove();
var item = $('<datalist id="ingredients"></datalist>');
for (var i in ingredients) {
item.append("<option>"+ ingredients[i].name +"</option>");
}
item.appendTo('#container');
}
});
even better without #container and using jQuery replaceWith():
$.ajax({
url: "/api/ingredients",
data: {search: value.length > 0 ? value + "*" : ""},
success: function(ingredients) {
var item = $('<datalist id="ingredients"></datalist>');
for (var i in ingredients) {
item.append("<option>"+ ingredients[i].name +"</option>");
}
$("#ingredients").replaceWith(item);
}
});

Your issue is that the AJAX is asynchronous.
You'd actually have to have a callback for the AJAX which you call onSuccess which would then update the datalist. Of course, then you might not have great performance/still have a "skipping" behavior, where your datalist options are lagging behind.
If your list of items from the AJAX isn't too large, you should:
1. load the ENTIRE list into memory array with the first query, then...
1. use a filtering function that is applied to the array each time you have a keyUp event.

I found a solution tested only on GNOME Web (WebKit) that consist on set the 'list' attribute of the input element to empty string and, inmediately after, set it again with the id of the datalist element. Here is the example, supose that your input element is stored in a variable named input_element:
var datalist = document.getElementById(input_element.list.id);
// at this point input_element.list.id is equals to datalist.id
// ... update datalist element here
// And now the trick:
input_element.setAttribute('list','')
input_element.setAttribute('list',datalist.id)

Related

Knockout.js delay valueUpdate: afterkeydown

I have a search bar that data binds results in a grid using afterkeydown. However, the binding is happening too quickly. Users only have time for a single key press before the results start to populate. I have a Block UI element that prevents interaction with the page while results are loading, thus stopping the search query at a single character until results are loaded.
I know there's a knockout extender called rateLimit to delay the call until after a specified time period after changes stop, but I've been unable to see any difference when add it to the definition of 'searchTerm'. Is there another method I should be using?
I have provided my search box, the definition of the 'searchTerm' observable and where it's used to load my grid results:
<input data-bind="value: searchTerm, valueUpdate: 'afterkeydown'" />
Knockout:
var app = app || {};
app.data = #Html.Raw(Json.Encode(Model));
app.CreateVM = function (data) {
var vm = {};
vm.searchTerm = ko.observable(data.Search);
...
}
vm.filterResults = function () {
app.getResultsList(vm.selectedItem(), vm.searchTerm(), vm.currentPage() + 1, vm.sortDirection(), vm.sortProperty(), updateGrid);
}
app.getResultsList = function (Id, searchTerm, pageIndex, sortDirection, sortProperty, callBack) {
$.ajaxCall({
url: $('#clientGrid').data('url'),
type: 'GET',
dataType: 'json',
data: {
Id: Id,
pageIndex: pageIndex,
sortDirection: sortDirection,
sortProperty: sortProperty,
filter: searchTerm
},
...
}
You haven't posted the relevant code so I can't be sure, but I'm guessing the problem is that you're firing off updates manually every time rather than using the searchTerm observable's change events to trigger it. The knockout way to do this would be to data-bind your results grid to a computed observable.
Here's a jsFiddle demonstrating the rateLimit extension: fiddle
If you want to include some more code regarding what triggers your ajax call to update your grid we can probably help pinpoint the problem.

DOM Elements loaded from ajax and (probably) not ready in time

I'm using sorting/filtering jQuery plugin Isotope and also jQuery $.ajax() to dynamically load some new elements to the page that need to be sorted with Isotope. That library seems to set all the new sorted elements with absolute position and with fixed (left, top) position in order to perform sorting.
The problem is that when you load the first set of elements with clear cache the the element positions in that absolute grid are incorrect (they are overlapping). This is caused by Isotope initialization. My (inexperienced) guess would be that the all the new DOM elements are not fully loaded by the time Isotope starts to calculate the future positions of the elements and there's where the in-accuracy comes in. If I do the exact same ajax request again it manages to calculate the positions correctly.
EDIT #1 ajax reuqest
var $isocont = $('#page-content-result');
var isoActive = false;
$.ajax({
url: actionUrl + 'ajax',
type: 'POST',
data: searchData,
success:function (data) {
if(data.trim().length > 0) {
$('#page-content-result').hide().empty().html(data).fadeIn(600);
initIsotope();
} else {
var visible = $('#page-content-result').is(':visible');
if(visible === true)
{
$('#page-content-result').empty();
}
$('#result-notif').show();
}
}
});
var initIsotope = function() {
if(isoActive === true) {
$isocont.isotope('destroy');
console.log('iso stop');
isoActive = false;
}
if(isoActive === false) {
$isocont.isotope({
getSortData: {
name: '.iso-docname',
}
});
console.log('iso start');
isoActive = true;
}
}
Can someone explain the nature of this problem and give few hints for solution?
Thnx!
Seems that once again the "morning is smarter than the night" (it's a saying in Estonian :P).
I placed a piece of test-code after part where the new data was inserted to the DOM.
if (/complete/.test(document.readyState)) {
alert('loaded');
}
The alert kind of pauses the loading of the page and I realized that the containers where quite a lot shorter because of the not-yet-loaded images. Meaning the DOM doesn't have the information how high the containers are going to be when we initialize the Isotope and that's why the Isotope fails to calculate the correct positions.
FIX: After understanding the problem the fix is really simple just tell the DOM how high the image is going to be in CSS with min-height: 122px; and that is all. I guess if the image height varies then I believe setting an interval checker for document.readyState == 'loaded' would help.
Idea came from that blog http://callmenick.com/2014/06/04/check-everything-loaded-javascript/
So my general js file now looks something like that:
var $isocont;
$(document)(function() {
//When DOM create a var of selector.
$isocont = $('#page-content-result');
//One time aka first init of Isotope with basic options.
$isocont.isotope({
getSortData: {
name: '.iso-docname',
}
});
$.ajax({
url: actionUrl,
type: 'POST',
data: searchData,
success:function (data) {
if(data.trim().length > 0) {
//Set HTML data from ajax request
$('#page-content-result').hide().empty().html(data).fadeIn(600);
//Reload isotope items and re-arrange the items according to config.
reloadIsotope();
} else {
//DO SMTH
}
}
});
});
var reloadIsotope = function() {
$isocont.isotope('reloadItems');
//We are resetting sorting since we might not want same ordering as on last result.
$isocont.isotope({sortBy: null, sortAscending: true});
}
Thanx TimSPQR for looking into!

Making editable table work cross-browser

I've been working on an ASP.NET page containing a ListView. When the user clicks a row, the content of the last (visible) column of this (once parsed) HTML table is replaced with a textbox (by means of jQuery), making the value editable.
So far, this works like a charm in Chrome but no joy in IE10.
In this jsfiddle, the value becomes editable but then the Save button doesn't work as expected.
In IE the textbox doesn't appear. Funny detail: if I comment out the four vars (invNr, newInvNr, oldHtml and spanWidth), the input element DOES appear in IE10 but of course I have no data to work with. Really REALLY weird.
The jQuery:
$(document).ready(function () {
$('tr[id*="itemRow"]').click(function () {
$clickedRow = $(this);
//this makes sure the input field isn't emptied when clicked again
if ($clickedRow.find('input[id$="editInvNr"]').length > 0) {
return;
}
var invNr = $clickedRow.find('span[id$="InvoiceNumber"]').text(),
newInvNr = '',
oldHtml = $clickedRow.find('span[id$="InvoiceNumber"]').html(),
spanWidth = $clickedRow.find('span[id$="InvoiceNumber"]').width();
$clickedRow.find('span[id$="InvoiceNumber"]').parent('td').html('<input type="text" ID="editInvNr"></input>');
$clickedRow.find('input[id="editInvNr"]').val(invNr).focus().on('input propertychange', function () {
$clickedRow.find('span[id$="SaveResultMsg"]').hide();
$clickedRow.find('td[id$="SaveOption"]').show();
$clickedRow.find('input[id*="btnSaveInvNrFormat"]').show();
newInvNr = $(this).val();
if (newInvNr == $clickedRow.find('span[id$="InvoiceNumber"]').text()) {
$clickedRow.find('td[id$="SaveOption"]').hide();
}
});
});
$('tr[id*="itemRow"]').focusout(function () {
$rowLosingFocus = $(this);
var previousValue = $rowLosingFocus.find('input[id$="editInvNr"]').val();
$rowLosingFocus.find('input[id$="editInvNr"]').closest('td').html('<asp:Label ID="lblInvoiceNumber" runat="server" />');
$rowLosingFocus.find('span[id$="InvoiceNumber"]').text(previousValue);
});
});
function UpdateInvoiceNrFormat(leButton) {
$buttonClicked = $(leButton);
$buttonClicked.focus();
var companyName = $buttonClicked.closest('tr').find('span[id$="lblCompanyName"]').text(),
invoiceType = $buttonClicked.closest('tr').find('span[id$="lblInvoiceType"]').text(),
invNrFormat = $buttonClicked.closest('tr').find('span[id$="lblInvoiceNumber"]').text();
PageMethods.UpdateInvoiceNumberFormat(companyName, invoiceType, invNrFormat, onSuccess, onError);
function onSuccess(result) {
$buttonClicked.hide();
$buttonClicked.siblings('span[id$="SaveResultMsg"]').text(result).show();
}
function onError(result) {
$buttonClicked.hide();
$buttonClicked.siblings('span[id$="SaveResultMsg"]').text('Error:' + result).show();
}
}
I've tried various combinations of jQuery statements, chaining and avoiding chaining, placing it at the bottom of the page as someone suggested, commenting out various parts of the code out of sheer desperation. Still nada.
There was no way to make the html() method replace the html correctly in IE10, although I never did find out exactly why. I ended up writing both elements into the table cell, set style="display:none" for one of them and use show() / hide() and that's good enough for me (and apparently for IE10 as well).
For anyone encountering the same issue: this is a workaround, not a solution in the strictest sense.

Clearing select options with javascript on change

Before you -1 this for being such a simple question read further, because ive spent a while searching and cant figure it out. What i have is 5 select boxes. Each ones results depend on what is selected before it. There is no submit button. I just need the to change based on whats selected in the previous box and so far i have that. Oh and the results are being pulled from a database. The problem is when you select an option in lets say box 1, box 2 options appear, but when you go and select an option in box 1 again, they options are just stacked on the others. I need them to clear when its changed again. I'll post the js i have and if you need anything else just ask.
p.s. im getting the options using php-mysql.
p.s.s this site has to be finished by tomorrow so im kinda in a rush. Please dont waste time telling me this is a stupid question or what not.
Thanks in advance for the help.
$(function () { //When the page is finished loading run this code
$('#country').change(function () {
$.ajax({
url: "<!--Base url taken out-->/discovery/aLoad",
success: function (data) {
eval(data);
},
error: function () {
alert('failed');
},
data: {
country: getSelectValues('#country')
},
type: 'POST'
});
});
});
function changeSearchBar (newBar) {
//remove the crap
function ClearOptions(country)
{
document.getElementById('country').options.length = 0;
}
for (var i = 0; i <= newBar.length; i++) {
newBar[i].id = newBar[i][0];
newBar[i].name = newBar[i][1];
$('#group').append(
$('<option></option>').attr('value', newBar[i].id).text(newBar[i].name)
);
}
}
function getSelectValues (sId) {
var str = "";
$(sId +" option:selected").each(function () {
str += $(this).val() + ",";
});
return str.replace(/.$/g, '');
}
From what I understand from your long post you just need to remove all select child's before starting to append the new option from the AJAX request.
Try something like this before your for loop:
$('#group').html('');​
Demo: http://jsfiddle.net/57TYu/2/
Here you go, I think this is basically what you are trying to do:
How do you remove all the options of a select box and then add one option and select it with jQuery?
Basically, find the 'option' tags inside the select and remove them.
$('select').find('option').remove()
will clear all the select boxes.
This will append the data, which is assume are the options in their place:
$('select').find('option').remove().end().append(data);
I think that is what you are looking for.

ExtJS Change Event Listener failing to fire

I was asked to post this as a question on StackOverflow by http://twitter.com/jonathanjulian which was then retweeted by several other people. I already have an ugly solution, but am posting the original problem as requested.
So here's the back story. We have a massive database application that uses ExtJS exclusively for the client side view. We are using a GridPanel (Ext.grid.GridPanel) for the row view loaded from a remote store.
In each of our interfaces, we also have a FormPanel (Ext.form.FormPanel) displaying a form that allows a user to create or edit records from the GridPanel. The GridPanel columns are bound to the FormPanel form elements so that when a record is selected in the GridPanel, all of the values are populated in the form.
On each form, we have an input field for the table row ID (Primary Key) that is defined as such:
var editFormFields = [
{
fieldLabel: 'ID',
id: 'id_field',
name: 'id',
width: 100,
readOnly: true, // the user cannot change the ID, ever.
monitorValid: true
} /* other fields removed */
];
So, that is all fine and good. This works on all of our applications. When building a new interface, a requirement was made that we needed to use a third-party file storage API that provides an interface in the form of a small webpage that is loaded in an IFrame.
I placed the IFrame code inside of the html parameter of the FormPanel:
var editForm = new Ext.form.FormPanel({
html: '<div style="width:400px;"><iframe id="upload_iframe" src="no_upload.html" width="98%" height="300"></iframe></div>',
/* bunch of other parameters stripped for brevity */
});
So, whenever a user selects a record, I need to change the src attribute of the IFrame to the API URL of the service we are using. Something along the lines of http://uploadsite.com/upload?appname=whatever&id={$id_of_record_selected}
I initially went in to the id field (pasted above) and added a change listener.
var editFormFields = [
{
fieldLabel: 'ID',
id: 'id_field',
name: 'id',
width: 100,
readOnly: true, // the user cannot change the ID, ever.
monitorValid: true,
listeners: {
change: function(f,new_val) {
alert(new_val);
}
}
} /* other fields removed */
];
Nice and simple, except that it only worked when the user was focused on that form element. The rest of the time it failed to fire at all.
Frustrated that I was past a deadline and just needed it to work, I quickly implemented a decaying poller that checks the value. It's a horrible, ugly hack. But it works as expected.
I will paste my ugly dirty hack in an answer to this question.
"The GridPanel columns are bound to
the FormPanel form elements so that
when a record is selected in the
GridPanel, all of the values are
populated in the form."
As I understand it from the quote above, the rowclick event is what actually triggers the change to your form in the first place. To avoid polling, this could be the place to listen, and eventually raise to your custom change event.
Here is the ugly hack that I did to accomplish this problem:
var current_id_value = '';
var check_changes = function(offset) {
offset = offset || 100;
var id_value = document.getElementById('id_field').value || '';
if ( id_value && ( id_value != current_id_value ) ) {
current_id_value = id_value;
change_iframe(id_value);
} else {
offset = offset + 50;
if ( offset > 2500 ) {
offset = 2500;
}
setTimeout(function() { check_changes(offset); }, offset);
}
};
var change_iframe = function(id_value) {
if ( id_value ) {
document.getElementById('upload_iframe').src = 'http://api/upload.php?id=' + id_value;
} else {
document.getElementById('upload_iframe').src = 'no_upload.html';
}
setTimeout(function() { check_changes(100); }, 1500);
};
It's not pretty, but it works. All of the bosses are happy.
If you took a moment to read the source, you would see that the Ext.form.Field class only fires that change event in the onBlur function

Categories

Resources