DRY Then and Always in jQuery Promise - javascript

I have the following example JS code:
function loadData(url, data) {
return $.get(url, data ? data : {});
}
function example1() {
showSpinner();
$.when(loadData('/example1', { hello: 'world' }))
.then(function (resp) {
// do something with the data
})
.always(function(){
hideSpinner();
})
.fail(function (jqXHR, textStatus, errorThrown) {
handleError(jqXHR);
});
}
function example2() {
showSpinner();
$.when(loadData('/example2', { hello: 'world' }))
.then(function (resp) {
// do something with the data
})
.then(function () {
// do something else
})
.always(function(){
hideSpinner();
})
.fail(function (jqXHR, textStatus, errorThrown) {
handleError(jqXHR);
});
}
Both example1() and example2() use the same promise logic and will do something different with the returned data and then always hide the spinner and handle the failures the same (if any). The issue is that I may want to do different things after the data is loaded in each example and have multiple then's follow.
However I am having to repeat the always and fail code. How can I make them more DRY so they are only written once but used in both scenarios (and other examples if need be). So I can't just move the when into a method and pass a callback as there maybe more than one and in different then`s. So this wouldn't work:
function customPromise(load, callback) {
showSpinner();
$.when(load)
.then(function (resp) {
callback(resp);
})
.always(function(){
hideSpinner();
})
.fail(function (jqXHR, textStatus, errorThrown) {
handleError(jqXHR);
});
}
function example3() {
customPromise(loadData('/example2', { hello: 'world' }));
}

If you have successive and unknown number of callbacks, I suggest you move the whole processing to the created function:
function handleRequest(url, data, errorCallback) {
// create promise
let promise = $.get(url, data ? data : {});
// if callback functions provided as args, chain them in `then()` calls
if (arguments.length > 3) {
let callbacks = Array.slice.call(arguments, 3);
let fn = (results) => (results);
let callback;
for (let i = 0; i < callbacks.length; i++) {
callback = callbacks[i] instanceof Function ? callbacks[i] : fn;
promise.then(callback);
}
}
// handle static behavior
promise.always(() => {
hideSpinner();
})
.fail(errorCallback);
// return the created promise
return promise;
}
You can now use this function as follows:
handleRequest(
'/example',
{},
(jqXHR, textStatus, errorThrown) => {
handleError(jqXHR);
},
(resp) => {
// do something with the data
},
() => {
// do something else
}
);

