Generic way of error handling in Angular using interceptors - javascript

I'm using Angular 1.08, hence I need to use responseInterceptors.
First the code.
Interpreter:
app.factory('errorInterceptor', ['$q', 'NotificationService', function ($q, NotificationService) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
// do something on error
alert('whoops.. error');
NotificationService.setError("Error occured!");
return $q.reject(response);
});
}
}]);
app.config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('errorInterceptor');
});
NotificationService:
app.service("NotificationService", function () {
var error = '';
this.setError = function (value) {
error = value;
}
this.getError = function () {
return error;
}
this.hasError = function () {
return error.length > 0;
}
});
Directive error-box:
app.directive("errorBox", function (NotificationService) {
return {
restrict: 'E',
replace: true,
template: '<div data-ng-show="hasError">{{ errorMessage }}</div>',
link: function (scope) {
scope.$watch(NotificationService.getError, function (newVal, oldVal) {
if (newVal != oldVal) {
scope.errorMessage = newVal;
scope.hasError = NotificationService.hasError();
}
});
}
}
});
The problem: When I use <error-box> at multiple places, all these boxes will display the error message. This is not my intent. I'd like to show only the error-box where the exception occurs.
For example, I have a directive which shows a list of transactions. When fetching the transactions fails, I want to show the error-box which is declared in that part.
I also have a directive where I can edit a customer. This directive also contains the error-box tag.
What happens is when saving a customer fails, both error-boxes are displayed, however, I only want the error-box of the customer to be displayed.
Does someone has an idea to implement this?

Angular services are Singleton objects, as described in the angular docs here. This means that Angular only creates one single "global" instance of a service and uses that same instance whenever the given service is requested. That means that Angular only ever creates one single instance of your NotificationService services and then will supply that one instance to every instance of your errorBox directive. So if one directive updates the NotificationService's error value, then all of the <error-box directives will get that value.
So, you're going to have to either create multiple notification services for each type of error (ie TransactionNotification and CustomerNotification, etc) or add different methods to your main NotificationService that would allow you to set only specific alerts (such as NotificationService.setCustomerError() or NotificationService.setTransactionError()).
None of those options are particularly user-friendly nor clean, but I believe (given the way you've set up your service), that's the only way to do it.
UPDATE: After thinking about it, I might suggest just dropping your whole NotificationService class and just using $scope events to notify your <error-box> elements when an error occurs:
In your 'errorInterceptor':
app.factory('errorInterceptor', ['$q', '$rootScope', function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
// do something on error
alert('whoops.. error');
var errorType = ...; // do something to determine the type of error
switch(errorType){
case 'TransactionError':
$rootScope.$emit('transaction-error', 'An error occurred!');
break;
case 'CustomerError':
$rootScope.$emit('customer-error', 'An error occurred!');
break;
...
}
return $q.reject(response);
});
}
}]);
And then in your errorBox directive:
link: function (scope, element, attrs) {
var typeOfError = attrs.errorType;
scope.$on(typeOfError, function (newVal, oldVal) {
if (newVal != oldVal) {
scope.errorMessage = newVal;
}
});
}
And then in your view:
<error-box error-type="transaction-error"></error-box>
<error-box error-type="customer-error"></error-box>
Does that make sense?

Related

ng-bind to value returned by promise resolve

