angular-ui ui-router controller not getting instantiated with $state.go - angularjs

I am trying to build an app which on login changes the state to a view with $state.go but when calling $state.go the controller is not instantiated as defined for that state.
Here is my state change logic (removing other code for brevity):
config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('login', {
url: "/",
templateUrl: "templates/login.html",
controller: 'LoginController'
})
// setup an abstract state for the tabs directive
.state('tab', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html"
})
// Each tab has its own nav history stack:
.state('tab.dash', {
url: '/dash',
views: {
'tab-dash': {
templateUrl: 'templates/tab-dash.html',
controller: 'DashboardController'
}
},
resolve: {
authenticated: ['RestService', function(restService) {
return restService.authenticationStatus();
}],
sessionService: ['SessionService', function(sessionService) {
return sessionService;
}]
}
})
My LoginController is something like:
// Perform the login action when the user submits the login form
$scope.doLogin = function(loginForm) {
if (loginForm.$invalid) {
$log.debug('Invalid Form...', $scope.loginData);
return;
}
$log.debug('Doing login', $scope.loginData);
RestService.login($scope.loginData)
.then(function(data) {
$log.debug("Inside loginController...");
$log.debug(data);
$scope.$state.go("tab.dash");
}, function(data) {
$log.debug(data);
$scope.formErrors = data.errors;
});
};
And my DashboardController is something like:
angular.module('starter.controller.dashboard', [])
.controller('DashboardController', ['$scope', '$log', '$http',
function($scope, $log, $http) {
$log.debug("reaching here..................");
$log.debug($scope.authenticated);
}]);
Now when the login succeeds, the state is transitioned to /tab/dash but the controller is not instantiated i.e. the debug logs in DashboardController are not printed. If I directly navigate to /tab/dash then the controller does get instantiated and I do see the logs getting printed.
Moreover the value of "authenticated" passed via resolve in state definition is not available via scope in templates.

Well turns out that the controller is getting instantiated but it gets instantiated only once. If I don't refresh the page while testing and just change the path then since the controller is already instantiated, it is not instantiated again. Only if I refresh the page (LoginPage) and then navigate to Dashboard page (via $state.go on logging in) the controller gets instantiated again.
And the issue with resolve data not available in the controller is because my assumption was that it is auto injected in $scope but actually it is not so. The resolve params get injected explicitly via the passed params to constructor function of controller and then one needs to assign the values manually in the scope. Something like:
.controller('DashboardController', ['$rootScope', '$scope', '$log', '$ionicLoading', '$http', 'authenticated',
function($rootScope, $scope, $log, $ionicLoading, $http, authenticated) {
$scope.authenticated = authenticated;
}]);

Related

Angular ui.router. Deep nested routes

Here is an example to check http://embed.plnkr.co/uVMlkk/preview
When we navigate to 'page2' route there is a 'hey, I'm a subroute' note.
But once we navigate anywhere else that note will disappear forever.
The goal is to make some nested states to be shown right away (as a default ones).
I assume there should be some cases using $state.go(), but can't figure it out so far. Any help is highly appreciated.
State definition snippet:
.state('root.page2.tab', {
url: '/:tabId',
templateUrl: 'tpl.page2.tab.html',
controller: 'Page2TabController'
})
.state('root.page2.tab.subroute', {
url: '',
templateUrl: 'tpl.page2.tab.subroute.html'
})
the content of the 'tpl.page2.tab.subroute.html':
hey, I'm a subroute
related controller:
.controller('Page2TabController', ['$scope', '$state', function($scope, $state) {
$scope.tabId = $state.params.tabId;
$state.go('root.page2.tab.subroute');
}])
There is a fixed version.
I removed the url from the 'root.page2.tab.subroute'
.state('root.page2.tab.subroute', {
//url: '',
templateUrl: 'tpl.page2.tab.subroute.html'
})
And because the parent has defined paramater tabId:
.state('root.page2.tab', {
url: '/:tabId',
templateUrl: 'tpl.page2.tab.html',
controller: 'Page2TabController'
})
We have to pass that param inside of the redicrection:
.controller('Page2TabController', ['$scope', '$state', function($scope, $state) {
$scope.tabId = $state.params.tabId;
// instead of this
// $state.go('root.page2.tab.subroute');
// we need this
$state.go('root.page2.tab.subroute', $state.params);
}])
Check the working, fixed version here
ANOTHER approach - using redirectTo - there is a working plunker
One way, inspired by this:
Redirect a state to default substate with UI-Router in AngularJS
could be to add a very smart but small redirect code snippet:
.run(['$rootScope', '$state', function($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function(evt, to, params) {
if (to.redirectTo) {
evt.preventDefault();
$state.go(to.redirectTo, params)
}
});
}])
And adjust our state like this:
.state('root.page2.tab', {
url: '/:tabId',
templateUrl: 'tpl.page2.tab.html',
controller: 'Page2TabController',
redirectTo: 'root.page2.tab.subroute',
})
Check it here
There is a trick how to handle scenarios:
Parent should trigger some action in case that
it is accessed, or
its reached again, when navigating back from child in a parent state
In that case, we can use the "target (ui-view) for a child" as a place where sits the special view, with special controller. This will be
injected into that position once parent is created and
re-injected into that position again, once child is left. In that case, it will be re-init.
Enough explanation. There is a working plunker. There is adjusted state:
.state('root.page2', {
url: '/page2',
views: {
'content#root': {
templateUrl: './tpl.page2.html',
controller: 'Page2Controller'
},
'#root.page2': {
template: '<div></div>',
controller: 'RedirectorController'
}
}
})
So, now we can do some magic inside of our 'RedirectorController'
.controller('RedirectorController', ['$scope', '$state',
function($scope, $state) {
$state.go('root.page2.tab', { tabId: $scope.activeTabId });
}])
Check it in action here
Read more about what that new view/controller get from the other (Scope Inheritance by View Hierarchy Only) one here
Nested states or views for layout with leftbar in ui-router?
How do I share $scope data between states in angularjs ui-router?