I could think of something like this. Not sure if loadData should be held responsible for showing the spinner.
$(function() {
example1();
example2();
});
function loadData(url, data) {
showSpinner();
return $.get(url, data ? data : {})
.always(function() {
hideSpinner();
})
.fail(function(jqXHR, textStatus, errorThrown) {
handleError(jqXHR);
});
}
function example1() {
$.when(loadData('https://jsonplaceholder.typicode.com/users', {
hello: 'world'
}))
.then(function(resp) {
console.log(resp.length + ' lengthed data received');
});
}
function example2() {
$.when(loadData('https://jsonplaceholder.typicode.com/posts', {
hello: 'world'
}))
.then(function(resp) {
return resp;
})
.then(function(data) {
console.log(data.length + ' lengthed data received');
});
}
function showSpinner() {
console.log('Showing spinner...');
}
function hideSpinner() {
console.log('Hiding spinner...');
}
function handleError(xhr) {
console.error(xhr);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Perhaps you can factor the .when() out into a function returning a promise itself, then call that and chain onto the returned promise with .then(), so something like foobar().then().then(). There's no law stating that always or fail must come last in the chain.
This is a rough sketch for a solution, I have not tested this.
function loadData(url, data) {
return $.get(url, data ? data : {});
}
function wrapWhen(endpoint, data) {
// return a promise
return $.when(loadData(endpoint, data))
.always(function(){
hideSpinner();
})
.fail(function (jqXHR, textStatus, errorThrown) {
handleError(jqXHR);
});
}
function example1() {
showSpinner();
wrapWhen('/example1', {hello: 'world'})
.then(function (resp) {
// do something with the data
});
}
function example2() {
showSpinner();
wrapWhen('/example2', {hello: 'world'})
.then(function (resp) {
// do something with the data
})
.then(function () {
// do something else
});
}

Related

How To: correctly chain AngularJS async calls (AngularJs 1.7.5)

Recently started thinking that it was time to do a massive update to my logical operations, and part of that is the proper chaining of Angular JS asynchronous promise calls. Given the following code, how would I re-write it to be a proper chaining of two separate methods? (Yes, I've seen other posts about this, but they all deal with other versions of Angular, or other syntaxes, and I'm looking for something more up-to-date.)
vm.functionName = (
function() {
vm.processing = true;
api.promise1({ Id: vm.Id })
.then(
function(result) {
if (result.error) {
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result.error));
} else {
api.promise2({ param: vm.param })
.then(
function(result2) {
if (result2.error) {
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result2.error));
} else {
vm.data = result2.data;
notificationService.success("<h5>Operation successful!.</h5>");
}
vm.processing = false;
}
)
.catch(
function (err) {
console.error(err);
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
vm.processing = false;
}
);
}
}
)
.catch(
function (err) {
console.error(err);
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
vm.processing = false;
}
);
}
);
Logically, my brain tells me that I should be able to do something like this:
vm.functionName = (
function() {
vm.processing = true;
vm.promise1()
.then(
vm.promise2()
.then(
notificationService.success("<h5>Operation successful!.</h5>");
vm.processing = false;
);
);
);
}
);
vm.promise1 = (
function() {
api.promise1({ Id: vm.Id })
.then(
function(result) {
if (result.error) {
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result.error));
}
}
)
.catch(
function (err) {
console.error(err);
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
}
);
}
);
vm.promise2 = (
function() {
api.promise2({ param: vm.param })
.then(
function(result) {
if (result.error) {
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result.error));
} else {
vm.data = result2.data;
}
}
)
.catch(
function (err) {
console.error(err);
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
}
);
}
);
Update:
the "api...." calls above call to my service.js layer, where methods exist like such:
promise1: function (params, error) {
return $http
.post("/C#Controller/Method1", params)
.then(handleSuccess)
.catch(function (e) {
handleError(e, error);
});
},
promise2: function (params, error) {
return $http
.post("/C#Controller/Method2", params)
.then(handleSuccess)
.catch(function (e) {
handleError(e, error);
});
},
Updated, per Pop-A-Stash's ideas, as now implemented:
//#region Api Calls and Helper
function apiCallOne() {
return api.promise1({ Id: vm.Id });
}
function apiCallTwo() {
return api.promise2({param: vm.param });
}
function handleApiCallError(resultOrError, ngModelToSet) {
var errMsg = resultOrError.statusText === undefined ? resultOrError.error === undefined ? "Unknown Error" : resultOrError.error : resultOrError.statusText;
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(errMsg));
//This allows updating things like variables and ng-model goodies, via an inset function.
if (ngModelToSet) {
ngModelToSet();
}
}
//#endregion
//#region Initialization
function init() {
vm.pgLoaded = false;
apiCallOne()
.then(
function(result) {
if (!result.error) {
vm.data = result.data;
vm.pgLoaded = true;
} else {
handleApiCallError(result, function() { vm.pgLoaded = true; });
}
}
)
.catch(function(errorOne) { handleApiCallError(errorOne, function() { vm.pgLoaded = true; }); });
}
init();
//#endregion
You could shorten your code significantly using recursion to call the next promise in an array of objects containing promises and their parameters using something similar to this:
function runPromises(promises) {
var first = promises.shift();
first.function(first.params).then(function(resp) {
if (promises.length > 1) {
runPromises(promises);
}
}).catch(function (error) {
handleError(error);
});
}
with an initial array of something like this:
var promises = [
{
function: promise1,
params: any
},
{
function: promise2,
params: any
}
];
If each promise response requires individual handling you could add a callback to be fired after the promise is resolved for each promise.
If you want to chain them in a specific order, then you are already doing it correctly. However I see some code duplication that could be cleaned up:
vm.apiCallOne = apiCallOne;
vm.apiCallTwo = apiCallTwo;
vm.runChainedCalls = runChainedCalls;
function runChainedCalls() {
vm.processing = true;
vm.apiCallOne()
.then(function(result1) {
if(!result1.error) {
vm.apiCallTwo().then(function(result2) {
if(!result2.error) {
notificationService.success("<h5>Operation successful!.</h5>");
vm.data = result2.data;
vm.processing = false;
}
else {
handleError(result2);
}
})
.catch(function(errorTwo) {
handleError(errorTwo)
});
}
else {
handleError(result1);
}
})
.catch(function(errorOne) {
handleError(errorOne);
});
}
function apiCallOne(){
return api.callOne(param); //don't forget to return the promise object
};
function apiCallTwo() {
return api.callTwo(param); //don't forget to return the promise object
};
function handleError(resultOrError) {
notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(resultOrError.statusText));
vm.processing = false;
}
You only need one .then() and .catch() for each call in your controller. Anymore than that is code duplication.
If you want to run them concurrently and don't care about order, you would use the $q.all() function to run them at the same time:
function runConcurrentCalls() {
$q.all([api.callOne(param), api.callTwo(param)]).then(function(responseArray) {
// responseArray contains array of both call responses
console.log(responseArray[0]);
console.log(responseArray[1]);
})
.catch(function() {
//something went wrong with one or both calls
});
}