I have angular service working with db, directive using method of this service and view where I need to show the value I get from db. I stuck on the moment where I need to bind value to view, first I tried to do it with controller (service->controller->view), but I couldn't bind value from view to controller one and realized that for asynchronously assigned value I need directive and its link function, I moved element into directive and tried to do it from there, but still no luck. So, I've got 2 questions:
Is my way of implementing this "AngularJS way"?
And if it is, how can I bind this value to view at last?
Here is the code (part of it, cropped for question):
Service:
var purchaseHistoryService = function ($q) {
this.getUserPurchases = function() {
var deferred = $q.defer();
Parse.Cloud.run('getUserPurchases').then(function(res) {
var purchases = [];
// take only part we need from purchase objects
if(res) {
res.forEach(function (purchase) {
purchases.push(purchase.attributes);
});
}
return deferred.resolve(purchases);
}, function(err) {
return deferred.reject(err);
});
return deferred.promise;
};
};
module.exports = purchaseHistoryService;
Directive:
var purchaseHistoryTable = function (purchaseHistoryService) {
return {
restrict: 'E',
link: function (scope) {
scope.purchases = purchaseHistoryService.getUserPurchases().then(function(res) {
console.log(res); // gives expected result, array of objects
return res;
}, function(err) {
console.error(err);
});
},
templateUrl: "purchaseHistoryTable"
};
};
module.exports = purchaseHistoryTable;
View:
.table--purchase-history
.table_cell-content_header--purchase-history(ng-bind="purchases") // check binding here, got '[object Object]'
.table_row--purchase-history(ng-repeat="purchase in purchases")
.table_cell--purchase-history-big
.table_cell-content--bought-packs
.table_cell-content_header--purchase-history(ng-bind="purchase.totalPrice")
.ver-line--purchase-history
Investigating issue I've found out that if I change directive like that:
var purchaseHistoryTable = function (purchaseHistoryService) {
return {
restrict: 'E',
link: function (scope) {
scope.purchases = 'example';
},
templateUrl: "purchaseHistoryTable"
};
};
module.exports = purchaseHistoryTable;
I've got 'example' in my view, which is ok, but when I add assigning to promise resolve value back, I've got [object Object] even if change return of resolve function to return 'example';
And questions again:
Is my way of implementing this "AngularJS way"?
And if it is, how can I bind this value to view at last?
Just rewrite your link function like this :
link: function (scope) {
purchaseHistoryService.getUserPurchases().then(function(res) {
console.log(res); // gives expected result, array of objects
scope.purchases = res;// <--- HERE COPY IN THE THEN
}, function(err) {
console.error(err);
});
},

Using $http in Angular Service - Cannot read property 'post' of undefined

I'm trying to learn how to use Angular right, by having all my business logic in services.
When I do a post request in a service, I get the following error:
Cannot read property 'post' of undefined
Here is some code:
UrlApp.controller('UrlFormCtrl', UrlFormCtrl);
UrlApp.factory('addUrlService', addUrlService);
function UrlFormCtrl($scope, $http) {
console.log('Url Form Controller Initialized');
$scope.addUrl = addUrlService.bind(null, $http);
}
function addUrlService($scope, $http){
console.log('initializing addUrlService');
return $http.post('urls/create', {'test':'test'}).then(function(response){
return response.data;
});
}
I'm just getting the hang of Angular, so I'm not entirely sure what I'm doing wrong. See any problems?
Firstly, you don't need to inject $scope in your service.
Secondly, you don't need to inject $http service in your controller.
Thirdly, you need to inject the service in your controller.
Finally, addUrlService service is returning a promise meaning it will make a request when service is instantiated. You may want to return a function instead or an object containing several functions.
So I would change your code to:
UrlApp.controller('UrlFormCtrl', UrlFormCtrl);
UrlApp.factory('AddUrlService', AddUrlService);
function UrlFormCtrl($scope, AddUrlService) {
$scope.addUrl = AddUrlService.addUrl;
}
function AddUrlService($http) {
function addUrl() {
return $http.post('urls/create', {
'test': 'test'
}).then(function (response) {
return response.data;
});
}
return {
addUrl: addUrl
};
}
Can you try like this
UrlApp.controller('UrlFormCtrl', UrlFormCtrl);
UrlApp.factory('addUrlService', addUrlService);
function UrlFormCtrl($scope,addUrlService) {
console.log('Url Form Controller Initialized');
$scope.addUrl = addUrlService;
}
function addUrlService($http){
console.log('initializing addUrlService');
return $http.post('urls/create', {'test':'test'}).then(function(response){
return response.data;
});
}

AngularJS: Bind angular service property to scope