ui-router resolve is not working with the index page controller

I want to resolve some value before I load the first page of my application, but it kept telling me
Unknown provider: programClassSummaryProvider <- programClassSummary <- HomeCtrl
I pretty sure I did it correctly, because I did the same thing for any other controller and routing. but it is not working for my homepage controller.
It seems like it load the controller first, before it is resolved in the routing. Anything wrong with my code?
In routing.js
$stateProvider
.state('home', {
url: '/home',
controller: 'HomeCtrl',
controllerAs: 'vm',
templateUrl: 'index_main.html',
resolve: {
programClassSummary: ['GroupDataFactory', function (groupDf) {
return groupDf.getProgramClassSummary();
}]
},
ncyBreadcrumb: {
skip: true
}
});
in controller.js
angular
.module('issMccApp')
.controller('HomeCtrl', homeCtrl);
homeCtrl.$inject = ['$scope', '$location', '$state', '$auth', 'programClassSummary'];
/* #ngInject */
function homeCtrl($scope, $location, $state, $auth, programClassSummary) {
var vm = this;
vm.isAuthenticated = isAuthenticated;
vm.programClassSummary = programClassSummary;
if (!$auth.isAuthenticated()) {
$state.go('login');
return;
}
function isAuthenticated() {
return $auth.isAuthenticated();
}
}
in factory.js
function getProgramClassSummary(showAll) {
var query = "";
if (showAll)
query = APIConfigObj.base_url + '/api/group/infor/programclasssummary?all=1';
else
query = APIConfigObj.base_url + '/api/group/infor/programclasssummary';
return $http.get(query)
.success(function (result) {
return result;
})
.error(function (err) {
return err;
})
}
I'd say, we really have to distinguish the UI-Router state world, and angular itself. Reason why is clearly defined here (extracted $resolve from UI-Router API documentation):
$resolve
resolve(invocables, locals, parent, self)
Resolves a set of invocables. An invocable is a function to be invoked via $injector.invoke(), and can have an arbitrary number of dependencies. An invocable can either return a value directly, or a $q promise. If a promise is returned it will be resolved and the resulting value will be used instead. Dependencies of invocables are resolved (in this order of precedence)
from the specified locals
from another invocable that is part of this $resolve call
from an invocable that is inherited from a parent call to $resolve (or recursively
from any ancestor $resolve of that parent).
There is a wroking plunker, which uses this index.html
<body ng-controller="RootCtrl">
a summary for a root:
<pre>{{summary}}</pre>
<ul>
<li>home
<li>other
</ul>
<div ui-view=""></div>
So, here we use some RootCtrl, which won't go through state machine UI-Router, it is angular basic stuff
The root controller must be defined as
.controller('RootCtrl', ['$scope', 'GroupDataFactory', function ($scope, groupDf) {
$scope.summary = groupDf.getProgramClassSummary();
}])
For a state home, we can use different approach, in fact the same as above (simplifed version below)
.state('home', {
url: "/home",
templateUrl: 'tpl.home.html',
resolve: {
programClassSummary: ['GroupDataFactory', function (groupDf) {
return groupDf.getProgramClassSummary();
}]
},
controller: 'HomeCtrl',
})
And its controller is now able to consume the locals
.controller('HomeCtrl', ['$scope', 'programClassSummary', function ($scope, summary) {
$scope.summaryForHome = summary;
}])
Check it in action here

AngularJS - Determine if the view should be displayed by an ajax call result upon the route changes

