I'm attempting to handle http errors within AngularJS (using ui-router), but am losing the context of my run function in the following code.
bugtracker.run(['$rootScope', '$state', function($rootScope, $state) {
//at this point $state and $rootScope are defined
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
//at this point $state and $rootScope are undefined
});
}]);
The code that causes $stateChangeError to trigger is as follows.
//main.js
bugtracker.config(['$stateProvider', '$httpProvider', '$compileProvider', function($stateProvider, $httpProvider, $compileProvider) {
//...
$stateProvider.state('projects.show', {
url: '/{projectId:[0-9]{1,8}}',
templateUrl: '/assets/projects/show.html',
controller: 'ProjectShowCtrl',
resolve: bugtracker.controller('ProjectShowCtrl').resolve
});
//...
}]);
//ProjectShowCtrl.js
projectShowCtrl.resolve = {
project: function(Project, $q, $stateParams, $state) {
var deferred = $q.defer();
Project.findById($stateParams.projectId, function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(errorData); // you could optionally pass error data here
});
return deferred.promise;
},
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
};
I would like $state to be defined within the anonymous function called by the $on function so that I could redirect the user to a 401, 403, etc. page, but I'm unsure why it is not.
In other examples I have seen (https://github.com/angular-ui/ui-router/issues/898) it is implied that $state is defined within the context of the anonymous function.
If anyone could explain why $state is not defined or what I can change to make it defined I would greatly appreciate it!
There's no way these would be undefined, unless they were already undefined in the run block. However I've seen cases where the debugging tool thinks some variables are undefined when they actually aren't. That happened to me in FireBug with an old FF version.
Related
I have a partial in which data is coming from multiple controllers, not the situation is those functions which are called in the controller,they are hitting the server for more than fifty times, and they keep hitting as long as they dont get the response from server. I dont know how to tackle this situation please guide me.
mainControllers.controller('AddProductController', ['$scope', '$http', '$routeParams', '$cookies', '$rootScope', 'Upload', '$timeout', '$uibModal', '$log', '$document', '$window', 'variantsService', 'toaster', '$route', '$rootScope', 'Lightbox', function ($scope, $http, $routeParams, $cookies, $rootScope, Upload, $timeout, $uibModal, $log, $document, $window, variantsService, toaster, $route, $rootScope, Lightbox) {
/*Currency dynamic*/
$scope.currency = function () {
$http.get('currencies',
{headers:
{'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': $rootScope.keyword_auth_token, 'Accept-Language': $cookies.get('type')}
})
.success(function (data) {
$scope.user_curr = data[0].code;
})
.error(function (data) {
console.log(data);
});
};
/*Currency dynamic ends here*/
$scope.currency();
}]);
Is there any way, any way, so that I can limit this thing?
It definitely is a bad idea to have multiple controllers for a single partial. You should consider using angular factories for maintaining data in such cases. But to provide you a short solution, you should remove the line $scope.currency(); from your controller (because it would make an api call as soon as your controller is initialized) and consider using ng-init built-in directive. So, basically in your partial where you are using ng-controller="AddProductController", you can add ng-init="currency()" (If you want to make an api call).
I always put the calls in a service, and then you can take full control. Something like this:
app.service("currencyService", function($q, $http) {
var _currencyPromise = null,
_currencies = null;
this.getCurrencies = function() {
var deferred = $q.defer();
// Check if the currencies are resolved before
// If so, immediately return these
if (_currencies) {
deferred.resolve(_currencies);
}
// Else check if the promise is already running
// If so, use that promise
else if (_currencyPromise) {
_currencyPromise.then(function(response) {
deferred.resolve(response.data);
});
}
// Else make the http call and assign to the promise
// So that the promise can be used instead of a new http call
else {
_currencyPromise = $http.get("..");
_currencyPromise.then(function(response) {
// Assign data to the currencies, so that it can be used
// by next calls immediately
_currencies = response.data;
deferred.resolve(_currencies);
}, function(error) {
// something went wrong
_currencyPromise = null;
deferred.reject();
});
}
return deferred.promise;
}
});
Then in your controllers you can always use this service, while the http call will only be made once:
app.controller("myCtrl", ["$scope", "currencyService", function($scope, currencyService) {
currencyService.getCurrencies().then(function(currencies) {
$scope.user_curr = currencies[0].code;
});
}]);
See this jsfiddle for reference. In the console you can see that the API is only called once.
I came up with a quite simple solution. For example I have a view like this
<div ng-controller="HomeController">
<div class="active tab-pane" ng-controller="AddProductController" ng-init="subcategories_id();currency();">
<p>{{user_curr}}</p>
</div><!--ends here->
<p>first controller {{abc}}</p>
</div>
I am using the nginitwhich works fine.
I have a single-page application built in angularjs, using UI-Router and nested views. My code looks like this:
app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider' ,'$mdDateLocaleProvider', '$locationProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, $mdDateLocaleProvider, $locationProvider) {
var rootpath = config.paths.components;
$stateProvider.state('home', {
url: '/',
views: {
index: {
templateUrl: rootpath + '_root/template.html',
controller: "RootController"
},
"header#home": {
templateUrl: rootpath + 'header/header-template.html',
controller: "HeaderController"
},
"sidebar#home": {
templateUrl: rootpath + 'sidebar/sidebar-template.html',
controller: "SidebarController"
},
"main#home": {
templateUrl: rootpath + 'main/main-template.html',
controller: "MainController"
}
},
resolve: {
authorize: ['RootService', function (RootService) {
var auth = RootService.getKey();
return RootService.getConfig(auth.key);
}]
},
});
$urlRouterProvider.otherwise('/');
}]).run(['$rootScope', '$state', '$window', function($rootScope, $state, $window){
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){
$window.location = error.status +'.html';
});
In the "home" state, I'm doing a resolve to see if the authentication key passed in is valid.
If the key is invalid or has expired, $stateChangeError event catches the error and redirects the user to an error page.
This works fine when the application starts and on refresh.
Problem is, when the key has expired (only valid for ten minutes) and the application is not reloded, the
$stateChangeError event doesn't catch the error message.
Any thoughts on how to fix this?
You can solve this problem by using the $watch provided by the angular js.
$scope.$watch(function(scope) { return scope.data.myVar },
function() {}
);
The first function is the value function and the second function is the listener function.
This example value function returns the $scope variable scope.data.myVar. If the value of this variable changes, a different value will be returned, and AngularJS will call the listener function.
I think this might solve your problem...
Like many folks, I'm new to testing Angular with Jasmine and am struggling to get this right. I use ui-router to do my routing and right now, the problem I'm having is that the $state.current.name in the test is an empty string and I have no idea why it does that.
This is the code in my routing module:
var cacRouteViewMod = angular.module('cacRouteViewMod', ['ui.router', 'cacLib']);
cacRouteViewMod.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('countries', {
url: '/countries',
templateUrl: 'countries/countries.html',
controller: 'countriesCtrl',
resolve : {
countries: ["getCountry", function(getCountry) {
return getCountry();
}]
}
});
}]);
and the test I wrote is this:
describe('cac_app_views (routing)', function() {
var $rootScope,
$state,
$injector,
getCountryMock,
state = 'countries';
beforeEach(function() {
module('cacRouteViewMod', function($provide, $urlRouterProvider) {
$urlRouterProvider.deferIntercept();
$provide.value('getCountry', getCountryMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
$templateCache.put('countries/countries.html', '');
})
});
// Test 1
it('should respond to URL', function() {
expect($state.href(state)).toEqual('#/countries');
});
// Test 2
it('should resolve getCountry', function() {
getCountryMock = jasmine.createSpy('getCountry').and.returnValue('nanana');
$rootScope.$apply(function() {
$state.go('countries');
});
expect($state.current.name).toBe('countries');
expect($injector.invoke($state.current.resolve.countries)).toBe('nanana');
});
});
Test 1 is fine, but test 2 is the issue. The test fails because it expected '' to be 'countries'.
When I log $state.current to the console it gives
Object {name: "", url: "^", views: null, abstract: true}
I'm getting pretty desperate at this point. Could anyone help me understand/solve this problem?
I solved this in this manner:
By reading similar stockoverflow posts, I put a listener for $stateChangeError and it got triggered. I logged out the error data and saw that it's a typeError: getCountry is not a function. This caused the $state not to be updated, and therefore still contains the original(empty) $state.
I fixed the $provide.value to such:
$provide.value('getCountry', getCountryMock = function() {return 'nanana';});
which says "whenever getCountry is called, provide getCountryMock instead, which is a function that returns a string 'nanana'.
Now the tests all work the way I want them to.
Note: I found that getCountryMock = jasmine.createSpy..... line of code to be obsolete with my other change to $provide.value() so I commented it out.
According to the documentation $state.go returns a promise.
You should use done() function from Jasmine in order to test such a code: http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine/
I'm trying to ensure that upon loading the frame state, that my $rootScope has all the necessary properties defined from previous states.
The ionic.utils module is properly injected into my angular app. This module comes from my services.js file.
angular.module('ionic.utils', [])
.factory('dataService', ['$rootScope','$q','$timeout', function($rootScope, $q, $timeout) {
return {
get: function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve($rootScope);
}, 2000);
return deferred.promise;
}
}
}]);
Inside my controllers.js file, this is the corresponding controller for my frame state:
.controller('FrameCtrl', ['$scope','$state','$rootScope','dataService',
function($scope, $state, $rootScope, dataService) {
// get active address and delivery time.
dataService.get().success(function() {
console.log("derp");
});
}])
However, this controller returns the following console error upon state transition:
ionic.bundle.js:17696 TypeError: Cannot read property 'get' of undefined
at new <anonymous> (controllers.js:201)
at invoke (ionic.bundle.js:11591)
at Object.instantiate (ionic.bundle.js:11602)
at $get (ionic.bundle.js:14906)
at updateView (ionic.bundle.js:42986)
at IonicModule.directive.directive.compile.eventHook (ionic.bundle.js:42933)
at Scope.$get.Scope.$broadcast (ionic.bundle.js:20605)
at $state.transitionTo.$state.transition.resolved.then.$state.transition (ionic.bundle.js:34122)
at deferred.promise.then.wrappedCallback (ionic.bundle.js:19197)
at ionic.bundle.js:19283
I'm having trouble finding the error in the service I've written. Some help would be greatly appreciated!
EDIT
After adding the dependency injections into my controller, now the error has changed. Here it is:
TypeError: object is not a function
at new <anonymous> (controllers.js:202)
at invoke (ionic.bundle.js:11591)
at Object.instantiate (ionic.bundle.js:11602)
at $get (ionic.bundle.js:14906)
at updateView (ionic.bundle.js:42986)
at IonicModule.directive.directive.compile.eventHook (ionic.bundle.js:42933)
at Scope.$get.Scope.$broadcast (ionic.bundle.js:20605)
at $state.transitionTo.$state.transition.resolved.then.$state.transition (ionic.bundle.js:34122)
at deferred.promise.then.wrappedCallback (ionic.bundle.js:19197)
at ionic.bundle.js:19283
Your dependency array in controller is missing numerous dependencies passed to arguments
.controller('FrameCtrl', [ 'Rootscope', function($scope, $state,
$rootScope, Rootscope) {
Should be
.controller('FrameCtrl', ['$scope','$state', '$rootScope', 'Rootscope', function($scope, $state,
$rootScope, Rootscope) {
Sure seems confusing to me to name a service Rootscope!
Normally with promises we just use .then, which takes the success function as the first parameter and the error function as the second.
success and error are functions on a promise that AngularJS adds
for us when using $http or $resource. They're not standard, you
won't find them on other promises.
Code
dataService.get().then(function() {
console.log("derp");
});
Return was missing from deferred.resolve()
angular.module('ionic.utils', []).factory('dataService', ['$rootScope', '$q', '$timeout', function($rootScope, $q, $timeout) {
return {
get: function() {
var deferred = $q.defer();
$timeout(function() {
return deferred.resolve($rootScope);
}, 2000);
return deferred.promise;
}
}
}]);
Hopefully this will help you. Thanks.
Attempting to get ui-router to resolve the Authentication service before I allow the switch into that state.
unfortunately it continues to evade me, with angular hanging whenever I inject a service that is not part of angular.
I know that I can't inject a service into .config, but surely I should be able to do it just before the controller is loaded?
here's a cut down version of the code:
app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
// For any unmatched url, redirect to state:home
$urlRouterProvider.otherwise("/");
// Now set up the states
$stateProvider
.state('home', {
url: "/",
templateUrl: "partials/home.html",
resolve: {
isAuthorised: ['$log', '$q', 'Auth', function ($log, $q, auth) {
var q = $q.defer();
$log.debug("checking if authorised..."); //this doesn't show
if (auth.whatever()) {
q.resolve(true);
} else {
q.reject(false);
}
return q.promise;
}]
},
controller: "DefaultCtrl"
});
}]);
So the controller never actually gets instantiated... there are no errors in the console.
The same happens if I type any random string in as the service name... so my best guess is that it can't find it? But all other areas of the app can...
I have spent a while researching this and I'm pretty sure I should be able to accomplish this somehow.
any help would be most appreciated.
Grant.
Edit
Thanks Chris T for the Plunkr! I am closing in on the issue, I added your console logs and I'm getting the following error:
Start: {} -> home{}
Error: {} -> home{} ReferenceError: $window is not defined {stack: "ReferenceError: $window is not defined↵ at a (h…pm/planitmoney/claw/dist/js/bower.min.js:815:112)", message: "$window is not defined"}
That bower.min.js file is just angular followed by UI router.. I'm going to do some more checking. Thanks again.
Thanks again to Chris T who created the Plunkr: http://plnkr.co/edit/aeEt4ddUYMM3xRi8M38T?p=preview
which helped solve the issue.
I copied the console logs he has in there over to my application:
// Adds state change hooks; logs to console.
app.run(function($rootScope, $state) {
$rootScope.$state = $state;
function message(to, toP, from, fromP) { return from.name + angular.toJson(fromP) + " -> " + to.name + angular.toJson(toP); }
$rootScope.$on("$stateChangeStart", function(evt, to, toP, from, fromP) { console.log("Start: " + message(to, toP, from, fromP)); });
$rootScope.$on("$stateChangeSuccess", function(evt, to, toP, from, fromP) { console.log("Success: " + message(to, toP, from, fromP)); });
$rootScope.$on("$stateChangeError", function(evt, to, toP, from, fromP, err) { console.log("Error: " + message(to, toP, from, fromP), err); });
});
where I immediately noticed the error:
Start: {} -> home{}
Error: {} -> home{} ReferenceError: $window is not defined {stack: "ReferenceError: $window is not defined↵ at a (h…pm/planitmoney/claw/dist/js/bower.min.js:815:112)", message: "$window is not defined"}
digging a little deeper I traced the error to what was actually throwing it and it was fixed minutes later.
The service that I was attempting to resolve also called in some dependencies of its own. Unknown to me there was a missing $window injection in one of these services.
The Error did not show up in the console
I guess that it may have been because of the unique way in which it was accessed (using ui-routers resolve) Either that or my use of Raven in the application has thrown it somewhere that I didnt see it.
I hope this helps someone else get to the bottom of any issue.
Thanks again everyone.
We have a similar setup that checks localStorage for an token and resolves the promise.
Check the following example:
var requireUser = { User: [ "$location", "$rootScope", "$q", "API", "Session",
function ( $location, $rootScope, $q, API, Session )
{
var deferred = $q.defer();
if( Session.get.authToken() )
{
if( Session.get.user() )
{
deferred.resolve( $rootScope.user );
}
else
{
$location.path( "/login" );
}
}
else
{
deferred.reject( "No user token" );
$location.path( "/login" );
}
return deferred.promise;
} ] };
$routeProvider
.when( "/users", { templateUrl: "/build/templates/users.html", controller: "UsersCtrl", resolve: requireUser, active: "users" } )