Using promises with AngularJS

I have this service
service.getCurrentUser = function () {
var def = $q.defer(); 
if (service.user == null)
{
$http.get("./api/GetCurrentUser/")
.success(function(data) { 
service.user=data; 
def.resolve(data); 
}) 
.error(function() { 
def.reject("Failed to get user"); 
}); 
}
else
def.resolve(service.user);
return def.promise;
} 
in my controller I want to call this and wait for return then if the user is in a certain group run other code
How do I write it so it uses the promise returned by the service
The promise implementation can be like:
service.getCurrentUser = function () {
return new Promise((resolve,reject)=>{
if (service.user == null) {
$http.get("./api/GetCurrentUser/")
.success(function (data) {
service.user = data;
resolve(data);
})
.error(function () {
reject("Failed to get user");
});
}else{
resolve(service.user);
}
});
}
You can call it as:
service.getCurrentUser()
.then(user => {
console.log('user', user);
})
.catch(error => {
console.log('error', error);
});

Close modal in ajaxStop

I have the following code:
var menuButtonClick = {
onReady: function () {
$(document).on('click', '.menu-button', function () {
menuButtonClick.clickedButton($(this).html());
});
},
clickedButton: function (val) {
switch (val) {
case 'CheckModelBank':
modelBankHandler.get();
break;
}
}
}
var modelBankHandler = (function () {
var get = function () {
var selectedCellData = handsonTable.selectedCellData.get();
var webGrid = handsonTable.WebGrid.get();
$.ajax({
type: 'POST',
url: "http://localhost:56292/api/Data/CheckModelBank",
data: { "": selectedCellData },
success: function (response) {
if (response != null) {
serverResult = JSON.parse(response);
printModelBank(serverResult, webGrid);
}
},
error: function (jqXHR, textStatus, errorThrown) {
if (textStatus == "error") {
modalHandler.printErrorModal();
}
}
});
}
var printModelBank = function (serverResult, webGrid) {
///
}
return {
get: get
}
})();
var fileHandler = {
onReady: function () {
var documentType = "";
$('.upload-file').click(function () {
$('[data-remodal-id=documentModal]').remodal().open();
});
$('.document-option').click(function () {
//Need to get the type of document the user is going to work with so we can parse the document correctly to the webgrid
documentType = $(this).html();
$('#fileUpload').click();
});
$('#fileUpload').change(function () {
fileHandler.upload(documentType);
});
$('.save-to-excell').click(fileHandler.saveDocument);
},
upload: function (documentType) {
var formData = new FormData();
var totalFiles = document.getElementById("fileUpload").files.length;
for (var i = 0; i < totalFiles; i++) {
var file = document.getElementById("fileUpload").files[i];
formData.append("fileUpload", file);
}
$.ajax({
type: 'post',
url: 'http://localhost:59973/Home/Upload',
data: formData,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
jsonData = JSON.parse(response.data);
if (jsonData != null) {
if (documentType == "Infolog") {
fileHandler.printDocument(jsonData); //This is used for pickinglist and infolog
} else {
var webGrid = handsonTable.WebGrid.get();
webGrid.loadData(jsonData);
}
}
},
error: function (error) {
if (textStatus == "error") {
modalHandler.printErrorModal();
}
}
});
},
}
$(document).ready(function () {
handsonTable.init();
menuButtonClick.onReady();
fileHandler.onReady();
buttonClicks.onReady();
}).ajaxStart(function () {
$('[data-remodal-id=modalAjax]').remodal().open();
}).ajaxStop(function () {
$('[data-remodal-id=modalAjax]').remodal().close();
});
When I upload a file (fileHandler), the modal shows during ajaxStart and closes on ajaxStop. However, If I click on a button in my menu (menuButtonclick) which trigger my modelBankHandler function, the modal shows during ajaxstart, but does not close on ajaxStop.
Why? All the data are retrieved as expected in my modelBankHandler, so why does not the modal closes?
If you have pressed F12 in the browser and looked at the console you would probably have found an error there. This video might help you to figure out basic problems yourelf.
I think printModelBank might throw an error, if the success or error functions throw an error then jQuery crashes and does not execute the ajaxStop handler:
$(document)
.ajaxStart(function () {
console.log("open model");
}).ajaxStop(function () {
console.log("close model");
});
$.ajax({
type: 'GET',
url: "/none",
data: {},
success: function (response) {
console.log("success");
throw new Error("now stop won't execute");
},
error: function (jqXHR, textStatus, errorThrown) {
console.log("error");
throw new Error("now stop won't execute");
}
});
You could solve this by having success and error as promise handlers, errors in promise handlers should not crash jQuery (but it does):
$(document)
.ajaxStart(function () {
console.log("open model");
}).ajaxStop(function () {
console.log("close model");
});
$.ajax({
type: 'GET',
url: "/none",
data: {}
})
.then(
response => {
console.log("success");
throw new Error("now stop won't execute");
},
(jqXHR, textStatus, errorThrown) => {
console.log("error");
throw new Error("now stop won't execute");
}
);
You could try native promises (jQuery still doesn't get promises right) and have it not crash on error in handler:
$(document)
.ajaxStart(function () {
console.log("open model");
}).ajaxStop(function () {
console.log("close model");
});
Promise.resolve()
.then(_ =>
$.ajax({
type: 'GET',
url: "/none",
data: {}
})
)
.then(
response => {
console.log("success");
throw new Error("now stop WILL execute");
},
(jqXHR, textStatus, errorThrown) => {
console.log("error");
throw new Error("now stop WILL execute");
}
);
IE does not support native promises so you will need a polyfill or try babel with ES2016

.getAll(...).then is not a function

I updated Angular to version 1.6.4.
So I had to update .success and .error to .then
Now I get the following error:
TypeError: .getAll(...).then is not a function
The Problem is here in the service:
function getAll(page, size) {
return $http.get(baseUrl + '/jobprofiles?page='+page+'&size='+size, {timeout: 5000}).then(function (response) {
data = response;
}), (function(response) {
alertService.setAlert({'message': 'jobmatch.server.unavailable', 'classified': 'danger', 'lives':1});
});
}
Here is the controller:
if($cookies.get("authenticated")=='true'){
//get a list of all candidateprofiles with the use of a page and a size
candidateprofilesService.getAll($scope.page, $scope.size).then(function() {
$scope.data = candidateprofilesService.getData()._embedded.candidateprofiles;
candidateprofilesService.getAll($scope.page, $scope.size+10).then(function() {
if(candidateprofilesService.getData()._embedded.candidateprofiles.length > $scope.data.length){
$scope.moreData = true;
}
else {
$scope.moreData = false;
}
})
});
}
You service code should be like :
myApp.service('candidateprofilesService', function($http) {
this.getAll = function (page, size) {
// just return the promise , don't evaluate here
return $http.get(baseUrl + '/jobprofiles?page='+page+'&size='+size, {timeout: 5000});
}
this.getData = function(){
// your getData() method body, also just return the promise.
}
});
Then in your controller after injecting service,
candidateprofilesService.getAll($scope.page, $scope.size).then(function(response){
//Evaluate promise here and handle the response
}, function(error){
//handle error
});
Your function should be like this,
function getAll(page, size) {
return $http.get(baseUrl + '/jobprofiles?page='+page+'&size='+size, {timeout: 5000}).then(function (response) {
return data = response;
}, function(response) {
alertService.setAlert({'message': 'jobmatch.server.unavailable', 'classified': 'danger', 'lives':1});
});
}

Then .then() function seems to run before the promise is finished

In the following code, the controller runs a function in a service that logs "object was destroyed". That function returns a promise.
On the .then() of that promise, in the controller, it logs "deleted"
"object was destroyed" should be logged first, and then (in the.then()) it should log "deleted". Unfortunately, it seems as if "deleted" is logged first.
It seems as if the .then() of the promise is ran before the function completes.
app.js
this.deleteData = function (db, objectId) {
var database = Parse.Object.extend(db);
var query = new Parse.Query(database);
return query.get(objectId, {
success: function (result) {
result.destroy({
success: function (result) {
console.log("object was destroyed");
},
error: function (result, err) {
console.log("object was not destroyed");
console.log(err);
}
});
},
error: function (result, err) {
console.log(err);
}
});
}
Controller
function ResourcesCtrl ($scope, globalFunctions, CRUD) {
$scope.deleteResource = function (objectId) {
CRUD.deleteData('resources', objectId).then(function (result2) {
console.log("deleted");
refreshResources();
});
}
The call to result.destroy() inside the success call back is asynchronous so therefore your success function will continue on and return and then promise that you return from your deleteData function will be resolved. All this before callback for result.destroy().
Ultimately you want deleteData to return the promise from result.destroy().
Here is a rough example
this.deleteData = function (db, objectId) {
var database = Parse.Object.extend(db);
var query = new Parse.Query(database);
return query.get(objectId).then(function (result) {
return result.destroy().then(function (result) {
console.log("object was destroyed");
}, function (err) {
console.log("object was not destroyed");
console.log(err);
});
}, function (err) {
console.log(err);
});
};
and then if you don't need the logging in can be even more succinct
this.deleteData = function (db, objectId) {
var database = Parse.Object.extend(db);
var query = new Parse.Query(database);
return query.get(objectId).then(function (result) {
return result.destroy();
});
};

Categories

Resources