To put it simple, I would like to check out that if the session is still alive (which means user has logged in) before displaying the view of some routes, if not, then redirect to log-in view.
I have tried to listen to $routeChangeStart event from inside the log-in page, which is displayed initially by default, but user can still go to other views by typing in the routes.
Now what I am doing is:
(function() {
'use strict';
angular.module("appClubS", ["appClubS.userModule", "appClubS.productModule", "clubsServices", "ngRoute", "ui.bootstrap"])
.config(routeConfigurator);
angular.module("appClubS")
.controller("checkLoginCtrl", checkLoginController);
checkLoginController.$inject = ['$scope', '$rootScope', '$location', 'userFactory'];
routeConfigurator.$inject = ['$routeProvider', '$locationProvider'];
function routeConfigurator($routeProvider, $locationProvider) {
$routeProvider.when("/home", {
templateUrl: "views/index.html"
});
// ...
$routeProvider.when("/account-profile/my-redeem", {
templateUrl: "views/member_zone/my.redeem.html",
controller: 'checkLoginCtrl'
});
$routeProvider.when("/account-profile/my-survey", {
templateUrl: "views/member_zone/my.survey.html",
controller: 'checkLoginCtrl'
});
}
function checkLoginController($scope, $rootScope, $location, userFactory) {
var loginStatus = userFactory.loggedin();
if (!loginStatus) {
alert('Please Login First!');
$location.path('/login');
}
}
})();
But the view still gets displayed before user is navigated to log-in page.
Could anyone help? Thanks so much in advance.
You can use a resolve function
$routeProvider.when("/home", {
templateUrl: "views/index.html",
resolve:{
checkLogin: function (sessionService) {
sessionService.redirectIfLogged();
}
}
});
So this ensures your check is runned before the view is rendered

Why controller doesn't respond on route update?

I would like preserve instance of controller without reloading. I set reloadOnSearch to false and I manage route change in my controller. Here is the code.
This is example of my link next. I have defined following module.
angular.module('app.products', ['ngRoute', 'ngResource'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/:section/:view/:number', {
templateUrl: 'tpl/table.html',
controller: 'ProductsCtrl',
controllerAs: 'productsCtrl',
reloadOnSearch: false
});
}])
.controller('ProductsCtrl', ['$scope', '$routeParams', '$location', ProductsCtrl]);
Controller
function ProductsCtrl($scope, $routeParams, $location) {
$scope.$on('$routeUpdate', function () {
console.log("routeUpdate");
});
}
But the controller doesn't respond on changed route and text is not written to console output.
In the angular jargon, "search" refers only to the query string parameters part of the URL. For instance: ?key=value&page=42
And the "path" refers to the URL without that query string. Here /products/page/2 is a path.
When setting reloadOnSearch: false you're telling angular not to reload the view and the associated controller when only the query string parameters changes.
So if the path changes, for instance you navigate from /products/page/2 to /products/page/3, then the view will still be reloaded. No $routeUpdate will be fired because there is no need for that. You'll get the new parameters from $routeParams when your controller initialization function is called again.
However if the path doesn't change, but only the query string parameters do change. For instance when you navigate from /products?page=2 to /products?page=3. Then the view will not be reloaded and a $routeUpdate will be broadcast.
So the solution here would be to define page as a query string parameter instead of a path parameter:
angular.module('app.products', ['ngRoute', 'ngResource'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/:section', {
templateUrl: 'tpl/table.html',
controller: 'ProductsCtrl',
controllerAs: 'productsCtrl',
reloadOnSearch: false
});
}])
.controller('ProductsCtrl', ['$scope', '$routeParams', '$location', ProductsCtrl]);
Controller:
function ProductsCtrl($scope, $routeParams, $location) {
$scope.setupView = function setupView(section, page) {
// Setup you view here
// ...
};
$scope.$on('$routeUpdate', function () {
// This is called when only the query parameters change. E.g.: ?page=2
$scope.setupView($routeParams.section, $routeParams.page)
});
// This one is called when the the path changes and the view is reloaded.
$scope.setupView($routeParams.section, $routeParams.page)
}
Instead of $routeUpdate, try to use $routeChangeSuccess.
$scope.$on('$routeChangeSuccess', function (scope, next, current) {
console.log("Your text goes here.");
});
You can use next and current to check your previous and next route.
Hope it helps.

Controller on nested state not getting executed

I have nested states, with the parent and child state having a separate controller. But only the parent state is getting executed.
I have the url structure: #/restaurant/2/our-food
So, I want it to load the restaurant with the ID 2 and then the child controller load the 'our-food' content and take care of some other functions.
My code is:
var app = angular.module("app", ['ui.router']);
app.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/");
$stateProvider
.state('home', {
url: '/',
controller: function($scope, $stateParams) {
$scope.setRestaurant(0);
}
})
.state('restaurant', {
url: '/restaurant/:restaurantId',
controller: function($scope, $stateParams, $state) {
console.log('first');
if($stateParams.restaurantId > 0) {
$scope.setRestaurant($stateParams.restaurantId);
}
else {
$state.go('home');
}
}
})
.state('restaurant.our-food', {
url: '/our-food',
templateUrl: 'templates/our-food.html',
controller: function() {
console.log('second');
}
});
});
The controller for your 'restaurant.our-food' state is not being executed because its parent state has no template. This means there is no ui-view directive for it to attach its own template and controller. Even if your parent directive doesn't do anything other than setup some state, it needs to provide at the very least a minimal template.
Try adding the following to your 'restaurant' state and see if that makes it work for you:
template: "<div ui-view />"
This is documented in the ui-router docs:
Remember: Abstract states still need their own for their children to plug into. So if you are using an abstract state just to
prepend a url, set resolves/data, or run an onEnter/Exit function,
then you'll additionally need to set template: <ui-view />'.
https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views

Resources