History.js causes infinite loop after four pushStates - javascript

I'm learning History.js and I stumbled upon a problem. I use this function to load content into block:
loadPost: function(article) {
Functions.foldAllBlocks();
var url = article.attr('data-url'),
content = article.find('.entry'),
oldText = content.text(),
blockPosition = article.offset().top,
postTitle = article.find('h2').text();
if(article.hasClass('expand')) {
article.attr('data-entry',oldText);
$.get(url, function (data) {
article.addClass('collapse').removeClass('expand');
var entry = $(data).find('.entry > *');
content.html(entry);
Functions.orderBlocks();
Functions.maintainHistory(postTitle, url);
$('body, html').animate({
scrollTop: blockPosition
},200);
});
} else {
article.addClass('expand').removeClass('collapse');
content.text(article.attr('data-entry'));
Functions.orderBlocks();
Functions.maintainHistory(baseSitename, baseURL);
$('body, html').animate({
scrollTop: blockPosition
},500);
}
}
and this to maintain my history state:
maintainHistory: function(title, url){
(function(window, undefined) {
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState(),
cleanUrl = State.cleanUrl;
if(cleanUrl === baseURL) {
Functions.foldAllBlocks();
} else {
var article = $('h2 a[href="'+cleanUrl+'"]').closest('article');
Functions.loadPost(article);
}
});
History.pushState({ page: title }, title, url);
})(window);
}
There two functions are the only one using History.js. My problem is - every time, after fourth click (and thus, forth loadPost and maintainHistory firing) browser is getting stuck between last two articles.
What can I do to prevent this from happening?

Your maintainHistory function could use a minor rewrite. The way you have it now, the History.Adaptor will get stacked up with "statechange" handlers. Binding your event should only take place once, not every time you call the method. Try this:
maintainHistory: (function(window, undefined) {
console.log("Binding 'statechange' event - this should only happen once");
History.Adapter.bind(window, 'statechange', function() {
console.log("window.statechange event fired!");
var State = History.getState(),
cleanUrl = State.cleanUrl;
if(cleanUrl === baseURL) {
Functions.foldAllBlocks();
} else {
var article = $('h2 a[href="'+cleanUrl+'"]').closest('article');
Functions.loadPost(article);
}
});
// This is the function that is bound to maintainHistory
// The wrapping function is self-executing, will only happen once, and
// creates a nice little closure for binding the "statechange" event
return function(title, url) {
console.log("maintainHistory was called with title", title, "and url", url);
History.pushState({ page: title }, title, url);
};
})(window),
nextPropertyInFunctions: function() { ...}

Related

Scope of variable in DOJO when created from within function