Scenario
I have a service UserService that maintains the (boolean) sessionStatus of the user.
The view conditionally shows [LOGOUT] on ng-show=sessionStatus (i.e. if not logged in (false), no show).
sessionStatus of ViewController should therefore always match that of UserService ... right?
If you click [LOGOUT] when it's visible, it does some loggy outty things, sessionStatus value changes, and view should update with new outcome of ng-show....
Problem
Currently, clicking logout does not seem to update var UserService.sessionStatus?
How do I keep $scope.sessionStatus updated when UserService.logout() occurs and changes UserService.sessionStatus?
How do I map the changing $scope to ng-show?
Files
View
<a ng-show="sessionStatus" ng-click="logout()">Logout</a>
ViewController
app.controller('AppController', function($scope, $interval, $http, UserService) {
$scope.logout = function() { UserService.logout(); }
// This ain't working
$scope.$watch(UserService.sessionStatus, function() {
$scope.sessionStatus = UserService.sessionStatus;
});
});
UserService
NB: appUser is an injected global var in the HTML head (a hacky fix until I get session/cookie stuff working properly)
app.factory('UserService', function($http) {
var pre;
var sessionStatus;
function init() { // Logged in : Logged out
pre = appUser.user != undefined ? appUser.user : { name: 'Logged out', uid: '0' };
sessionStatus = pre.uid != "0" ? true : false;
}
function resetSession() { appUser = null; init(); }
init();
return {
sessionStatus: function() { return sessionStatus; }, // update on change!
logout: function() {
$http.get("/logout").then(function (data, status, headers, config) {
resetSession();
})
}
};
});
Instead of a watch, simply use a scoped function that returns the session status from the service.
$scope.sessionStatus = function() {
return userService.sessionStatus();
};
Your Logout link would look as below:
<a ng-show="sessionStatus()" ng-click="logout()">Logout</a>
A stripped down Plunker for your functionality: http://plnkr.co/edit/u9mjQvdsvuSYTKMEfUwR?p=preview
Using a scoped function is cleaner and is the "correct" way to do it. Yet, for the sake of completeness you could also have fixed your watch:
$scope.$watch(function () {
return UserService.sessionStatus;
}, function() {
$scope.sessionStatus = UserService.sessionStatus;
});
The first argument of the $watch method takes a WatchExpression which can be a string or a method.
But again, $watch should not be used in controllers. Using scoped methods as suggested are cleaner and easier to test.

Jquery function after angularJS $http

I am building a search application.I am using the highlighter function from Johann Burkard's JavaScript text higlighting jQuery plugin.
After an angularJS $Http call all the data is bound. I created a directive to call the Highlighter function.
searchApplication.directive('highlighter', function () {
return {
restrict: 'A',
link: function (scope, element) {
element.highlight(scope.searchText);
}
}
});
here is the controller
`searchApplication.controller('searchController', function ($scope, $http, searchService) {
$scope.myObject= {
searchResults: null,
searchText: "",
};
$scope.search = function () {
searchService.doSearch($scope.luceneSearch.searchText).then(
function (data) {
$scope.myObject.searchResults = data;
},
function (data, status, headers, configs) {
$log(status);
});
}
});`
here is the service
searchApplication.factory('searchService', function ($http, $q) {
return {
doSearch: function (_searchText) {
var deferred = $q.defer();
var searchURL = '/Search';
$http({
method: 'POST',
url: searchURL,
params: { searchText: _searchText }
})
.success(function (data, status, headers, configs) {
deferred.resolve(data);
})
.error(function (data, status, headers, configs) {
deferred.reject(data, status, headers, configs);
});
return deferred.promise;
}
}
});
In the html I have the following
<td ng-repeat="col in row.fields" highlighter>
{{col.Value}}
</td>
The directive is not getting the value to be searched, rather it gets {{col.Value}} and hence it is not able to highlight.
What am I doing wrong? How can I get the actual bound values so that I can manipulate it? Is there a better way to do this?
Updated: with controller and service code
From the code given, it should work fine if your controller has $scope.searchText properly set. I defined your directive in my app and debugged the link() function in Chrome, and scope.searchText is found as expected. If from browser debugging you find scope.searchText to be undefined you probably need to also post your controller code here.
UPDATE: From your comment, it seems the problem here is the execution order within Angular. Apparently the linking function is getting called before text interpolation is finished, so the solution is to wait for that process before proceeding with the highlighting part.
The trick here is to $watch for an update in col.Value and invoke the highlight logic afterward. The code below should do the trick:
app.directive('highlighter', function ($log) {
return {
restrict: 'A',
compile: function compile(element, attributes) {
// at this point, we still get {{col.Value}}
var preinterpolated = element.text().trim();
// removing the enclosing {{ and }}, so that we have just the property
var watched = preinterpolated.substring(2, preinterpolated.length - 2);
$log.info('Model property being watched: ' + watched);
return {
post: function (scope, element) {
// we watch for the model property here
scope.$watch(watched, function(newVal, oldVal) {
// when we reach here, text is already interpolated
$log.info(element.text());
element.highlight(scope.searchText);
});
}
};
}
};
});
This time around, $log logic should print out the interpolated value instead of just col.Value.
UPDATE2: I'm not quite sure how to go from there with directive, but if you don't mind using Angular filter, you can try this Angular UI solution. After attaching js file to your page, just include 'ui.highlight' module in your app and the filter will be available for use. It's also small like your jquery lib as well:
https://github.com/angular-ui/ui-utils/blob/master/modules/highlight/highlight.js
You can try live example here:
http://angular-ui.github.io/ui-utils/
Your HTML should now look like this (directive is no longer needed):
<td ng-repeat="col in row.fields">
{{col.Value | highlight:searchText}}
</td>
Also noted that now the CSS class for hightlighted text is ui-match instead of highlight.
UPDATE3: If you're set on using a directive, this person seems to do something very similar, except that he's adding a timeout:
http://dotnetspeak.com/2013/07/implementing-a-highlighting-directive-for-angular
setTimeout(function () {
element.highlight(scope.searchText);
}, 300);

