I'm writing my first AngularJS application. The landing page has two links #/view1 and #/view2. Every link will call individual controller to render <div ng-view></div> individually.
The view1Ctrl will fetch data from server periodically. I can see ajax call in console every X seconds. Now I click #/view2, the app should use view2Ctrl. I expect view1Ctrl should no longer fetch data from the server. But actually it does.
.controller('View1Ctrl', function($scope, $http) {
$scope.update = function() {
$http.get('/api/foo')
.success(function(data) { /* got new data */ })
.error(function(data) { /* error occurred */ })
.finally(function() {
setTimeout($scope.update, 1000);
});
}
$scope.update();
});
I've two questions:
Is a controller always active after it is initialized.
What is the best practice to stop any background controllers? My idea is to check the $location.path().
Update 1:
The controller is actually destroyed. But the operation update will invoke it by itself, so the operation will be called endless.
My current workaround will check whether the current location has been change. So it works. I'm looking for a more elegant way, if it exists.
.controller('View1Ctrl', function($scope, $http) {
$scope.update = function(path) {
$http.get('/api/foo')
.success(function(data) { /* got new data */ })
.error(function(data) { /* error occurred */ })
.finally(function() {
if (path === $location.path()) {
setTimeout($scope.update, 1000, path);
}
});
}
$scope.update($location.path());
});
Update 2:
A better way is to watch destruction and cancel an ongoing timeout promise. Thanks #pankajparkar
.controller('View1Ctrl', function($scope, $http, $timeout) {
var timeoutPromise;
$scope.$on("$destroy", function(){
$timeout.cancel(timeoutPromise);
});
$scope.update = function() {
$http.get('/api/foo')
.success(function(data) { /* got new data */ })
.error(function(data) { /* error occurred */ })
.finally(function() {
timeoutPromise = $timeout($scope.update, 1000);
});
}
$scope.update();
});
I think you are mentioned ng-controller on the body tag or the parent of your ng-view, In your case you should load the controller from your $routeProvider that will handle it,
Config
app.config(function($routeProvider) {
$routeProvider
.when('/view1', {
templateUrl: 'view1.html',
controller: 'view1Ctrl' //load specific controller from here
})
.when('/view2', {
templateUrl: 'view2.html',
controller: 'view2Ctrl' //load specific controller from here
})
.otherwise({
redirectTo: '/view1'
});
});
HTML
<div class="wizard">
<ng-view></ng-view>
</div>
Below are the answer of your questions.
Now when $routeProvider load view1.html the view1Ctrl will exists, as soon as user navigates to view2.html the view1Ctrl will stop its working view2Ctrl will comes into a picture as you mentioned that inside your route configuration.
You should always use $routeProvider or $stateProvider(angular-ui-router) to set up routing inside you application, and load templates and controller on basis of routing. No need to check $location.path()
Working Plunkr
Update
Your problem was with the repetitive function call, which was running in background even after controller scope has vanished from DOM.
The best way to clear out this function while redirecting to other controller.
I'd suggest you to put your $timeout promise(you used setTimeout changed it to $timeout) in one java script variable, and cancel that while leaving(unregistered the controller). While leaving controller $destroy event gets dispatched by angular, which is act like destruct-or of controller, you can use of it, you could cancel your $timeout promise inside that.
CODE
var timeoutPromise = setTimeout($scope.update, 1000);
$scope.$on("$destroy", function(){
$timeout.cancel(timeoutPromise);
});
Plunkr with canceling promise of $timeout.
Reference Link
Hope this could help you, Thanks.
Related
I have two templates as a.html and b.html. Now a.html is for logged in users and b.html for people who are not. Authentication is through an api running on a separate sub-domain. Now i have a call that tells whether a person is authenticated in scope.
How can i optionally load templates based on that. One method is i load b.html and conditionally redirect authenticated users but that is not something i am looking for.
Here is my ngRoute codes
var app = angular.module('app',['ngRoute']);
app.config(function ($routeProvider,$locationProvider) {
$locationProvider.html5Mode(true);
$routeProvider
.when('/',{
templateUrl : '/templates/b.html',
controller : 'mainController'
})
.otherwise({
templateUrl : 'templates/404.html',
controller : 'mainController'
});
});
app.controller('mainController', function($scope) {
});
The following code could somehow satisfy your requirements but it is in fact a bit tricky since i used ng-include instead of directly setting the templateUrl.
The key is to use the resolve param in $routeProvider. when u pass a $promise as a member in it, the router will wait for them all to be resolved.
app.controller('MainController', ['$scope','loadedTemplate',function ($scope,loadedTemplate) {
//inject the template from the resolve function
$scope.loadedTemplate = loadedTemplate;
}]);
//Service to keep loginStatus
app.factory("loginService",function(){
return {
isLogin:false,
loginStatusFetched:false,
}
})
app.config(['$routeProvider',function ($routeProvider) {
$routeProvider.
when('/', {
template: "<div ng-include='loadedTemplate'></div>",
controller: 'MainController',
resolve:{
loadedTemplate:function($q, $http,$route,loginService) {
console.log("fetching login status");
if(loginService.loginStatusFeteched){
if(loginService.isLogin){
return "a.html"
}
else{
return "b.html"
}
}
console.log("fetching remote authentication");
var deferred = $q.defer();
$http({method: 'GET', url: 'http://jsonplaceholder.typicode.com/posts/1'})
.success(function(data) {
//update the login status here
loginService.loginStatusFeteched = true;
loginService.isLogin = true;
if(loginService.isLogin){
deferred.resolve("a.html");
}
else{
deferred.resolve("b.html");
}
})
.error(function(data){
//error
deferred.resolve("b.html");
});
return deferred.promise;
}
}
})
}]);
I attempted to achieve what u required by setting the templateUrl to a function, but it only takes routeParams, and when the $promise in resolve get resolved, this function has already been executed and there is no way to change the current loaded template.
There is still other choices that you can try. You may use angular-ui-router instead of ng-route to handle state changes, in which u may set child states and present them with the same url conditionally.
But I believe there might be better ways to achieve what you want. If anyone has any better solution, please let me know.
I try to load route only after promises are resolved
var app = angular.module("thethaoso", ["ngRoute"]);
app.config(['$routeProvider', '$locationProvider', function ($routeProvider) {
$routeProvider
.when('/', {
resolve: {
message: function (repoService) {
return repoService.getMsg();
}
}
});
}]);
app.factory('repoService', function ($http) {
return {
getMsg: function () {
return "hihihi";
}
};
});
app.controller('teamLoadCtrl', function ($scope,message) {
$scope.message= message;
});
View:
<div ng-app='thethaoso' ng-controller='teamLoadCtrl'>
{{message}}
</div>
Always get the error Error: [$injector:unpr]http://errors.angularjs.org/1.3.7/$injector/unpr?p0=messageProvider%20%3C-%20message%20%3C-%20teamLoadCtrl
full code at http://jsfiddle.net/c0y38yp0/5/
Am I missing something ?
Thanks all.
The problem is that you have not specified a template and a controller to resolve the message object to. If you used the following syntax, it will work.
.when("/", {
templateUrl: "yourView.html",
controller: "yourController",
resolve: {
message: function(yourService){
return yourService.get();
}
}
Here is a working jsfiddle: http://jsfiddle.net/c0y38yp0/10/
You can also resolve the promise manually in your controller like so:
repoService.getMsg()
.then(function (msg) {
$scope.message = msg;
}
When the promise is resolved onto the scope as I did above, the ui will update. You can show a loading bar and use ng-hide to make the pages feel fluent while the loading occurs.
When you resolve, service have to return promise not value.
Here is example service
app.factory('repoService', function ($http, $q) {
var user = {};
var q = $q.defer();
$http.get('https://api.github.com/users/Serhioromano')
.success(function(json){
user = json;
q.resolve();
}).error(function(){
q.reject();
});
return {
promise: function() {
return q.promise;
},
get: function() {
return user;
}
};
});
The point here is that you return promise only. You handle how you save result. And then you can use this result like in get(). You know that by the time you call get() the user variable already set because promise was resolve.
Now in router.
app.controller('MainCtrl', function($scope, repoService) {
$scope.user = repoService.get();
});
app.config(function ($routeProvider, $locationProvider) {
$routeProvider
.when('/', {
templateUrl: '/view.html',
controller: 'MainCtrl',
resolve: {
message: function (repoService) {
return repoService.promise();
}
}
})
.otherwise({ redirectTo: '/' });
});
You return promise by repoService.promise()
In controller repoService.get() is triggered only after that promise resolved.
So you get your data.
Another thing in your code, you used ng-controller. But that thing is not binded to router and thus it avoid if it is resolved or not. You have to delete ng-controller and use controller router controller: 'MainCtrl',.
This affect your HTML
<body ng-app="myapp">
<ng-view></ng-view>
<script type="text/ng-template" id="/view.html">
<p>Hello {{user.name}}!</p>
</script>
<body>
You have to use <ng-view> to include subtemplate there and then in sub template you can use scope of the controller.
See plunker.
There are a few things wrong with the code you posted, in contrast to the code you are attempting to draw inspiration from.
When you resolve a route with the $routeProvider, the results are applied against an element <ng-view></ngview>, not a base element <div> as you have specified. Without the <ng-view> element, there is no way for the $routeProvider to bind the correct controller to the correct html fragment. Using ng-controller instantiates a controller instance when the dom element is rendered, and does not allow passing parameters to the controller as you have tried. Thus your resolution error due to an unknown message object. Effectively, message is not available outside the $routeProvider instance.
I am new to AngularJS. I made a simple app that have a login function using AngularJS. I used routing and on resolve i put some logic to check if user is logged in and then only proceed accordingly. I have everything working fine, the problem is, when i am not logged in, if i browse to /home it doesn't load the main.html page(that's how it's supposed to be) but a GET request gets called and that returns content of main.html in console.My code looks like this:
app.config(function($routeProvider){
$routeProvider.when('/', {
templateUrl: 'partials/login.html',
controller: 'LoginCtrl',
resolve:{
test: function($http, $q,$location){
var defer = $q.defer();
//checks if user is logged and returns boolean
$http.post('login/getLoggedUser', {}, {}).success(function(data){
if(!data.logged){
defer.resolve(data);
$location.url('/');
}
else{
defer.resolve(data);
$location.url('/home')
}
});
return defer.promise;
}
}
})
.when('/home',{
templateUrl: 'partials/main.html',
controller: 'MainCtrl',
resolve:{
test: function($http, $q,$location){
var defer = $q.defer();
$http.post('login/getLoggedUser', {}, {}).success(function(data){
if(data.logged){
defer.resolve(data);
$location.url('/home');
}
else{
defer.resolve(data);
$location.url('/')
}
});
return defer.promise;
}
},
})
.otherwise({ redirectTo: '/' });
});
When i direct to /home, GET http:/localhost:8080/an-grails/partials/main.html is called in console which contains the content of main page. How do i disable this call? Is there any other method to do this? I read documentation on AngularJS official page and also watched few videos of Egghead.io about resolve and got idea that controller and template gets loaded only after resolve is processed, So what am i doing wrong?
The simplest way to manage rights in your different routes is to catch the $routeChangeStart which is fired by the $route service everytime the route is changed.
With this, you can access the actual route and the next one. This object is the same that you register with $routeProvider.when(). You just have to add a boolean and compare this boolean with the actual user status.
$rootScope.$on("$routeChangeStart", function(event, next, current) {
if (next.loggedOnly && !user.isLogged()) {
// You should implement a isLogged method to test if the user is logged
$location.replace();
// This prevent a redirect loop when going back in the browser
return $location.path("/");
}
}
And inside your route declaration use :
$routeProvider.when('/home', {
templateUrl: 'partials/main.html',
controller: 'MainCtrl',
loggedOnly: true
});
I am migrating from ng-view to ui-view.
In my controller, upon successful create / update / delete, I would like to redirect to an index view with the newly updated / created / deleted data.
I currently do this using $state.go.
app.controller('AdminSupplierCreateController', ['$scope', '$state', 'Supplier',
function ($scope, $state, Supplier) {
$scope.h1 = "Register a new Supplier";
$scope.store = function()
{
Supplier.post($scope.supplier)
.then(
function() {
console.log("Successfully Created Supplier");
$state.go('admin.supplier');
},
function(response) {
console.log("Error with status code: " + response.status);
}
);
};
}]);
The problem is that when the $state is changed, the scope is not updated. It appears as though the original admin.supplier scope data is not being refreshed.
I have tried refreshing the ui-view with $state.reload() before setting the scope in AdminSupplierIndexController.
app.controller('AdminSupplierIndexController', ['$scope', '$state', 'suppliers',
function ($scope, $state, suppliers) {
$state.reload();
$scope.suppliers = suppliers;
}]);
Doing this makes the get request to api/v1/suppliers, however it doesn't actually show the updated scope unless I either hit the refresh button on the browser or manually navigate to a different state, then navigate back again.
$stateProvider
...
.state('admin.supplier', {
url : "/supplier",
templateUrl : 'templates/admin/supplier/index.html',
controller: "AdminSupplierIndexController",
resolve: {
suppliers: ['Supplier', function(Supplier) {
return Supplier.getList();
}]
}
})
.state('admin.supplier.create', {
url : "/create",
templateUrl : 'templates/admin/supplier/create.html',
controller: "AdminSupplierCreateController"
})
Solution:
app.controller('AdminSupplierCreateController', ['$scope', '$state', 'Supplier',
function ($scope, $state, Supplier) {
$scope.h1 = "Register a new Supplier";
$scope.store = function() {
Supplier.post($scope.supplier)
.then(
function() {
console.log("Successfully Created Supplier");
// Force Reload on changing state
$state.go('admin.supplier', {}, {reload: true});
},
function(response) {
console.log("Error with status code: ", response.status);
}
);
};
}]);
This happens because you move to parent state which is already created. You would have to signal to ui-router that you do want to reload it.
I think you might be looking for the reload option of $state.go(). Refer to the docs.
Also, a sort of a workaround might be to move the "index" state to a sibling state of edit. admin.supplier.index or so. Then it would be destroyed as you transition to admin.supplier.edit and recreated when you transition back and no additional options are necessary.
Also as a side note I guess it would be considered good practice to use $state.go instead of $location whenever possible.
I have the following code under my AngularJS .run which works perfectly on my local development machine but won't work when uploaded to my client server...after few tests it is obvious that by the time the controller is loaded the event is not triggered yet so most of the functions in the controller depending on this event do not work. Can someone please tell me what I am doing wrong here and how to fix it? Thanks
myApp.run(['AuthDataSvc', '$rootScope', function (AuthDataSvc, $rootScope) {
AuthDataSvc.getAuth().then(function(Token){
$rootScope.$broadcast('Token', Token);
}, function(status){
console.log(status);
});
}]);
You are always going to have a race condition. I have a couple alternatives you can do:
1) Use a service. I am not really a fan of this option because it leads to Spaghetti code. And most time you don't want the controller to run until you have logged on. I prefer option 2.
myApp.run(['AuthDataSvc', '$rootScope', function (AuthDataSvc, $rootScope) {
AuthDataSvc.getAuth(); /* no op we will use the service to determine logged in */
}]);
/* inside a controller */
if(AuthDataSvc.isLoggedIn()){
//do something.
}
2) Use a route.resolve. Resolves are defined on the route and the Controller will only load once it the promise has been set to resolved. I showed you an example for ui-router and ng-route you need to pick your poison. If you dont use ui-router you should consider it.
/* app.config ... route config.. */
var waitForLogon = {
UserToken: ["AuthDataSvc", function (AuthDataSvc) {
return AuthDataSvc.logon();
}]
};
//this is for ng-route
$routeProvider
.when('/Book/:bookId', {
templateUrl: '--',
controller: 'MyCtrl',
resolve: waitForLogon
})
//this is for ui-router
$stateProvider
.state('me', {
templateUrl: '--',
controller: 'MeCtrl',
resolve: waitForLogon
})
/* controller */
angular.module('yourapp')
.controller('MyCtrl', ["UserToken", ... , function(UserToken){
//User Token will always be here when your Ctrl loads.
});
/* service code -- */
angular.module('yourapp')
.service('AuthDataSvc', ["LogonModel", "$q", function(LogonModel, $q) {
this._q = null;
var that = this;
this._doAuth = function(){
this.getAuth().then(function(Token){ that._q.resolve(Token) }, function(error){that._q.reject(error);}
};
this.logon = function () {
if(!this._q){
this._q = $q.defer();
this._doAuth();// <-current auth do here, and resolve this._q when done
}
return this._q.promise;
};
});