I'm new to Angular and states and wrapping my head around ui-router. I've been doing it with jQuery for too long. In jQuery, I can load up something with ajax, then on the success, perhaps run another function. How do you do that with Angular?
For example, I have the following
var ivApp = angular.module('ivApp', ['ui.router']);
ivApp.config(function($urlRouterProvider, $stateProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home', {
url: '/',
templateUrl: 'partials/partial-home.html'
})
});
Which simply loads up partial-home.html into my ui-view. But how to tell it to run a function once that is done? For example, I have authenticate.js and a function authenticate(). How do I run authenticate() once 'home' state has loaded?
Additionally, can I tell angular to only load authenticate.js for this state? Or should I have already loaded it in the template. I know that if I include the script in partial-home.html (e.g. <script src="authenticate.js"></script>) chrome throws me an error about synchronous xmlhttprest being deprecated. So somhow in the config, can I declare authenticat.js as a dependency of the state or something like that?
At the moment I have worked out I can do something like:
ivApp.controller('authenticate', function($scope) {
// start authorisation
authenticate();
});
And then define the controller authenticate in my ui-router states. But is that how it's done? It works basically. My authenticate function is doing things like changing things in the DOM, but I read controllers shouldn't be used for this.
Thanks for any pointers
Let's break down into parts.
If you just want to load authenticate.js in this particular home state, use ocLazyLoad. It's one of the best way to load a resource lazily. And it works really well if ui-router too!
$stateProvider.state('index', {
url: "/", // root route
views: {
"lazyLoadView": {
controller: 'AppCtrl', // This view will use AppCtrl loaded below in the resolve
templateUrl: 'partial-home.html'
}
},
resolve: { // Any property in resolve should return a promise and is executed before the view is loaded
loadMyCtrl: ['$ocLazyLoad', function($ocLazyLoad) {
// you can lazy load files for an existing module
return $ocLazyLoad.load('js/authenticate.js');
}]
}
});
If you want to run authenticate() once the state is loaded, there are quite a number of ways to do it. One way of course is listening to the $stateChangeSuccess event, but I would avoid using it since you know, global variables, and global variables are bad. I do not want to pollute my $rootScope just because I have a really specific use case.
You can use resolve in ui-router too. Resolve is executed after the state is loaded and before the controller is instantiated. I would recommend to use this method as you can chain your promises together with ocLazyLoad, if you are using it (which you should).
Manipulating DOMs after a state is loaded? Sure, that's what templateUrl for! Design your template such that it accomadates to your authenticate() functions. If you combine it with resolve, there isn't really much of a problem separating concerns as you would already have executed authenticate() before controller is loaded.
Edit: Adding in Plnkr
You want to first lazily-load authenticate.js, and then use the function inside authenticate.js to do something. Since resolve in ui.router executes promise chains in parallel, we have to chain them up, i.e, load your jsfiles first, and then return your status of authentication.
We need to declare a deferred promise using $q service. We then return this promise in the resolve, so that you controller is listening to one promise instead of two. Here is how:
$stateProvider
.state('Home', {
templateUrl: 'home.html',
controller: 'homeCtrl',
resolve: {
//need to chain our promises since we neeed to first load the authenticate.js
//and second, execute authenticate()
loadJsAndAuth: ['$ocLazyLoad', '$q', '$injector', function($ocLazyLoad, $q, $injector) {
//declare a deferred promise
var deferred = $q.defer();
//now load the authenticate.js
$ocLazyLoad.load('authenticate.js').then(
//load successful! proceed to use our authenticate function!
function(success) {
//since we already have loaded authenticatejs, now we can inject the service and use it
var authSvc = $injector.get('authenticateSvc');
//this is just a demo on how to authenticate.
//change this to banana to see the authenticate fail
var fruits = 'apple'
if (authSvc.authenticate(fruits)) {
//authenticate pass, resolve the promise!
deferred.resolve('authenticated!');
}
//authenticate fail, reject the promise
deferred.reject('authenticate failed');
},
//load of jsfiles failed! reject the promise.
function(error) {
deferred.reject('Cannot load authenticate.js')
})
return deferred.promise;
}]
}
})
And in your controller, you can get the resolved promises!
//you can get access to what is is being resolved by loadJsAndAuth
.controller('homeCtrl', ['$scope', 'loadJsAndAuth', function($scope, loadJsAndAuth) {
$scope.status = loadJsAndAuth // this is resolved promises.
}]);
Related
So I am still on a crash course with Angular. I am working on quite a complicated dashboard framework, all written in angular. Before I load the controllers, I need to get a bunch of dashboard settings from the server first using $HTTP. These settings are then used to control the layout of the dashboards.
So I read the way angular builds is by first running config methods, then run methods, then the controllers.
I can't use $HTTP in a config method, so I have built this in my main.js:
MetronicApp.run(['$rootScope','$http', function($rootScope,$http) {
var CUID = Cookies("CUID");
console.log('portlet settings for '+ CUID);
$http.get('/myurl/V3_portlet_settings?p_user_id='+CUID)
.then(function(response) {
console.log(response.data);
console.log('portlet status: ' + response.status);
$rootScope.$broadcast("dashSettings",response.data);
});
}]);
When I run, this all works happily and I see the data in the console.
Then in my controller:
$scope.$on( "dashSettings",
function(event,data){
$scope.dData = data;
console.log('dash data service identified in dash controller');
console.log($scope.dData.count);
} );
Couple of questions:
Is this the best way to get settings before initializing the dash. My plan would be to embed the calls that build the dash inside my $scope.$on block. I started looking at how to run a run method synchronously before the controllers initialize, but maybe I don't need to.
Any obvious idea why the $scope.$on method does not seem to fire?
Thanks in advance
A different approach would be to place your $http functions in a service or factory and then resolve these in your controller.
The key here is the use of promise. Angular documentation describes this as
A service that helps you run functions asynchronously, and use their
return values (or exceptions) when they are done processing
First create a service:
app.factory('DataService', function($http) {
var getValues= function() {
var url = '/myurl/V3_portlet_settings?p_user_id='+ CUID;
return $http.jsonp(url) // returns a promise
};
return {
getValues: getValues
}
});
And then in your controller:
myApp.controller('MyController', function ($scope, DataService) {
DataService.getValues().then( // resolve the promise using .then()
function(data){
// successcallback
// you can now safely populate the data in you controller
console.log(data);
},
function(error){
// errorcallback
console.log(error);
})
});
I think it is a better approach to use data services to handle data operations such as $http requests.
The return of promises allows for chaining of (multiple) promises and better handling of async calls.
You might find John Papa's style guide useful, especially the section about 'Separate Data Calls' (Y060) and 'Return a Promise from Data Calls' (Y061)
Before user goes to the next page in my AngularJS app, I make a custom call to retrieve some data. I achieve this successfully with AngularJS promise
app.config(['$routeProvider', function ($routeProvider) { $routeProvider
.when("/", {templateUrl: "partials/home.html", controller: "PageCtrl",
resolve: {delay: myFunction} })
...
var myFunction = function($q, $timeout){
var defer = $q.defer();
myCustomAjaxCall({
success: function() {
defer.resolve();
},
error: function() {
defer.reject('Reject error');
}
});
return defer.promise;
};
Note myCustomAjaxCall function. I have to use it as is. It makes the call and runs callbacks on success. This in turn resolved my promise.
This myCustomAjaxCall function tries to manipulate DOM for the new view. However, the promise is resolved before the new view is rendered. It is router's routeChangeSuccess method when the view is already changed and ready for DOM manipulation. For my case, however, it is too late because DOM is trying to be manipulated before the new view is rendered.
Is there any solution you suggest to follow to manipulate new view during promise resolve execution?
You can execute code in defer.resolve itself. Have you tried?
defer.resolve(function() {
// your code
})
I am really new to AngularJS and after reading several questions and some articles I am a little confused about the correct way to load data and wait till its loaded to display the view.
My controller looks like this
app.controller('ResultsController', ['$scope','$http', '$routeParams', function($scope, $http, $routeParams) {
$scope.poll = {};
$scope.$on('$routeChangeSuccess', function() {
showLoader();
$http.get("rest/visualizacion/" + $routeParams.id)
.success(function(data) {
$scope.poll = data;
hideLoader();
})
.error(function(data) {
// Handle error
});
});
}]);
I have seen there are people who create a service for $http calls, is it necessary? Why is it better?
The appropriate way to do that is to use the resolve property of the route. From the documentation:
resolve - {Object.<string, function>=} - An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated. If all the promises are resolved successfully, the values of the resolved promises are injected and $routeChangeSuccess event is fired. If any of the promises are rejected the $routeChangeError event is fired. The map object is:
key – {string}: a name of a dependency to be injected into the controller.
factory - {string|function}: If string then it is an alias for a service. Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before its value is injected into the controller. Be aware that ngRoute.$routeParams will still refer to the previous route within these resolve functions. Use $route.current.params to access the new route parameters, instead.
So, if you want poneys to be retrieved from the backend before the router goes to the poney list page, you would have
resolve: {
poneys: function($http) {
return $http.get('/api/poneys').then(function(response) {
return response.data;
)};
}
}
And your controller would be defined as
app.controller('PoneyListCtrl", function($scope, poneys) {
$scope.poneys = poneys;
// ...
});
Of course, you could also put the code making the $http call and returning a list of poneys in a service, and use that service in the resolve.
I am trying to initialize my applications services before the controller starts running.
I would have thought that i could achieve this by resolving a promise-returning function first:
va.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/', {templateUrl: '../partials/home.php', controller: 'VaCtrl',resolve: {
pp: vac.loadData
}});
}]);
var vac = va.controller('VaCtrl',function($scope,$http,$q,packingProvider){
console.dir(packingProvider.data[2]);
});
vac.loadData = function($http,$timeout,$q,packingProvider){
$http.post('../sys/core/fetchPacking.php').then(function(promise){
packingProvider.data = promise.data;
});
var defer = $q.defer();
$timeout(function(){
defer.resolve();
},2000);
return defer.promise;
};
However, the controller is still loaded before the promise has beenr esolved, resulting in the console yelling
Cannot read property '2' of undefined
at me.
What am i doing wrong?
Edit:
Also, the controller seems to get invoked twice, first time with the undefined pacingProvider.data object, and 2 secons later with everything fine.
Instead of using the promise returned by $timeout, you could directly use the promise
returned by $http.
Rewrite your loadData fn this way -
vac.loadData = function($http,$timeout,$q,packingProvider){
var promise = $http.post('../sys/core/fetchPacking.php').then(function(promise){
packingProvider.data = promise.data;
});
return promise;
};
Read the first line in the General Usage section here - $http promise
Also, resolve is a map of dependencies.
resolve - {Object.=} - An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated.
Hence, Angular will automatically expose the map for injection. So, you can do this -
var vac = va.controller('VaCtrl', function($scope, pp){
// 'pp' is the name of the value in the resolve map for $routeProvider.
console.dir(pp[2]);
});
NOTE: although this will solve your problem, this answer is probably the right solution.
Services are always initialized before controllers. The problem, as you stated, is that your promise hasn't resulted yet. The best option for that is to stick to the promises.
Instead of exposing the data object, expose the promise and use it:
vac.loadData = function($http,$timeout,$q,packingProvider){
packingProvider.data = $http.post('../sys/core/fetchPacking.php');
return packingProvider.data;
};
And in your controller, always attached to the promise, after the first resolval, it will get resolved in the next tick:
var vac = va.controller('VaCtrl',function($scope,$http,$q,packingProvider){
packingProvider.data.then(function(value) {
console.dir(value[2]);
});
});
Folks, I have a problem. I'm using AngularJS and I am setting up a deferred object inside an Angular service definition:
angular.module('myServices', []).
service('Brand', function($rootScope, $q){
var service = {
getNext: function() {
var deferred = $q.defer();
setTimeout(function() {
deferred.resolve('foo');
}, 2000);
return deferred.promise;
}
};
return service;
});
The service is used in my controller:
angular.module({
controllers: {
brand: function($scope, Brand) {
$scope.changeBrand = function() {
$scope.brand = Brand.getNext();
}
}
}
}, ['myServices]);
And finally the view waits for the promise to be resolved and then displays it:
<a ng-click="changeBrand()" id="changeBrand">Change</a>
<p ng-bind="brand"></p>
The trouble is that although the promise is being resolved and although the view does just fine waiting for the promise to be resolved and showing the result, it doesn't do it immediately. It only shows up when I add this code and click the link:
// View:
<a ng-click="apply()">Apply</a>
// Controller:
$scope.apply = function() {
$scope.$apply();
};
Does the part of the digest that the promise lives in exist separately from the digest that runs when the view/controller's scope is changed? How can I get the digest to run on the view/controller's scope automatically when the deferred resolves?
Thanks!
Few days ago I had the same issue while implementing lazy controllers with promisses. When you take a look at the documentation about routeProvider there is a sample that uses $timeout service with promises (https://github.com/angular/angular.js/blob/master/src/ng/route.js#L186). $timeout implemation uses $rootScope.$apply() to call the lifecycle and refresh the bindings (AFAIK it calls all dirty check functions - it's how bindings work in Angular). So the solution to my problem (and I think also yours) was adding $rootScope.$apply() after the resolve(), like this:
setTimeout(function() {
deferred.resolve('foo');
$rootScope.$apply();
}, 2000);