How to cancel asynchronous process in javascript? - 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;
}
});
},

Related

'No Data' view getting opens first and then Detail Page opens with the data in Fiori

I am developing a Master Detail application in which if the service URL doesn't return data, then a view called 'NoData' should open. But what actually is happening that first, the 'NoData' view opens and then the Detail Page with the data gets displayed. I don't know why and how that 'NoData' page is appearing first. Below is my code for Master Page :
Controller.js :
onInit: function () {
this.router = sap.ui.core.UIComponent.getRouterFor(this);
this._custTemp = this.getView().byId("listItemTemp").clone();
this.refreshFlag = true; // Flag to get new data or not for customers
this.totalModel = sap.ui.getCore().getModel("totalModel");
this.getView().setModel(this.totalModel, "totalModel");
this.oDataModel = sap.ui.getCore().getModel("DataModel");
this.getView().setModel(this.oDataModel, "DataModel");
this.oInitialLoadFinishedDeferred = jQuery.Deferred();
var oEventBus = sap.ui.getCore().getEventBus();
this.getView().byId("listId").attachEvent("updateFinished", function () {
this.oInitialLoadFinishedDeferred.resolve();
oEventBus.publish("MasterPage", "InitialLoadFinished", {
oListItem: this.getView().byId("listId").getItems()[0]
});
if (!sap.ui.Device.system.phone) {
this._getFirstItem();
}
}, this);
this.functionData = [];
},
waitForInitialListLoading: function (fnToExecute) {
jQuery.when(this.oInitialLoadFinishedDeferred).then(jQuery.proxy(fnToExecute, this));
},
_getFirstItem: function () {
sap.ui.core.BusyIndicator.show();
this.waitForInitialListLoading(function () {
// On the empty hash select the first item
var list = this.getView().byId("listId");
var selectedItem = list.getItems()[0];
if (selectedItem) {
list.setSelectedItem(selectedItem, true);
var data = list.getBinding("items").getContexts()[0];
sap.ui.getCore().getModel("detailModel").setData(data.getObject());
this.router.navTo('DetailPage', {
QueryNo: data.EICNO
});
sap.ui.core.BusyIndicator.hide();
} else {
this.router.navTo('NoData');
}
}, this);
},
onBeforeRendering: function () {
this._fnGetData();
},
_fnGetData: function (oEvent) {
var that = this;
this.getView().setModel(this.totalModel, "totalModel");
if (this.refreshFlag === true) {
sap.ui.core.BusyIndicator.show(0);
$.ajax({
url: "/sap/opu/odata/sap/ZHR_V_CARE_SRV/EmpQueryInitSet('10002001')?$expand=QueryLoginToQueryList/QueryToLog",
method: "GET",
dataType: "json",
success: function (data) {
that.getView().getModel("totalModel").setData(data.d.QueryLoginToQueryList);
that.refreshFlag = false;
sap.ui.core.BusyIndicator.hide();
that.statusList();
},
error: function (err) {
sap.ui.core.BusyIndicator.hide();
MessageBox.information(err.responseText + "Please try again");
}
});
}
}
totalModel is a json model, right? You'll get two updateFinished events on app load. The first one is triggered once the list control is rendered and binding is done (when the model has no data), and the second comes after your $.ajax call updates data to totalModel.
I think you can solve it by moving your NoData navigation to both 'success' and 'error' callbacks of your $.ajax call. Doing so may cover other use cases e.g. if you are using URL navigation parameters and a user changes the entity ID in the URL to some random number, it'd navigate to your NoDatapage.

knockout bind do not updating when observable has changed

