I have a page on my site that displays sale listings, and users can either scroll down it as it is, or apply filters. If the user chooses not to apply any filters and just scroll down it as is, it works perfectly. When they choose to apply filters, I update the parameters of the infinite scroll instance so that it's loading more of the filtered results. Then, the very first time they scroll, the plugin tries to load 2 pages at once, then it crashes and unbinds itself.
Here's the function I'm using to apply the infinite scroll initially:
function initInfiniteScroll( $container, flag ){
//Initialize the plugin
console.log('Initializing infinite scroll');
$container.infinitescroll({
navSelector : "#paginationControl",
nextSelector : "#paginationControl a#next",
itemSelector : "#json_pen",
bufferPx : 200,
animate : false,
debug : true,
loading : {
img : "/includes/gif/loading.gif",
finishedMsg : "No more stuff.",
msgText : ""},
errorCallback : function(){
$("#loader.grid").fadeOut('normal');
}
},
function( entry_json ){
/* I'm loading a block of JSON to throw to Backbone instead of
grabbing html. */
console.log( 'Adding more stuff' );
//Parse the entries
var entries = JSON.parse( $(entry_json).html() );
//Throw them to the grid
Grid.addPage( entries );
});
//Mark it as having been applied
$container.addClass(flag);
}
And here is the code to update Infinite Scroll:
//Replace the pagination controls
$("#main #paginationControl").html( $("#listing_pen #paginationControl").html() );
var nextPage = $('#main #paginationControl #next').attr('href');
var basePath = nextPage.slice( 0, nextPage.length - 2 ); //Cut off the last character
//Overwrite path
$("#grid").infinitescroll({ state : { currPage : 1 },
path : [ basePath+'/', ' #json_pen' ] });
After this is called, as soon as the user tries to scroll, the next two pages try to load simultaneously and crash the plugin. I put in a few log statements to try to tell what's going on. Here's the console log:
loading /organize/10002/listings/all/10001/1001v/all/all/all
Updating the page scheme
initializing masonry
["math:", 1274, 1635]
in scroll calling the retrieve function
defined begin ajax
setting during ajax to true
["heading into ajax", Array[2]]
retrieving /organize/10002/listings/all/10001/1001v/all/all/all/2 #json_pen
["Using HTML via .load() method"]
["heading into ajax", Array[2]]
retrieving /organize/10002/listings/all/10001/1001v/all/all/all/3 #json_pen
["Using HTML via .load() method"]
["Error", "end"]
["Binding", "unbind"]
As far as I can tell, it looks like the scroll function in the plugin calls retrieve once, but retrieve calls beginAjax twice twice. Does anyone have any idea what could be going on here?
The issue wasn't caused by the plugin itself. When the opts.loading.start function is defined, it shows your loading spinner with a jQuery function call, then attaches a callback function which calls the beginAjax function to retrieve the next page of results. I modified this slightly, to:
opts.loading.start = opts.loading.start || function() {
$(opts.navSelector).hide();
$('#loader').show(opts.loading.speed, function () {
beginAjax(opts);
});
};
And it just so happened when I was grabbing the next page of results, I was also grabbing its loader. I didn't know that this was possible, but the $('#loader') was matching both the original loader and the one from the retrieved page, so the callback got executed twice, once for each loader. I added a :first to the selector and now it's working perfectly.
The way I figured this out was by riddling the retrieve function with console.log statements, then logging the scroll function the same way to find out that both were only being called once. From there it was relatively simple to figure out that it was the beginAjax function being called twice, and a quick look at the jQuery documentation explained why. Hopefully now this answer won't be completely useless to everyone else.
Related
I have an ajax call that builds a small graph in a popup window. The html for the link is re-used in many different links for different devices on the page. What happens, is that when you click a graph for the first device, you get that device. You click a button for the second device, you get that device, however, if you keep clicking away, after the third click or so, you suddenly start getting only the first device, over and over. I think my variables are being cached in some odd way, and I don't understand:
the HTML:
<a class="bluebtn graphbutton ingraph" href="http://wasat/cgi-bin/rrdjson.cgi?res=3600&start=-24h&end=now-1h&uid=28.7B2562040000" data-uid="28.7B2562040000" data-name="Laundry Room Freezer"></a>
<a class="bluebtn graphbutton ingraph" href="http://wasat/cgi-bin/rrdjson.cgi?res=3600&start=-24h&end=now-1h&uid=28.F7A962040000" data-uid="28.F7A962040000" data-name="Garage Temp"></a>
The code in question:
$(document).ready(function() {
$('.graphbutton').click(function(e) {
var formURL = $(this).attr("href");
var uid = $(this).data("uid");
var name = $(this).data("name");
e.preventDefault();
$.ajax({
url: formURL,
dataType: "json",
cache: false,
context: this,
success: function(data){
console.log("calling mkgraph with uid "+uid+" name " +name);
make_graph(data.data, uid, name);
},
error: function(ts) {
console.log(ts.responseText); }
});
}); /* clickfunc */
}); /*docready */
What happens:
Click freezer:
"calling mkgraph with uid 28.7B2562040000 name Laundry Room Freezer"
Click Garage:
"calling mkgraph with uid 28.F7A962040000 name Garage Temp"
Click Garage again:
"calling mkgraph with uid 28.7B2562040000 name Laundry Room Freezer"
Some of these links are being manufactured by the make_graph() function. I'm a bit worried that this is the issue, and somehow the ajax thing needs to be re-initialized after doing this?
By request, the relevant code in make_graph() that I think is causing my issue here. Basically, I'm editing the buttons in the css popup on the fly, and I think this is creating a wierd situation where the ajax binding is bound to the old href, and not being updated, even though the link is correct in the produced html. This is consistent with the effect where the binding only gets mangled on the third attempt.
$(".ingraph").each(function() {
this.href = $(this).attr("href").replace(/uid=.*/g, 'uid=' + uid);
this.setAttribute('data-uid' ,uid);
if (devname.length > 0) {
this.setAttribute('data-name', devname);
}
});
EDIT: adding a long answer:
I have multiple buttons on the main page. Each one specifies a "uid" that gets fed to rrdjson.cgi, which takes the uid and finds the data for that device, and returns it as json. When make_graph() recieves this json data, it populates a css popup, with the graph, and edits 5 buttons so they reference that UID. Those 5 buttons change the timescale of the graph by re-requesting the data from rrdjson.cgi.
What I am worried is happening, is that I click on the frige, it changes the uid's of the buttons inside the popup to reference the frige. Then I close that, click on the garage, it also changes the uid's and correctly shows the garage data. Then I click on one of the buttons inside the popup for the garage, and poof, I get the refrigerator again. I suspect that ajax "remembers" the old values for $(this).attr("href") etc and passes those values to the code, rather than re-reading the contents of the HTML. (perhaps instead of HTML, I meant DOM there, I'm a little vauge on the difference, but I suspect I meant DOM)
Maybe the answer is to somehow un-register the ajax binding to those buttons and re-register it every time make_graph() changes them? How would I do the un-register? .off() ? .unbind() ?
After much gnashing of teeth, and google, I have answered my own question.
https://forum.jquery.com/topic/jquery-data-caching-of-data-attributes
Turns out, jquery caches "data" types, but not attr types. So when I do:
uid = $(this).data("uid");
vs
uid = $(this).attr("data-uid");
I get wildly different results. I guess the moral of the story is that .data is super evil.. :)
If you add a random value to your url like
var formURL = $(this).attr("href")+"?rv="+Math.random();
you'll force the ajax call to reload the URL. You can use the cache property (set it to false) JQuery will load the data again, but any proxy may send a cached version.
(Please check that there are no other attributes set in the url, otherwise set "&rv="+Math.random(); (& instead of ?) use
var formURL = $(this).attr("href");
formURL + (formURL.indexOf("?") > 0 ? "&rv=" : "?rv=" )+ Math.random();
Your problem should not have something to do with make_graph() as uid and name depend on $('.graphbutton')
(if not make_graph(), or some other function, changes the attributes of your buttons)
I have two jQuery mobile pages (#list and #show). There are several items on the #list page with different IDs. If I click on item no.5, the ID no5 will be stored in localStorage and I will be redirected to page #show
Now the problem:
Storing the ID in localStorage works, but the next page shows me not the item no.5, but it shows me an old item, that was in the localStorage before.
script from page #list
localStorage.setItem("garageID", $(this).attr('id'));
window.location.replace("#show");
I encountered this problem too (and not on a mobile : on Chromium/linux).
As there doesn't seem to be a callback based API, I "fixed" it with a timeout which "prevents" the page to be closed before the setItem action is done :
localStorage.setItem(name, value);
setTimeout(function(){
// change location
}, 50);
A timeout of 0 might be enough but as I didn't find any specification (it's probably in the realm of bugs) and the problem isn't consistently reproduced I didn't take any chance. If you want you might test in a loop :
function setLocalStorageAndLeave(name, value, newLocation){
value = value.toString(); // to prevent infinite loops
localStorage.setItem(name, value);
(function one(){
if (localStorage.getItem(name) === value) {
window.location = newLocation;
} else {
setTimeout(one, 30);
}
})();
}
But I don't see how the fact that localStorage.getItem returns the right value would guarantee it's really written in a permanent way as there's no specification of the interruptable behavior, I don't know if the following part of the spec can be legitimately interpreted as meaning the browser is allowed to forget about dumping on disk when it leaves the page :
This specification does not require that the above methods wait until
the data has been physically written to disk. Only consistency in what
different scripts accessing the same underlying list of key/value
pairs see is required.
In your precise case, a solution might be to simply scroll to the element with that given name to avoid changing page.
Note on the presumed bug :
I didn't find nor fill any bug report as I find it hard to reproduce. In the cases I observed on Chromium/linux it happened with the delete operation.
Disclaimer: This solution isn't official and only tested for demo, not for production.
You can pass data between pages using $.mobile.changePage("target", { data: "anything" });. However, it only works when target is a URL (aka single page model).
Nevertheless, you still can pass data between pages - even if you're using Multi-page model - but you need to retrieve it manually.
When page is changed, it goes through several stages, one of them is pagebeforechange. That event carries two objects event and data. The latter object holds all details related to the page you're moving from and the page you're going to.
Since $.mobile.changePage() would ignore passed parameters on Multi-page model, you need to push your own property into data.options object through $.mobile.changePage("#", { options }) and then retrieve it when pagebeforechange is triggered. This way you won't need localstorage nor will you need callbacks or setTimeout.
Step one:
Pass data upon changing page. Use a unique property in order not to conflict with jQM ones. I have used stuff.
/* jQM <= v1.3.2 */
$.mobile.changePage("#page", { stuff: "id-123" });
/* jQM >= v1.4.0 */
$.mobile.pageContainer.pagecontainer("change", "#page", { stuff: "id-123" });
Step two:
Retrieve data when pagebeforechange is triggered on the page you're moving to, in your case #show.
$(document).on("pagebeforechange", function (event, data) {
/* check if page to be shown is #show */
if (data.toPage[0].id == "show") {
/* retrieve .stuff from data.options object */
var stuff = data.options.stuff;
/* returns id-123 */
console.log(stuff);
}
});
Demo
I have a form that I am trying to monitor to see when it becomes "Dirty" (changed), so that I can then enable a Save Changes button. It's fairly simple. In the $(document).ready() section, I enable dirtyForms on my form.
$(".dirtyForm").dirtyForms();
Then, I load the form from an ajax call, and inside that same function, I set it to clean and then start calling a function to check when it's dirty.
$(".dirtyForm").dirtyForms("setClean");
constant = setInterval(function(){checkDirty()}, 500);
Here is my function to check for when this becomes Dirty. Btw, if anyone knows of a better way to do this part, please let me know.
function checkDirty(){
if ($.DirtyForms.isDirty()){
$(".saveDirtyForm").removeAttr("disabled");
$(".resetDirtyForm").removeAttr("disabled");
console.log("Dirty...");
clearTimeout(constant);
}
}
You will notice that in the checkDirty function, it has clearTimeout(constant) to stop the function from continually repeating after the form becomes dirty. This all up to this point works fine. The buttons stay disabled until I change something and then almost immediately become enabled. The problem comes when I recall the ajax function to load the form with more info. When it resets the form, it gives an error when it's set to clean. It says,
Uncaught TypeError: Object function ( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init( selector, context, rootjQuery );
} has no method 'facebox'
This error is in the jquery.dirtyForms.js file. It doesn't set the form to clean and start the monitoring function again. Does anyone have an idea what I'm doing wrong?
Here is the function that zeroflagL was asking about.
function getOrderDets(id){
$.ajax({
url: DONEP+"blueBlob/donors/"+donorID+"/orders/"+id,
type: "GET",
dataType: "json",
success: function(data){
console.log(data);
dataSec = data.main.aResultData[0];
//Clear Fields
$("#orderIdTag").text('');
$("#orderNum").val('');
$("#doPlaced").val('');
$("#doShip").val('');
$("#dTrack").val('');
$("#doNote").val('');
//Set Main fields
$("#orderIdTag").text(dataSec.OrderID);
$("#orderNum").val(dataSec.OrderNumber);
$("#doPlaced").val(dataSec.OrderDate);
$("#doShip").val(dataSec.ShipDate);
$("#dTrack").val(dataSec.TrackingNumber);
$("#doNote").val(dataSec.OrderNote);
//Clean Dirty Form
$(".dirtyForm").dirtyForms("setClean");
constant = setInterval(function(){checkDirty()}, 500);
//Set Table
$(".orderDetTable").dataTable({
aaData: data.array,
"bAutoWidth": false,
"bDestroy": true,
"aoColumnDefs" : [
{"sWidth" : "13%", "sTitle" : "Quantity", "mData" : "Quantity", aTargets : [0]},
{"sTitle" : "Code", "mData" : "Code", aTargets : [1]},
{"sTitle" : "Amount", "mData" : "Amount", aTargets : [2]},
{"sWidth" : "6%", "sTitle" : "", "mData" : "OrderDetailsID", aTargets : [3], "mRender" : function(data, type, full){
return "<a href='#'><i class='glyphicon glyphicon-pencil orderDetEdit' id='"+data+"'></i></a>";
}}
]
});
}
});
}
Here is the stack trace for the facebox call.
$.facebox#http://dev.mysite.info/details.php?id=63#:540
.DirtyForms.dialog.fire#http://dev.mysite.info/assets/js/jquery.dirtyforms.js:25
bindFn#http://dev.mysite.info/assets/js/jquery.dirtyforms.js:421
aBindFn#http://dev.mysite.info/assets/js/jquery.dirtyforms.js:311
jQuery.event.dispatch#http://dev.mysite.info/assets/js/jquery.js:5095
jQuery.event.add/elemData.handle#http://dev.mysite.info/assets/js/jquery.js:4766
The first step is to call setClean after changing anything in the form including data tables.
If it's just the error, then here's a hack. This will get you by if you're under a deadline, and will also help you debug:
if ( typeof $.facebox !== 'function' )
{
$.facebox = function(){
var console = window['console'];
if ( console && console.error ){
console.error( 'Warning: $.facebox() was called', arguments );
}
return $();
}
}
Another tip: If you include the same jQuery library more than once, your plugins may not work.
To solve that, do a search for all instances of jquery in your codebase using this magic:
grep -o 'jquery:[^"]*"1\....' -R *
This searches for the jQuery.fn.version string.
You also need to check that the plugin is working. Try checking the following:
Is the plugin added after jQuery? - JQuery plugin not working
Does the plugin support your version of jQuery? - Why is the lightbox jQuery plugin not working for me?
Is more than one copy of jQuery included on the page? That can cause things to go crazy.
Read: jQuery in widget
EDIT: To get a stacktrace to see what's calling the facebox function:
$.facebox = function(){
alert( new Error().stack );
};
The correct way to enable/disable the buttons when the form is dirty/clean is now posted in the official documentation. Do note that this only works with Dirty Forms 2.x.
// Enable/disable the reset and submit buttons when the state transitions
// between dirty and clean. You will need to first set the initial button
// state to disabled (either in JavaScript or by setting the attributes in HTML).
$('form').find('[type="reset"],[type="submit"]').attr('disabled', 'disabled');
$('form').on('dirty.dirtyforms clean.dirtyforms', function (ev) {
var $form = $(ev.target);
var $submitResetButtons = $form.find('[type="reset"],[type="submit"]');
if (ev.type === 'dirty') {
$submitResetButtons.removeAttr('disabled');
} else {
$submitResetButtons.attr('disabled', 'disabled');
}
});
Also, it is pretty clear from your error message that you are using the default Dirty Forms 1.x behavior, which uses FaceBox as the dialog, but you don't have a reference in your page to FaceBox. You could fix that by adding the reference:
<script type="text/javascript" src="//cdn.jsdelivr.net/jquery.facebox/1.4.1/jquery.facebox.min.js"></script>
Alternatively, you can use any dialog you want by setting the $.DirtyForms.dialog property, or you can set it to false to use the browser's default dialog.
$.DirtyForms.dialog = false;
In Dirty Forms 2.x, false is now the default setting, so there is no need to add a reference to FaceBox.
I have a grid.Panel inside of a viewport that is binded to a store. Once the grid (or store) is loaded, I would like to look at a value in the first row (or any row), and if it's false, hide a column in the grid. I've tried many different events, but here's an example in my controller:
Ext.define('HelperBatchForm.controller.BatchController', {
extend: 'Ext.app.Controller',
stores: [
'Batches'
],
models: [
'Batch'
],
views: [
'batch.BatchGrid',
'batch.BatchEdit'
],
init: function () {
this.control({
'batchgrid': {
itemdblclick: this.editBatch
,viewready: this.onGridLoad
}
});
},
onGridLoad: function(grid){
stop;
},
"Stop" throws an error and opens the debugger in my IE browser. On the browser itself I can see the grid, and the rows, fully rendered. In the debugger, I can look at grid.store.data.items[0] and see the first row. So it seems that everything is well, and I should be able to put a condition in the function based on that data which hides the grid. But that doesn't work - here is where things start to get weird.
If I replace "stop;" with "debugger;", and reload, this time we get the visual studio debugger. But now, in the IE screen, I can only see the grid headers, and none of the data. And grid.store.data.items is an empty array. The instant I resume, I see the full grid.
But that's not all. If my function is:
onGridLoad: function (grid) {
alert('onGridLoad');
debugger;
},
Now, with the visual studio debugger loaded, I can see the full grid and data in IE. And grid.store.data.items[0] gives me the first row. If I replace "debugger" with my conditional code, it works! In other words, I have code that doesn't work, but suddenly starts working if I throw an alert() before it.
To summarize, the code below will hide the column:
onGridLoad: function (grid) {
alert('onGridLoad');
if (grid.store.findExact('is_rcm', false) >= 0) {
grid.columns[6].hide();
}
},
But if the alert is commented out, it will not hide the column.
Any ideas or explanations to why this might be would be greatly appreciated.
My guess the issue here is related to async loading of the store. You are probably seeing a race condition of a split second between the view is ready but the store is not yet populated. Just like in quantum physics the observation of the event is changing its outcome :)
My suggestion is to put a load listener on the store instead and inject your processing at that point.
I think that #dbrin is correct in assuming the data in the store is not yet loaded. But doing the processing on store load might also be problematic, when store load time is very fast, and the view is not yet ready. The following should work when the data is ready either after or before the view:
viewready: function(grid){
grid.getView().on({
refresh: {
fn: function(){
if (grid.store.findExact('is_rcm', false) >= 0) {
grid.columns[6].hide();
}
},
single: true
}
});
}
And here is a fiddle, where you can set the store load delay to test for different load times.
I use Ext.form.ComboBox in very similar way as in this example:
http://extjs.com/deploy/dev/examples/form/forum-search.html
What annoys me is that when the ajax call is in progress it shows loading text and I cannot see any results from before.
Eg I input 'test' -> it shows result -> I add 'e' (search string is 'teste') -> result dissapear and loading text is shown, so for a second I cannot see any result and think about if it's not what I'm searching for...
How can I change this to simply not to say anything when 'loading'...
The solution is to override 'onBeforeLoad' method of Ext.form.ComboBox:
Ext.override(Ext.form.ComboBox,
{ onBeforeLoad:
function() {this.selectedIndex = -1;}
});
Please be warned, that this overrides the class method, so all of the ComboBox instances will not have the LoadingText showing. In case you would like to override only one instance - please use plugins (in quite similar way).
You may also look at Ext.LoadingMask to set an appropriate loading mask to aside element if you wish.
If you don't show loading message to user how user will know what is happening? User will notice that its already loading results so may wait to see the results, but if nothing displayed then user wouldn't know if its bringing new data or not.
You may monit the expand event of the combobox and set picker loading to false.
// in the controller
init: function() {
this.control({
"form combobox[id=fieldId]": {
expand: function(combobox) {
combobox.getPicker().setLoading(false);
}
}
});
}