In a DOJO widget there is code in the postCreate and destroy method to create/start and stop a timer like you can see below. Depending on the value in a drop down box the timer is started or stopped. This works fine so far.
postCreate: function() {
var deferred = this.own(<...some action...>)[0];
deferred.then(
lang.hitch(this, function(result) {
this.t = new dojox.timing.Timer(result.autoRefreshInterval * 1000);
this.t.onTick = lang.hitch(this, function() {
console.info("get new data");
});
this.t.onStart = function() {
console.info("starting timer");
};
this.t.onStop = function() {
console.info("timer stopped");
};
})
);
this.selectAutoRefresh.on("change", lang.hitch(this, function(value) {
if (value == "Automatic") {
this.t.start();
} else {
this.t.stop();
}
}));
},
When leaving the page the timer is still active so I want to stop it when I leave the page using DOJOs destroy() method.
destroy: function() {
this.t.stop();
},
This however throws a this.t.stop is not a function exception. It seems like this.t is not created in the context of the widget although I use lang.hitch(this...
What am I missing here?
I solved that by just renaming the variable t to refreshTimer. Maybe t is some kind of reserved variable in Dojo?

Scraping an infinite scroll page stops without scrolling

I am currently working with PhantomJS and CasperJS to scrape for links in a website. The site uses javascript to dynamically load results. The below snippet however is not getting me all the results the page contains. What I need is to scroll down to the bottom of the page, see if the spinner shows up (meaning there’s more content still to come), wait until the new content had loaded and then keep scrolling until no more new content was shown. Then store the links with class name .title in an array. Link to the webpage for scraping.
var casper = require('casper').create();
var urls = [];
function tryAndScroll(casper) {
casper.waitFor(function() {
this.page.scrollPosition = { top: this.page.scrollPosition["top"] + 4000, left: 0 };
return true;
}, function() {
var info = this.getElementInfo('.badge-post-grid-load-more');
if (info["visible"] == true) {
this.waitWhileVisible('.badge-post-grid-load-more', function () {
this.emit('results.loaded');
}, function () {
this.echo('next results not loaded');
}, 5000);
}
}, function() {
this.echo("Scrolling failed. Sorry.").exit();
}, 500);
}
casper.on('results.loaded', function () {
tryAndScroll(this);
});
casper.start('http://example.com/', function() {
this.waitUntilVisible('.title', function() {
tryAndScroll(this);
});
});
casper.then(function() {
casper.each(this.getElementsInfo('.title'), function(casper, element, j) {
var url = element["attributes"]["href"];
urls.push(url);
});
});
casper.run(function() {
this.echo(urls.length + ' links found:');
this.echo(urls.join('\n')).exit();
});
I've looked at the page. Your misconception is probably that you think the .badge-post-grid-load-more element vanishes as soon as the next elements are loaded. This is not the case. It doesn't change at all. You have to find another way to test whether new elements were put into the DOM.
You could for example retrieve the current number of elements and use waitFor to detect when the number changes.
function getNumberOfItems(casper) {
return casper.getElementsInfo(".listview .badge-grid-item").length;
}
function tryAndScroll(casper) {
casper.page.scrollPosition = { top: casper.page.scrollPosition["top"] + 4000, left: 0 };
var info = casper.getElementInfo('.badge-post-grid-load-more');
if (info.visible) {
var curItems = getNumberOfItems(casper);
casper.waitFor(function check(){
return curItems != getNumberOfItems(casper);
}, function then(){
tryAndScroll(this);
}, function onTimeout(){
this.echo("Timout reached");
}, 20000);
} else {
casper.echo("no more items");
}
}
I've also streamlined tryAndScroll a little. There were completely unnecessary functions: the first casper.waitFor wasn't waiting at all and because of that the onTimeout callback could never be invoked.

How to cancel asynchronous process in javascript?

I have a one-window javascript application. I have a dashboard that displays certain images by loading via multiple get requests in the background.
Problem arises when not all get requests are finished on time and the context of the site changes because then I want to clear the dashboard. Yet if the get request havent't finished yet, they will populate the dashboard with the wrong images.
I am trying to think of a way to abort those get request. Can someone please direct me in the right direction?
var Dashboard = {
showAllAssets: function(){
var self = this;
this.resetDashboard();
$.get(this.urlForAllAssets, function(json){
self.loadAssets(json);
});
},
showAssetsForCategory: function(categoryId) {
...
},
getHtmlForAsset: function(id) {
var self = this;
$.get(this.urlForDashboardThumb + "/" + id.toString(), function(assetHtml){
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
// this gets inserted even when context changed, how can I prevent that?
var thumb = Object.create(Thumbnail);
thumb.init($asset);
}, 'html')
},
insertAssetThumbIntoDom: function($asset) {
$asset.appendTo(this.$el);
},
resetDashboard: function() {
this.$el.html("");
},
loadAssets: function(idList) {
var self = this;
var time = 200;
// These get requests will pile up in the background
$.each(idList, function(){
var asset = this;
setTimeout(function(){
self.getHtmlForAsset(asset.id);
}, time);
time += 200;
});
},
bind: function() {
$document.on('loadAssets', function(event, idList) {
self.loadAssets(idList);
});
$document.on('switched_to_category', function(event, categoryId) {
self.showAssetsForCategory(categoryId);
});
$document.on('show_all_assets', function(){
self.showAllAssets();
})
},
init: function($el) {
this.$el = $el;
this.resetDashboard();
this.bind();
}
}
Though you cant stop an already sent request, you can still solve your problem.
My solution is to generate a simple ID, a random set of numbers for example, and store somewhere in your dashboard, and send it along with the request and send it back with the image.
If a new context is generated, it will have a new ID.
If the image comes back with a different ID than the one in the current context, then discard it.
As pointed out by the comments, a possible solution is to store the current context and compare it within the success method on the get request.
I have changed my code insofar that now I'll store the current within the manager and also I pass the event around to the $.get-method.
This has the downside that the get requests are still processed though and the loading of the new context takes longer as those get requests are processed later if there are too many to process. I also dislike passing the event around.
var Dashboard = {
currentLoadEvent: null,
loadAssets: function(idList, event) {
var self = this;
$.each(idList, function(){
var asset = this;
self.getHtmlForAsset(asset.id, event);
});
},
getHtmlForAsset: function(id, event) {
var self = this;
$.get(this.urlForDashboardThumb + "/" + id.toString(), function(assetHtml){
if (event === self.currentLoadEvent) {
console.log('same event continuing');
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
var thumb = Object.create(Thumbnail);
thumb.init($asset);
} else {
console.log('context changed');
}
}, 'html')
},
bind: function() {
var self = this;
$document.on('loadAssets', function(event, idList) {
self.currentLoadEvent = event;
self.loadAssets(idList, event);
});
$document.on('switched_to_category', function(event, categoryId) {
self.currentLoadEvent = event;
self.showAssetsForCategory(categoryId, event);
});
$document.on('show_all_assets', function(event){
self.currentLoadEvent = event;
self.showAllAssets(event);
})
}
}
I created a different solution by storing the request in an array and aborting them when the context changed:
loadAssets: function(idList, event) {
var self = this;
var requests = [];
$.each(idList, function(){
var asset = this;
if (self.currentLoadEvent === event){
var request = $.get(self.urlForDashboardThumb + "/" + asset.id.toString(), function(assetHtml){
if (event === self.currentLoadEvent) {
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
var thumb = Object.create(Thumbnail);
thumb.init($asset);
console.log('completed get request');
} else {
console.log('context changed');
$.each(requests, function(){
this.abort();
console.log('aborted request');
})
}
}, 'html');
requests.push(request);
} else {
return false;
}
});
},

Backbone events not firing after on demand loading element

I'm using backbone and lazy loading views in a single page application as I need them. However, it appears doing this seems to be confusing the way backbone knows what my 'el' is when setting up events. Using the view definition below, I'm trying to get the code that fires on the submit button click or the input fields changing but right now, neither appear to work.
$(document).ready(function () {
editaddressView = Backbone.View.extend({
elementReady: false,
initialize: function () {
this.model = window.AccountData;
this.listenTo(this.model, "change", this.render);
if ($('#section-editaddress').length == 0) {
// Load UI
$('#ajax-sections').prepend('<div class="section" id="section-editaddress" style="display: none;"></div>');
}
this.el = $('#section-editaddress');
},
events: {
"click #edit-address-submit": "beginSaving",
"change input": "updateModel",
"change select": "updateModel"
},
render: function () {
$(this.el).find("[name=address]").val(this.model.get('owner_address1'));
// ...
return this;
},
switchTo: function () {
// Set menu state
$('.js-NavItem').removeClass('active');
$('#sN-li-personal').addClass('active');
if (this.options.isPreLoaded)
this.elementReady = true;
if (this.elementReady) {
this.renderSwitch();
}
else {
var model = this;
$('#section-editaddress').load('/ajax/ui/editaddress', function (response, status, xhr) {
if (status == "error") {
$('#page-progress-container').fadeOut('fast', function () {
$('#page-load-error').fadeIn('fast');
});
} else {
$('#section-editaddress').find('.routedLink').click(function (e) {
window.Router.navigate($(this).attr('href'), true);
return false;
});
model.delegateEvents();
model.elementReady = true;
model.render(); // First render
model.renderSwitch();
}
});
}
},
renderSwitch: function () {
// Abort showing loading progress if possible
if (window.firstRunComplete) {
clearTimeout(window.pageHide);
// Change screen - Fade progress if needed
$('#page-progress-container').fadeOut('fast', function () {
$('#page-load-error').fadeOut('fast');
var sections = $(".section");
var numSections = sections.length;
var i = 0;
sections.hide('drop', { easing: 'easeInCubic', direction: 'left' }, 350, function () {
i++;
if (i == numSections) {
$('#section-editaddress').show('drop', { easing: 'easeInExpo', direction: 'right' }, 350).removeClass('hidden');
$.scrollTo($('#contentRegion'), 250, { margin: true });
}
});
});
}
// Switch complete
window.changingPage = false;
},
updateModel: function () {
var changedItems = {};
if (this.model.get('csrf') != $(this.el).find("[name=csrf]").val())
changedItems.csrf = $(this.el).find("[name=csrf]").val();
// ...
},
beginSaving: function () {
alert('test');
}
});
});
Can anyone see what I've missed?
Whenever you need to change or modify the DOM element of a BackboneJS view manually, you should use setElement rather than setting the property directly. It moves all of the event handlers to the newly attached DOM element and also sets the $el property. In addition, the function also detaches any existing event handlers.
So, in the code you pasted, you'd just change it to:
this.setElement($('#section-editaddress'));

reinitiate jquery plugin after ajax, causes events to fire multiple times

I have a query plugin I'm working on and I certain functions after ajax content has loaded. The problem is that let's say I re-initiate it 15 times, a click event will then fire 15 times when it's only clicked once.
Is there a way so it doesn't keep piling up? I'm calling addToCart onload and also from itemDetail after the ajax return
thanks!
function addToCart()
{
$(options.add_to_cart).click(function ()
{
event.preventDefault();
var id = $(this).attr('id');
store_item_id_val = id.replace('store-item-id-', '');
var quantity = $('.quantity-' + store_item_id_val);
if (quantity.val() < 1)
{
showError('Please enter a quantity of 1 or more.');
$(quantity).val(1);
return this;
}
$.post($(this).attr('href'),
{ store_item_id: store_item_id_val, quantity: quantity.val() },
function (data)
{
var result = jQuery.parseJSON(data);
renderCart(result);
});
});
return this;
}
function itemDetails()
{
$('.item-details').click(function ()
{
event.preventDefault();
var url = $(this).attr('href');
$.getJSON(url, function (result)
{
$('.modal-title').empty().html(result.title);
$('.modal-content').empty().html(result.html);
$('#modal').slideDown(100);
$('.ui-button').button();
addToCart();
$('.modal-close').click(function ()
{
event.preventDefault();
$('#modal').hide();
});
});
});
Based on the code you provided, I would probably say that you have some other code calling itemDetails(). Each time itemDetails() is called, it ADDS another event handler for click to your .item-details. You may want to instead do:
$(document).ready(function()
{
$('.item-details').click(function ()
{
event.preventDefault();
var url = $(this).attr('href');
$.getJSON(url, function (result)
{
$('.modal-title').empty().html(result.title);
$('.modal-content').empty().html(result.html);
$('#modal').slideDown(100);
$('.ui-button').button();
addToCart();
$('.modal-close').click(function ()
{
event.preventDefault();
$('#modal').hide();
});
});
});
});
This would put the event handler on your .item-details classed items, and only fire the events once. If you have dynamic .item-details added and removed you probably should use:
$('.item-details').live('click', function() ...

Categories

Resources