Angular ui router unit testing (states to urls) - javascript

I'm having some trouble unit testing the router in my application, which is built on the Angular ui router. What I want to test is whether state transitions change the URL appropriately (there will be more complicated tests later, but this is where I'm starting.)
Here is the relevant portion of my application code:
angular.module('scrapbooks')
.config( function($stateProvider){
$stateProvider.state('splash', {
url: "/splash/",
templateUrl: "/app/splash/splash.tpl.html",
controller: "SplashCtrl"
})
})
And the testing code:
it("should change to the splash state", function(){
inject(function($state, $rootScope){
$rootScope.$apply(function(){
$state.go("splash");
});
expect($state.current.name).to.equal("splash");
})
})
Similar questions on Stackoverflow (and the official ui router test code) suggest wrapping the $state.go call in $apply should be enough. But I've done that and the state is still not updating. $state.current.name remains empty.

Been having this issue as well, and finally figured out how to do it.
Here is a sample state:
angular.module('myApp', ['ui.router'])
.config(['$stateProvider', function($stateProvider) {
$stateProvider.state('myState', {
url: '/state/:id',
templateUrl: 'template.html',
controller: 'MyCtrl',
resolve: {
data: ['myService', function(service) {
return service.findAll();
}]
}
});
}]);
The unit test below will cover testing the URL w/ params, and executing the resolves which inject its own dependencies:
describe('myApp/myState', function() {
var $rootScope, $state, $injector, myServiceMock, state = 'myState';
beforeEach(function() {
module('myApp', function($provide) {
$provide.value('myService', myServiceMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
// We need add the template entry into the templateCache if we ever
// specify a templateUrl
$templateCache.put('template.html', '');
})
});
it('should respond to URL', function() {
expect($state.href(state, { id: 1 })).toEqual('#/state/1');
});
it('should resolve data', function() {
myServiceMock.findAll = jasmine.createSpy('findAll').and.returnValue('findAll');
// earlier than jasmine 2.0, replace "and.returnValue" with "andReturn"
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
// Call invoke to inject dependencies and run function
expect($injector.invoke($state.current.resolve.data)).toBe('findAll');
});
});

If you want to check only the current state's name it's easier to use $state.transitionTo('splash')
it('should transition to splash', inject(function($state,$rootScope){
$state.transitionTo('splash');
$rootScope.$apply();
expect($state.current.name).toBe('splash');
}));

I realize this is slightly off topic, but I came here from Google looking for a simple way to test a route's template, controller, and URL.
$state.get('stateName')
will give you
{
url: '...',
templateUrl: '...',
controller: '...',
name: 'stateName',
resolve: {
foo: function () {}
}
}
in your tests.
So your tests could look something like this:
var state;
beforeEach(inject(function ($state) {
state = $state.get('otherwise');
}));
it('matches a wild card', function () {
expect(state.url).toEqual('/path/to/page');
});
it('renders the 404 page', function () {
expect(state.templateUrl).toEqual('views/errors/404.html');
});
it('uses the right controller', function () {
expect(state.controller).toEqual(...);
});
it('resolves the right thing', function () {
expect(state.resolve.foo()).toEqual(...);
});
// etc

For a state that without resolve:
// TEST DESCRIPTION
describe('UI ROUTER', function () {
// TEST SPECIFICATION
it('should go to the state', function () {
module('app');
inject(function ($rootScope, $state, $templateCache) {
// When you transition to the state with $state, UI-ROUTER
// will look for the 'templateUrl' mentioned in the state's
// configuration, so supply those templateUrls with templateCache
$templateCache.put('app/templates/someTemplate.html');
// Now GO to the state.
$state.go('someState');
// Run a digest cycle to update the $state object
// you can also run it with $state.$digest();
$state.$apply();
// TEST EXPECTATION
expect($state.current.name)
.toBe('someState');
});
});
});
NOTE:-
For a nested state we may need to supply more than one template. For ex. if we have a nested state core.public.home and each state, i.e. core, core.public and core.public.home has a templateUrl defined, we will have to add $templateCache.put() for each state's templateUrl key:-
$templateCache.put('app/templates/template1.html');
$templateCache.put('app/templates/template2.html');
$templateCache.put('app/templates/template3.html');
Hope this helps. Good Luck.

You could use $state.$current.locals.globals to access all resolved values (see the code snippet).
// Given
$httpBackend
.expectGET('/api/users/123')
.respond(200, { id: 1, email: 'test#email.com');
// When
$state.go('users.show', { id: 123 });
$httpBackend.flush();
// Then
var user = $state.$current.locals.globals['user']
expact(user).to.have.property('id', 123);
expact(user).to.have.property('email', 'test#email.com');
In ui-router 1.0.0 (currently beta) you could try to invoke $resolve.resolve(state, locals).then((resolved) => {}) in the specs. For instance https://github.com/lucassus/angular-webpack-seed/blob/9a5af271439fd447510c0e3e87332959cb0eda0f/src/app/contacts/one/one.state.spec.js#L29

If you're not interested in anything in the content of the template, you could just mock $templateCache:
beforeEach(inject(function($templateCache) {
spyOn($templateCache,'get').and.returnValue('<div></div>');
}

Related

Show loading indicator at root level until all route resolves are complete

I've been doing some Googling around this already but I'm unable to find a solution that works.
I'm using AngularJS 1.5.5 and .NET Web API 2 to build a web application and I would quite simply like to hide the ng-view element until all resolves have completed on the route.
I'm trying to use the $routeChangeStart and $routeChangeSuccess to set a variable on the $rootScope that is used in the index html to display the loading indicator and hide the content until the variable is false.
Here is my routing code for the routeChange properties:
_app.config([
'$routeProvider', '$httpProvider', '$provide',
function ($routeProvider, $httpProvider, $provide) {
$routeProvider.when('/Account',
{
templateUrl: '/Content/js/areas/account/account.html',
controller: 'accountController',
resolve: {
$accountResolver: function (accountService) {
return accountService.getMyAccountData();
}
},
caseInsensitiveMatch: true
});
$routeProvider.otherwise({ redirectTo: '404' });
}
]);
_app.run(['$rootScope', '$location', '$window', '$q', 'authService',
function ($rootScope, $location, $window, $q, authService) {
$rootScope.$on("$routeChangeStart",
function (e, curr, prev) {
$rootScope.$loadingRoute = true;
});
$rootScope.$on("$routeChangeSuccess",
function (evt, next) {
$rootScope.$loadingRoute = false;
});
$rootScope.$on("$routeChangeError",
function (evt, next) {
$rootScope.$loadingRoute = false;
});
}]);
And here is my html using that $loadingRoute variable:
<body class="ng-cloak" data-ng-app="wishlist" data-ng-controller="appController">
<wl-header></wl-header>
<preloader ng-if="$loadingRoute"></preloader>
<section ng-view ng-if="!$loadingRoute" class="container ng-cloak"></section>
</body>
I understand that there's quite a lot of articles covering this but none seem to work in my case. $loadingRoute gets set to true when the route change starts, as expected, which I will see if I add {{$loadingRoute}} to the HTML before the <section></section> tag. However before the $accountResolveris resolved, the $routeChangeSuccess gets fired, setting $rootScope.$loadingRoute = false which is unexpected.
I was under the impression that $routeChangeSuccess only got fired after all resolves had completed on the current route.
Am I doing something really obviously wrong here? Or has Angular simply changed?
Edit: I would also like to add that this approach worked in previous projects, so I'm at a real loss as to what's going wrong. I could set $rootScope.$loadingRoute manually in each page controller but that feels too dirty and unmaintainable.
Edit 2:
_app.factory('accountService', [
'accountResource',
function (accountResource) {
var _self = this;
return {
register: function (authData) {
return accountResource.register(authData);
},
getMyAccountData: function () {
return accountResource.getMyAccountData();
}
}
}
]);
_app.factory('accountResource', [
'$resource', 'rootUrl',
function ($resource, rootUrl) {
var api = rootUrl() + 'api/Account';
return $resource(api,
{},
{
register: {
method: 'POST',
url: '{0}/register'.format(api)
},
getMyAccountData: {
method: 'GET',
url: '{0}/GetMyAccountData'.format(api)
}
});
}
])
In order for a resolver to delay route change, it should return a promise. Otherwise route change happens immediately, this is what happens when $routeChangeSuccess is triggered before a promise from accountService.getMyAccountData() is resolved.
The problem is $resource methods (and so accountService.getMyAccountData()) return self-filling object that is populated with data asynchronously. A promise for this data is available as $promise property (see the reference), so it should be used for a resolver:
$accountResolver: function (accountService) {
return accountService.getMyAccountData().$promise;
}
If accountService is supposed to be purely promise-based wrapper for accountResource, a cleaner way to do this is to return a promise from its methods instead:
getMyAccountData: function () {
return accountResource.getMyAccountData().$promise;
}

AngularJS new router could not instantiate controller

I am trying to create a service to use throughout my Angular app that pulls in data from a .json file using $http. This is what the factory looks like:
var trooNewsServices = angular.module('trooNewsServices', []);
trooNewsServices.factory('Articles', ['$http',
function($http){
$http.get('resources/articles.json').success(function(data) {
return data;
});
}]);
I passed in the trooNewsServices dependency into my module declaration. Any controller that I try to pass in my new Articles service, I get a
"Could not instantiate controller HomeController"
error in the console. Not sure what I am missing/what is wrong with this code. Should I be using $resource instead of $http?
Here is how I am passing the 'trooNewsServices' into my main module:
var TrooNews = angular
.module('TrooNews', ['ngMaterial', 'ngNewRouter', 'trooNewsServices'])
.config(function($mdThemingProvider) {
$mdThemingProvider
.theme('default')
.primaryPalette('indigo')
.accentPalette('pink');
})
.config(function($locationProvider) {
$locationProvider.html5Mode({
enabled: false,
requireBase: false
});
});
Here is how I try to inject 'Articles' into one of my controllers:
TrooNews.controller('HomeController', ['Articles',
function(Articles) {
this.name = 'Troo News';
this.articles = Articles.query();
}]);
And here is how I set up routing in my 'AppController':
TrooNews.controller('AppController', function($router, $mdSidenav, $mdToast, $parse, $http) {
$router.config([{
path: '/',
component: 'home'
}, {
path: '/home',
component: 'home'
}, {
path: '/about',
component: 'about'
}, {
path: '/settings',
component: 'settings'
}, {
path: '/article/:id',
component: 'article'
}]);
this.toggleSidenav = function(menuId) {
$mdSidenav(menuId).toggle();
};
this.navigateTo = function(link) {
var parts = link.match(/^(.+?)(?:\((.*)\))?$/);
var url;
if (parts[2]) {
url = '.' + $router.generate(parts[1], $parse(parts[2])());
} else {
url = '.' + $router.generate(parts[1]);
}
$mdToast.show($mdToast.simple().content('Navigate To: ' + url).position('bottom right'));
$router.navigate(url);
this.toggleSidenav('left');
};
});
Inside your HomeController, you are executing this.articles = Articles.query();, but your Articles service doesn't define any query function.
Instead, your service is just immediately executing an HTTP GET request upon creation. Not sure why this would lead to your error, but it is a red flag.
Try changing your Articles service to the following:
trooNewsServices.factory('Articles', ['$http',
function Articles($http){
this.query = function() {
return $http.get('resources/articles.json')
.then(function(response) { return response.data; });
};
}]);
I was experiencing the same error message under different conditions. In my case, it was because I was referencing $scope in my dependencies (old habit that I'm trying to break). In my case, I wasn't using $scope and could easily remove the reference. That cleared up my error. Check your code for $scope references and see if that fixes it.
https://github.com/angular/router/issues/313
and
How can we watch expressions inside a controller in angular 1.4 using angular-new-router

How do I poll a database and populate a controller using AngularJs, Ui-Router, and MongoDB?

I've been trying to get my Angular app to populate the controller with data from a database (mongodb) before the page loads. I can't quite get it to work. I'm trying to use the "resolve" property of angular ui-router but it's not working and I can't figure it out!!
Here's the full code for my app:
var freezerApp = angular.module('freezerApp', ['ui.router']);
freezerApp.config([
'$stateProvider','$urlRouterProvider',function($stateProvider,$urlRouterProvider) {
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/partials/home.html',
controller: 'freezerCtrl',
});
$stateProvider
.state('freezer', {
url: '/freezers',
templateUrl: 'partials/freezers.html',
controller: 'freezerCtrl',
//not working:
resolve: {
freezerPromise: function($scope){
return $scope.getAll();
}
};
});
$urlRouterProvider.otherwise('home');
}]);
freezerApp.controller('freezerCtrl', ['$scope', '$http', function ($scope,$http) {
$scope.freezer =
{'freezername':'Freezer Name',
'building':'Building',
'floor':'Floor',
'room':'Room',
'shelves': 0,
'racks': 0
};
$scope.add_freezer = function() {
$scope.freezers.push(
{'freezername': $scope.freezer.freezername,
'building':$scope.freezer.building,
'floor':$scope.freezer.floor,
'room':$scope.freezer.room,
'shelves': $scope.freezer.shelves,
'racks': $scope.freezer.racks
}
);
};
$scope.freezers = [
{
}
];
$scope.default_freezer = $scope.freezers[0];
$scope.getAll = function() {
return $http.get('/freezers').success(function(data){
angular.copy(data, $scope.freezers);
});
};
}]);
According to the Ui-Router Resolve Documentation:
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
It looks like you are trying to provide $scope object from your freezerCtrl to your resolve property. This is incorrect.
I would recommend you create a factory like so for your api call.
angular.module.('freezerApp').factory('freezerFact',function($http){
return {
getAll: $http.get('/freezers')
}
});
Then inside of your freezer $state deceleration you could do it like this:
$stateProvider
.state('freezer', {
url: '/freezers',
templateUrl: 'partials/freezers.html',
controller: 'freezerCtrl',
resolve: {
freezerPromise: function(freezerFact){
return freezerFact.getAll;
}
};
});
Then you would pass the freezerPromise object into your freezerCtrl and manipulate the promise after that.

Angular UI-Router verify onEnter in test

I am in the middle of writing some tests for my application (AngularJS).
As we speak I encountered a problem with verifying if onEnter property of my state was called correctly.
Let me share some code with You
describe('Midway :: routesTest', function () {
var $state,
$rootScope,
$injector,
navigationService;
beforeEach(function () {
module('springatom', function ($provide) {
$provide.value('navigationService', navigationService = {});
});
states.configure();
inject(function (_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
// We need add the template entry into the templateCache if we ever
// specify a templateUrl
$templateCache.put('/static/sa/views/home/home.html', '');
$templateCache.put('/static/sa/tpls/grid.html', '');
});
navigationService.getNavigationModel = jasmine.createSpy('getNavigationModel').and.returnValue([]);
navigationService.setNavigatorModel = jasmine.createSpy('setNavigatorModel').and.callFake(function (arg) {
});
});
it("should have a working home route", inject(function () {
var homeState = $state.get('home');
expect(homeState).toBeDefined();
expect($state.href(homeState)).toEqual('#/sa');
$rootScope.$apply(function () {
$state.go(homeState);
});
var current = $state.current;
expect($injector.invoke(current.resolve.actionModel)).toEqual([]);
expect($injector.invoke(current.onEnter)).toHaveBeenCalled();
}));
});
The failing assertion is the last one I am trying to verify therefore mentioned onEnter.
Error is:
Error: [$injector:unpr] Unknown provider: actionModelProvider <- actionModel
http://errors.angularjs.org/1.3.8/$injector/unpr?p0=actionModelProvider%20%3C-%20actionModel
As it is expected Angular tries to resolve actionModel which is the property from the resolve.
I dont know what I might be doing wrong here, so any help will be gladly welcomed.
I am attaching the state configuration as well:
define(
[
'views/home/homeController',
'views/home/recentlyUpdatedController',
// angular deps
'services/navigation'
],
function homeState(homeController, recentlyUpdatedController) {
return {
name : 'home',
definition: {
url : '/sa',
templateUrl: '/static/sa/views/home/home.html',
resolve : {
actionModel: function (navigationService) {
return navigationService.getNavigationModel('main.navigation')
}
},
onEnter : function (actionModel, navigationService) {
navigationService.setNavigatorModel('main.navigation');
},
views : {
'': {
controller : recentlyUpdatedController,
templateUrl: '/static/sa/tpls/grid.html'
}
}
}
}
}
);

Delaying AngularJS route change until model loaded to prevent flicker

I am wondering if there is a way (similar to Gmail) for AngularJS to delay showing a new route until after each model and its data has been fetched using its respective services.
For example, if there were a ProjectsController that listed all Projects and project_index.html which was the template that showed these Projects, Project.query() would be fetched completely before showing the new page.
Until then, the old page would still continue to show (for example, if I were browsing another page and then decided to see this Project index).
$routeProvider resolve property allows delaying of route change until data is loaded.
First define a route with resolve attribute like this.
angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: PhoneListCtrl,
resolve: PhoneListCtrl.resolve}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: PhoneDetailCtrl,
resolve: PhoneDetailCtrl.resolve}).
otherwise({redirectTo: '/phones'});
}]);
notice that the resolve property is defined on route.
function PhoneListCtrl($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}
PhoneListCtrl.resolve = {
phones: function(Phone, $q) {
// see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
var deferred = $q.defer();
Phone.query(function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(); // you could optionally pass error data here
});
return deferred.promise;
},
delay: function($q, $defer) {
var delay = $q.defer();
$defer(delay.resolve, 1000);
return delay.promise;
}
}
Notice that the controller definition contains a resolve object which declares things which should be available to the controller constructor. Here the phones is injected into the controller and it is defined in the resolve property.
The resolve.phones function is responsible for returning a promise. All of the promises are collected and the route change is delayed until after all of the promises are resolved.
Working demo: http://mhevery.github.com/angular-phonecat/app/#/phones
Source: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
Here's a minimal working example which works for Angular 1.0.2
Template:
<script type="text/ng-template" id="/editor-tpl.html">
Editor Template {{datasets}}
</script>
<div ng-view>
</div>
JavaScript:
function MyCtrl($scope, datasets) {
$scope.datasets = datasets;
}
MyCtrl.resolve = {
datasets : function($q, $http) {
var deferred = $q.defer();
$http({method: 'GET', url: '/someUrl'})
.success(function(data) {
deferred.resolve(data)
})
.error(function(data){
//actually you'd want deffered.reject(data) here
//but to show what would happen on success..
deferred.resolve("error value");
});
return deferred.promise;
}
};
var myApp = angular.module('myApp', [], function($routeProvider) {
$routeProvider.when('/', {
templateUrl: '/editor-tpl.html',
controller: MyCtrl,
resolve: MyCtrl.resolve
});
});​
​
http://jsfiddle.net/dTJ9N/3/
Streamlined version:
Since $http() already returns a promise (aka deferred), we actually don't need to create our own. So we can simplify MyCtrl. resolve to:
MyCtrl.resolve = {
datasets : function($http) {
return $http({
method: 'GET',
url: 'http://fiddle.jshell.net/'
});
}
};
The result of $http() contains data, status, headers and config objects, so we need to change the body of MyCtrl to:
$scope.datasets = datasets.data;
http://jsfiddle.net/dTJ9N/5/
I see some people asking how to do this using the angular.controller method with minification friendly dependency injection. Since I just got this working I felt obliged to come back and help. Here's my solution (adopted from the original question and Misko's answer):
angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: PhoneListCtrl,
resolve: {
phones: ["Phone", "$q", function(Phone, $q) {
var deferred = $q.defer();
Phone.query(function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(); // you could optionally pass error data here
});
return deferred.promise;
]
},
delay: ["$q","$defer", function($q, $defer) {
var delay = $q.defer();
$defer(delay.resolve, 1000);
return delay.promise;
}
]
},
}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: PhoneDetailCtrl,
resolve: PhoneDetailCtrl.resolve}).
otherwise({redirectTo: '/phones'});
}]);
angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}]);
Since this code is derived from the question/most popular answer it is untested, but it should send you in the right direction if you already understand how to make minification friendly angular code. The one part that my own code didn't requires was an injection of "Phone" into the resolve function for 'phones', nor did I use any 'delay' object at all.
I also recommend this youtube video http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp , which helped me quite a bit
Should it interest you I've decided to also paste my own code (Written in coffeescript) so you can see how I got it working.
FYI, in advance I use a generic controller that helps me do CRUD on several models:
appModule.config ['$routeProvider', ($routeProvider) ->
genericControllers = ["boards","teachers","classrooms","students"]
for controllerName in genericControllers
$routeProvider
.when "/#{controllerName}/",
action: 'confirmLogin'
controller: 'GenericController'
controllerName: controllerName
templateUrl: "/static/templates/#{controllerName}.html"
resolve:
items : ["$q", "$route", "$http", ($q, $route, $http) ->
deferred = $q.defer()
controllerName = $route.current.controllerName
$http(
method: "GET"
url: "/api/#{controllerName}/"
)
.success (response) ->
deferred.resolve(response.payload)
.error (response) ->
deferred.reject(response.message)
return deferred.promise
]
$routeProvider
.otherwise
redirectTo: '/'
action: 'checkStatus'
]
appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->
$scope.items = items
#etc ....
]
This commit, which is part of version 1.1.5 and above, exposes the $promise object of $resource. Versions of ngResource including this commit allow resolving resources like this:
$routeProvider
resolve: {
data: function(Resource) {
return Resource.get().$promise;
}
}
controller
app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {
$scope.data = data;
}]);
This snippet is dependency injection friendly (I even use it in combination of ngmin and uglify) and it's a more elegant domain driven based solution.
The example below registers a Phone resource and a constant phoneRoutes, which contains all your routing information for that (phone) domain. Something I didn't like in the provided answer was the location of the resolve logic -- the main module should not know anything or be bothered about the way the resource arguments are provided to the controller. This way the logic stays in the same domain.
Note: if you're using ngmin (and if you're not: you should) you only have to write the resolve functions with the DI array convention.
angular.module('myApp').factory('Phone',function ($resource) {
return $resource('/api/phone/:id', {id: '#id'});
}).constant('phoneRoutes', {
'/phone': {
templateUrl: 'app/phone/index.tmpl.html',
controller: 'PhoneIndexController'
},
'/phone/create': {
templateUrl: 'app/phone/edit.tmpl.html',
controller: 'PhoneEditController',
resolve: {
phone: ['$route', 'Phone', function ($route, Phone) {
return new Phone();
}]
}
},
'/phone/edit/:id': {
templateUrl: 'app/phone/edit.tmpl.html',
controller: 'PhoneEditController',
resolve: {
form: ['$route', 'Phone', function ($route, Phone) {
return Phone.get({ id: $route.current.params.id }).$promise;
}]
}
}
});
The next piece is injecting the routing data when the module is in the configure state and applying it to the $routeProvider.
angular.module('myApp').config(function ($routeProvider,
phoneRoutes,
/* ... otherRoutes ... */) {
$routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });
// Loop through all paths provided by the injected route data.
angular.forEach(phoneRoutes, function(routeData, path) {
$routeProvider.when(path, routeData);
});
$routeProvider.otherwise({ redirectTo: '/' });
});
Testing the route configuration with this setup is also pretty easy:
describe('phoneRoutes', function() {
it('should match route configuration', function() {
module('myApp');
// Mock the Phone resource
function PhoneMock() {}
PhoneMock.get = function() { return {}; };
module(function($provide) {
$provide.value('Phone', FormMock);
});
inject(function($route, $location, $rootScope, phoneRoutes) {
angular.forEach(phoneRoutes, function (routeData, path) {
$location.path(path);
$rootScope.$digest();
expect($route.current.templateUrl).toBe(routeData.templateUrl);
expect($route.current.controller).toBe(routeData.controller);
});
});
});
});
You can see it in full glory in my latest (upcoming) experiment.
Although this method works fine for me, I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object; it would make things soooOOOOOooOOOOO much easier.
Edit: used Angular v1.2(rc2)
Delaying showing the route is sure to lead to an asynchronous tangle... why not simply track the loading status of your main entity and use that in the view. For example in your controller you might use both the success and error callbacks on ngResource:
$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
$scope.httpStatus = 200;
}, function(response) {
$scope.httpStatus = response.status;
});
Then in the view you could do whatever:
<div ng-show="httpStatus == 0">
Loading
</div>
<div ng-show="httpStatus == 200">
Real stuff
<div ng-repeat="project in projects">
...
</div>
</div>
<div ng-show="httpStatus >= 400">
Error, not found, etc. Could distinguish 4xx not found from
5xx server error even.
</div>
I worked from Misko's code above and this is what I've done with it. This is a more current solution since $defer has been changed to $timeout. Substituting $timeout however will wait for the timeout period (in Misko's code, 1 second), then return the data hoping it's resolved in time. With this way, it returns asap.
function PhoneListCtrl($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}
PhoneListCtrl.resolve = {
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
}
Using AngularJS 1.1.5
Updating the 'phones' function in Justen's answer using AngularJS 1.1.5 syntax.
Original:
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
Updated:
phones: function(Phone) {
return Phone.query().$promise;
}
Much shorter thanks to the Angular team and contributors. :)
This is also the answer of Maximilian Hoffmann. Apparently that commit made it into 1.1.5.
You can use $routeProvider resolve property to delay route change until data is loaded.
angular.module('app', ['ngRoute']).
config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
$routeProvider.
when('/entities', {
templateUrl: 'entities.html',
controller: 'EntitiesCtrl',
resolve: EntitiesCtrlResolve
}).
when('/entity/:entityId', {
templateUrl: 'entity.html',
controller: 'EntityCtrl',
resolve: EntityCtrlResolve
}).
otherwise({redirectTo: '/entities'});
}]);
Notice that the resolve property is defined on route.
EntitiesCtrlResolve and EntityCtrlResolve is constant objects defined in same file as EntitiesCtrl and EntityCtrl controllers.
// EntitiesCtrl.js
angular.module('app').constant('EntitiesCtrlResolve', {
Entities: function(EntitiesService) {
return EntitiesService.getAll();
}
});
angular.module('app').controller('EntitiesCtrl', function(Entities) {
$scope.entities = Entities;
// some code..
});
// EntityCtrl.js
angular.module('app').constant('EntityCtrlResolve', {
Entity: function($route, EntitiesService) {
return EntitiesService.getById($route.current.params.projectId);
}
});
angular.module('app').controller('EntityCtrl', function(Entity) {
$scope.entity = Entity;
// some code..
});
I like darkporter's idea because it will be easy for a dev team new to AngularJS to understand and worked straight away.
I created this adaptation which uses 2 divs, one for loader bar and another for actual content displayed after data is loaded. Error handling would be done elsewhere.
Add a 'ready' flag to $scope:
$http({method: 'GET', url: '...'}).
success(function(data, status, headers, config) {
$scope.dataForView = data;
$scope.ready = true; // <-- set true after loaded
})
});
In html view:
<div ng-show="!ready">
<!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
<div class="progress progress-striped active">
<div class="bar" style="width: 100%;"></div>
</div>
</div>
<div ng-show="ready">
<!-- Real content goes here and will appear after loading -->
</div>
See also: Boostrap progress bar docs
I liked above answers and learned a lot from them but there is something that is missing in most of the above answers.
I was stuck in a similar scenario where I was resolving url with some data that is fetched in the first request from the server. Problem I faced was what if the promise is rejected.
I was using a custom provider which used to return a Promise which was resolved by the resolve of $routeProvider at the time of config phase.
What I want to stress here is the concept of when it does something like this.
It sees the url in url bar and then respective when block in called controller and view is referred so far so good.
Lets say I have following config phase code.
App.when('/', {
templateUrl: '/assets/campaigns/index.html',
controller: 'CampaignListCtr',
resolve : {
Auth : function(){
return AuthServiceProvider.auth('campaign');
}
}
})
// Default route
.otherwise({
redirectTo: '/segments'
});
On root url in browser first block of run get called otherwise otherwise gets called.
Let's imagine a scenario I hit rootUrl in address bar AuthServicePrivider.auth() function gets called.
Lets say Promise returned is in reject state what then???
Nothing gets rendered at all.
Otherwise block will not get executed as it is for any url which is not defined in the config block and is unknown to angularJs config phase.
We will have to handle the event that gets fired when this promise is not resolved. On failure $routeChangeErorr gets fired on $rootScope.
It can be captured as shown in code below.
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
// Use params in redirection logic.
// event is the routeChangeEvent
// current is the current url
// previous is the previous url
$location.path($rootScope.rootPath);
});
IMO It's generally a good idea to put event tracking code in run block of application. This code run just after the config phase of the application.
App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
$rootScope.rootPath = "my custom path";
// Event to listen to all the routeChangeErrors raised
// by the resolve in config part of application
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
// I am redirecting to rootPath I have set above.
$location.path($rootScope.rootPath);
});
}]);
This way we can handle promise failure at the time of config phase.
I have had a complex multi-level sliding panel interface, with disabled screen layer. Creating directive on disable screen layer that would create click event to execute the state like
$state.go('account.stream.social.view');
were producing a flicking effect. history.back() instead of it worked ok, however its not always back in history in my case. SO what I find out is that if I simply create attribute href on my disable screen instead of state.go , worked like a charm.
<a class="disable-screen" back></a>
Directive 'back'
app.directive('back', [ '$rootScope', function($rootScope) {
return {
restrict : 'A',
link : function(scope, element, attrs) {
element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
}
};
} ]);
app.js I just save previous state
app.run(function($rootScope, $state) {
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
$rootScope.previousState = fromState.name;
$rootScope.currentState = toState.name;
});
});
One possible solution might be to use the ng-cloak directive with the element where we are using the models e.g.
<div ng-cloak="">
Value in myModel is: {{myModel}}
</div>
I think this one takes least effort.

Categories

Resources