I've a dropdown button which should be avaiable after ajax request will be finished.
<div class="form-input">
<label class="">Sort by:</label>
<select name="orderby" class="selectpicker" data-bind="value: sortBy, optionsCaption: 'Default', disable: waiting">
<option value="some_value">some_option</option>
<option value="some_value">some_option</option>
</select>
</div>
On page requested, it initially load data
$(function() {
//Initialization
var vm = new ArticleViewModel();
initialLoadArticles(vm);
ko.applyBindings(vm, $("#article-plugin")[0]);
});
function ArticleViewModel() {
var self = this;
//options =>
this.articles = ko.observableArray([]);
this.pageSize = 12;
this.sortBy = ko.observable('asc');
this.currentPage = ko.observable(1);
this.waiting = ko.observable(true);
this.totalPages = 0;
this.initMode = true;
this.timerId = null;
this.viewTemplate = ko.observable('listview-template');
if (this.viewTemplate() === "listview-template") {
this.pageSize = 4
} else {
this.pageSize = 12
};
this.sortBy.subscribe(function(event) {
console.log(event);
self.optionChanged();
loadArticles(self);
});
this.optionChanged = function() {
this.currentPage(1);
}
this.setCardView = function() {
self.viewTemplate('cardview-template');
loadArticles(self);
}
this.setListView = function() {
self.viewTemplate('listview-template');
loadArticles(self);
}
}
function initialLoadArticles(vm) {
vm.waiting(true);
var params = {
page: vm.currentPage(),
size: vm.pageSize,
sortby: vm.sortBy()
};
api.ajax.get(api.urls.article.getArticles, params, function(response) {
console.log('waiting: ' + vm.waiting());
if (response.success) {
vm.articles(response.data.items);
vm.waiting(false);
}
});
}
Well, on a page it display all articles, but dropdown button still blocked and I don't what exactly could be the problem of that.
I'd suggest a few changes to your viewmodel, featuring automatic loading via a subscription.
I think you always want to set waiting to false after loading, independent of whether the request was a success or not. Also think about low-level request errors, you need to add a handler for those.
function ArticleViewModel() {
var self = this;
self.articles = ko.observableArray();
self.pageSize = ko.observable();
self.sortBy = ko.observable('asc');
self.currentPage = ko.observable();
self.waiting = ko.observable(true);
self.viewTemplate = ko.observable();
// API
self.setCardView = function() {
self.viewTemplate('cardview-template');
self.pageSize(12);
self.currentPage(1);
};
self.setListView = function() {
self.viewTemplate('listview-template');
self.pageSize(4);
self.currentPage(1);
};
// compute Ajax-relevant parameters
self.ajaxParams = ko.pureComputed(function () {
return {
page: self.currentPage(),
size: self.pageSize(),
sortby: self.sortBy()
};
}).extend({ rateLimit: { timeout: 10, method: 'notifyWhenChangesStop' } });
// auto-load when params change
self.ajaxParams.subscribe(function (params) {
self.waiting(true);
api.ajax.get(api.urls.article.getArticles, params, function (response) {
if (response.success) {
self.articles(response.data.items);
}
self.waiting(false);
});
});
// set inital view (also triggers load)
self.setListView();
}
$(function() {
var vm = new ArticleViewModel();
ko.applyBindings(vm, $('#article-plugin')[0]);
});
More strictly speaking, you I'd advice against true or false as the "loading" indicator. It's technically possible that more than one Ajax request is running and this would be a race condition. The first request that comes back resets the "loading" state, and the next one still overwrites the viewmodel data. Either use a counter, or prevent new requests while there is a pending one.
The rateLimit extender makes sure that a rapid succession of changes to the parameters, like what happens when setListView() is called, does not cause multiple Ajax requests.
If your Ajax requests are done by jQuery internally, I would suggest the following setup to be able to make use of the done, fail and always promise handlers:
function ApiWrapper() {
var self = this;
function unwrapApiResponse(response) {
if (response.success) {
return new $.Deferred().resolve(response.data).promise();
} else {
return new $.Deferred().reject(response.error).promise();
}
}
self.getArticles = function (params) {
return $.get('articleUrl', params).then(unwrapApiResponse);
};
// more functions like this
}
var api = new ApiWrapper();
and in your viewmodel:
self.ajaxParams.subscribe(function (params) {
self.waiting(true);
api.getArticles(params).done(function (data) {
self.articles(data.items);
}).fail(function (err) {
// show error
}).always(function () {
self.waiting(false);
});
});

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?

History.js causes infinite loop after four pushStates

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() { ...}

modifying this code to run on window.load instead of on form submit

I'm pretty new to web development, and although I've wrapped my head around javascript im trying to modify some code i grabbed on github written in jquery for my own personal use. Basically right now the code is executed from an input form onsubmit however I'm planning on filling in the form data with php on my server before the code is sent to client to be executed so I would like to modify the code below to run on window.load instead. I've narrowed down what needs to be modified from a couple hundred lines of code to somewhere in the render: function(){ in code block below, but I am unsure on how to exactly do this and pass the input properly to the rest of the functions in the application
var InputView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'create');
this.template = _.template($('#input_template').html());
},
render: function() {
this.$el.html(this.template({}));
this.$el.find('form').submit(_.bind(function(event) {
window.location = '#' + this.$el.find('input').val();
}, this));
this.$el.find('#create').click(this.create);
this.$el.find('#create').on('click', this.create, this);
return this;
},
basically I just need to know how to properly bind this.$el.find('input').val(); to a php echo
... sorry for the outsourcing but I don't really understand exactly what is going on in all this code, so any help with this would be greatly appreciated
here is the rest of the InputView function just in case it's relevant but i don't think it is
create: function(event) {
if(this.$el.find('#create').hasClass('disabled')) return;
var button = this.$el.find('#create');
button.addClass('disabled');
event.preventDefault();
var product = 'Torque';
var btapp = new Btapp;
btapp.connect({
queries: [['btapp', 'create'], ['btapp', 'browseforfiles']],
product: product,
poll_frequency: 500
});
var status = new Backbone.Model({
btapp: btapp,
product: btapp.get('product'),
status: 'uninitialized'
});
var status = new StatusView({model: status});
$('.toolbox').append(status.render().el);
var browse_ready = function() {
btapp.off('add:bt:browseforfiles', browse_ready);
btapp.trigger('input:waiting_for_file_selection');
btapp.browseforfiles(function(files) {
var files = _.values(files);
if(files.length === 0) {
btapp.trigger('input:no_files_selected');
setTimeout(function() {
btapp.disconnect();
status.destroy();
button.removeClass('disabled');
}, 3000);
return;
}
var create_callback = function(data) {
btapp.disconnect();
btapp.trigger('input:torrent_created');
setTimeout(_.bind(btapp.trigger, btapp, 'input:redirecting'), 1000);
setTimeout(function() {
status.destroy();
window.location = '#' + data;
}, 3000);
}
var torrent_name = create_torrent_filename(files);
console.log('btapp.create(' + torrent_name + ', ' + JSON.stringify(files) + ')');
btapp.create(torrent_name, files, create_callback);
btapp.trigger('input:creating_torrent');
});
};
btapp.on('add:bt:browseforfiles', browse_ready);
btapp.on('all', _.bind(console.log, console));
}
});
so I've basically solved my own long winded question all i was looking for was a change of this line
this.$el.find('form').submit(_.bind(function(event) {
to
this.$el.ready(_.bind(function(event) {
anyways proably etter no one answered as it's started me to actually learn some jquery syntax something I've been putting of for to long now :)

Categories

Resources