Why im not getting the response back from this factory function in angularJS [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 8 years ago.
I got this angular factory:
var productApp = angular.module('productApp', ['ngRoute', 'LocalStorageModule', 'angularSlideables', 'ui.bootstrap']);
productApp.factory('productFactory', function($http, localStorageService, $q) {
var factory = {};
factory.getProductById = function(prod_id) {
if(prod_id !== '') {
$http({
url: 'rest/message/getProductById/' + prod_id,
method: 'GET'
}).success(function(data, status) {
return data;
}).error(function(data, status){
// do nothing
});
}else {
alert("There was an error while passing the ID. Please refresh the page and try again");
}
}
return factory;
});
Injecting the factory in a controller and calling to the "getProductById" function:
productApp.controller('ModalInstanceCtrl', function ($scope, $modalInstance, productFactory, prodId) {
console.log("this is the prod id " + prodId);
// search product in the database
$scope.prod = productFactory.getProductById(prodId);
console.log($scope.prod);
$scope.ok = function () {
console.log($scope.prodData);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
Now, don't know what's wrong with it... the function RETURNS the data because i did a console.log(data) and saw all the response, but in the controller if i inspect the $scope.prod, it's undefined. It's not returning the data back from the function.
(Just in case you guys ask, the "prodId" in the controller parameter is fine, and retrieving that from another controller)
How can i solve this? :(
Thanks in advance.
The pattern I have used to solve this problem is to pass in the success & error callback functions to the factory... like this:
var productApp = angular.module('productApp', ['ngRoute', 'LocalStorageModule', 'angularSlideables', 'ui.bootstrap']);
productApp.factory('productFactory', function($http, localStorageService, $q) {
var factory = {};
factory.getProductById = function(prod_id, successCallback, errorCallback) {
if(prod_id !== '') {
$http({
url: 'rest/message/getProductById/' + prod_id,
method: 'GET'
})
.success(successCallback)
.error(errroCallback);
} else {
alert("There was an error while passing the ID. Please refresh the page and try again");
}
}
return factory;
});
and then:
productApp.controller('ModalInstanceCtrl', function ($scope, $modalInstance, productFactory, prodId) {
console.log("this is the prod id " + prodId);
// search product in the database
productFactory.getProductById(prodId, function successCallback(data) {
$scope.prod = data;
}, function errorCallback(data, status) {
alert("An error occurred retrieving product. Please refresh the page & try again.");
});
console.log($scope.prod);
$scope.ok = function () {
console.log($scope.prodData);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
By doing it this way instead, you have access to the scope in the controller & can do whatever you need to with the returned data.
Here's what I do. I'm using $resournce instead of $http, but it should be enough to get you going. You may even want to use the $resource since it has the built in fns.
My factory:
.factory('WorkOrder', function($resource){
// $resource Usage: $resource(url[, paramDefaults][, actions]);
return $resource('/controller/get/:id.json', {}, {
/*
* By default, the following actions are returned; modify or add as needed
* { 'get': {method:'GET'},
* 'save': {method:'POST'},
* 'query': {method:'GET', isArray:true},
* 'delete': {method:'DELETE'} };
*/
});
})
My controller:
// get the work order data using the work order id from the tag attribute
var getWO = function() {
WorkOrder.get({woId:$attrs.id},
// success callback
function(response) {
// Assign the work order data to the scope
$scope.WorkOrder = response.WorkOrder;
},
// fail callback
function(response) {
// whatever...
}
);
};
getWO();
I put my success and fail fns in my controller because that's where I most likely know how I want to respond to success or failed calls. I also assign the function to a variable and then call it right after in case I want to include the fn call inside a $timeout or call it elsewhere.
Hope this answers your question.

Categories

Resources