My AngularJS needs to load a mapping (from my API) that is needed by the rest of the application to continue to make API calls. My current solution is saving the Promise that is used to load the map and having making every future API call using promise.then(...). Is this the right solution? Is it ok to keep a promise around and repeatedly call .then() on it?
As #blackhole noted, Promise.then() on an already-resolved promise is fast. It's not zero work, but it's just a quick check.
But if this data is a pre-requisite for your application, it seems a terrible burden in CODE to have to check it every time an API call needs to be made. What if you add a second pre-requisite? It's really messy to have to check this every single time in the future.
Both ngRoute and uiRouter allow you to require a resolved promise before starting a controller. They're great patterns for this in an Angular app because you can be granular - you can have a lot of smaller pre-requisites through the app that need to be resolved before those views start.
Here's a sample for ngRoute:
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'MainCtrl',
templateUrl: 'main.html',
resolve: {
init: ['MyService', function(MyService {
return MyService.getMyData();
}]
}
});
}]);
and here's one for uiRouter:
getMyData.$inject = ['MyService'];
function getMyData(MyService) {
return MyService.getMyData();
}
$stateProvider.state('home', {
url: '^/',
templateUrl: 'main.html',
controller: 'MainCtrl',
resolve: {
myData: getMyData
}
});
Since most apps want some form of routing anyway, it's an easy pattern to let the router do this for you, but as #Dave Newton pointed out, you could also do this with .run() if you want to roll-your-own.
If you cannot or do not want to use one of the routing mechanisms, you can also manually trigger Angular's bootstrap after loading the asset. Documentation for this is provided here:
https://docs.angularjs.org/guide/bootstrap
But basically you're doing this:
angular.element(document).ready(function() {
angular.bootstrap(document, ['myApp']);
});
Then you remove the ng-app directive from your top-level DOM element, wherever you put it.
Related
I'm trying to take advantage of the (new in 1.5.0) feature that adds the resolve map to the scope of the route. However, it seems like the map isn't actually getting added to the scope until after the controller has loaded.
Here's a very simple module that demonstrates the issue (from this plunk):
var app = angular.module("app", ['ngRoute']);
app.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
$routeProvider.otherwise({
controller: "editController",
controllerAs: "ec",
templateUrl: "edit.html",
resolve: {
resolveData: function() {
return {
foo: "bar",
blah: "halb"
}
}
}
});
}
]);
app.controller("editController", ["$scope", function($scope) {
// undefined
console.log($scope.$resolve);
setTimeout(function() {
// the object is there, including $scope.$resolve.resolveData
console.log($scope.$resolve);
}, 0)
}]);
If you watch the console, you'll note that $scope.$resolve is undefined when the controller is created. However, it's there immediately afterwards, as demonstrated by the setTimeout.
I can't find anything in the documentation that suggests this should be the case. Is this a bug? Or am I just not getting something fundamental about how angular works?
In case someone else comes across this - it was already reported on the angular github and resolved as intended behavior. $scope.$resolve isn't really meant to be used in controllers. You are supposed to inject it, or use $route.current.locals.
(Or you can wrap the code using $scope.$resolve in a $timeout.)
Notice your object is much bigger than you expect, and actually your object is on $resolve. This is mostly explained in the docs, however, could be more elaborate with examples...
the resolve map will be available on the scope of the route, under
$resolve
No need to dig into this, when you resolve, the named object becomes an injectable you can then place on $scope, in this case, resolveData. Observe the following...
app.controller('editController', ['$scope', 'resolveData', function($scope, resolveData) {
console.log(resolveData);
// -- $scope.data = resolveData; // -- assign it here
}]);
Plunker - updated demo
Investigation of why you are getting undefined is due to the nature of awaiting digest cycles in the framework. An acceptable workaround to get a handle on $resolve would include injecting and wrapping the call in a $timeout, which forces a digest cycle in which your object will be available in the asynchronous callback. If this approach is not ideal, forego injecting $timeout and either call $route.current.locals directly for your resolved object, or inject your object as a named parameter of which it will have resolved immediately as the example above demonstrates.
Following the ui-router FAQ entry, I recently implemented a generic AngularJS service that turns any ui-router state definition into a state definition that wraps a ui-bootstrap modal.
As a part of this, the resolve object needs to be pushed down from the state definition to the $modal.open() options. The major problem with this is that the $stateParams that is injected into these resolve functions are those of the previous state. After many hacky attempts at solving this, I found that simply wrapping the $modal.open() call in a $timeout block results in the desired behavior.
In general, I'd like to understand why this works the way that it does, whether or not it's an acceptable solution, and if there are any caveats involved. In the past I've been able to resolve several Angular timing issues by simply wrapping a block of code in $timeout, and it makes me nervous since I'm really not sure why it works. I'm guessing that a $timeout forces the block to run after the current digest cycle ends, but I'm not too confident about that.
I created a Plunker that demonstrates this -- if you remove the $timeout and invoke the modal, the parameter will not resolve.
Note: As a caveat, I MUST be able to resolve $stateParams properly in the modal resolves -- I have many existing controllers and state definitions that I'd rather not have to go back and refactor.
Follow-up: I have created ui-router issue 1649 to request a resolution to this issue -- also linked there is a minimal Plunker that only uses $injector.invoke to demonstrate the issue with no modal at all.
This is happening because you have not transitioned yet to the new state when secondparam is being resolved. The $timeout puts your code at the end of the queue, the state transition happens then executes your code with the expected state. You can tell by logging or alerting the current state in your resolve config:
secondparam: ['$stateParams', function($stateParams) {
alert($state.$current.url); //here
return $stateParams.secondparam;
}]
Unfortunately the documentation for onenter and onexit does not clearly tell when in the lifecycle they are invoked: https://github.com/angular-ui/ui-router/wiki#onenter-and-onexit-callbacks. However this post gives some indication:
These callbacks give us the ability to trigger an action on a new view
or before we head out to another state.
I think you'd be better off using a controller and opening your modal there instead of the $timeout (where I believe your context will be window/global).
$stateProvider.state('demo.modal', {
url: '/modal/:secondparam',
template: 'showing modal',
controller: function($scope, $modal, $state){
var modalInstance = $modal.open({
template: '<div class="alert alert-success" role="alert">Remove the $timeout and this will not resolve: {{secondparam}}</div>',
resolve: {
secondparam: ['$stateParams', function($stateParams) {
console.log($state.$current.url, $stateParams); //you are in the new state
return $stateParams.secondparam; //'secondparam' IS resolved, even without $timeout
}]
},
controller: function($scope, secondparam) {
$scope.secondparam = secondparam;
}
});
modalInstance.result.finally(function() {
$state.go('^');
});
}
});
Note that I had to add <div ui-view></div> to the demo state to get the controller to instantiate (ui-router nested route controller isn't being called).
$stateProvider.state('demo', {
url: '/demo/:firstparam',
template: '<button class="btn btn-primary" ui-sref=".modal({ secondparam: 2 })">Show Modal</button>' +
'<button class="btn btn-primary" ui-sref="demo.contacts">Show Contacts</button><div ui-view></div>'
});
http://plnkr.co/edit/zdSkAsEyIef0tWxSTC5p?p=preview
It could be an unintended behavior, or something "by design" of onEnter.
The solution is to inject $stateParams into the onEnter handler - it would point to the params of the second state (as per documentation) of onEnter.
$stateProvider.state('demo.modal', {
url: '/modal/:secondparam',
onEnter: showModal,
// ...
});
function showModal($state, $stateParams) {
var secondparam = $stateParams.secondparam; // equals 2
...
// BUT, the state has not yet transitioned
var secondParamFromState = $state.params.secondparam; // is undefined
...
}
The $timeout allows the state to transition, and the the resolve of the $modal gets injected the current params, which would be for the second state.
EDIT: updated plunker
The question:
Why does angular make you assign a controller to a route?
My thoughts:
Why not just have the controller on the template? Specifically the highest level of that template. What am I missing?
Based on my understanding this way makes more sense!
What I am currently doing:
When the template is rendered it comes with an ng-controller attribute on the highest level (I am using this! ). Is there something going on under-the-hood that I should be reading?
When you declare a simple pair of template-controller like
$routeProvider.when('/', {
templateUrL: '...',
controller: '...'
});
you may painlessly declare your controller in the template. There's no 'under-the-hood' behavior and the final result will be exactly the same. But issues start rising when you need something more complex.
First of all, imagine that your controller requires some condition to be satisfied before in can start.
$routeProvider.when('/', {
templateUrl: '...',
resolve: {
//do something before your controller kicks off
},
controller: '...',
});
This resolve condition might be whatever you want it to be. Say, require user to be logged in. You may handle this logic inside the controller but once you need any other route to resolve same condition you will have to duplicate your code.
Another feature that $routeProvider expose in configuration is controllerAs syntax.
See 'route' section in 'parameters' for further info http://docs.angularjs.org/api/ng/directive/ngController
Let's say I have this config:
app.config(['$routeProvider',
function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'app/partials/index.html',
controller: 'defaultCtrl'
})
.when('/other', {
templateUrl: 'app/partials/other.html',
controller: 'errorCtrl'
})
.otherwise({
templateUrl: 'app/partials/404.html'
});
}
]);
I'm looking for a place to do some basic, common maintenance code before the router calls the route. Say I'd like to clear the console log via console.clear() every time the route is changed. How and where would be the best place in the code to do that?
The $route service raise events like $routeChangeStart which you can use to perform such tasks. You can implement them using the $scope.$on method. Someting like
$scope.$on('$routeChangeStart',function(angularEvent,next,current) {
//do you work here
});
Read the $route documentation to get idea on this and other such events.
Also $routeProvider configuration method when also can take a parameter resolve which actually is used to setup dependencies before the route is resolved. This resolve object map can also be used to achieve what you want to do.
What is the better solution to hide template while loading data from server?
My solution is using $scope with boolean variable isLoading and using directive ng-hide, ex: <div ng-hide='isLoading'></div>
Does angular has another way to make it?
You can try an use the ngCloak directive.
Checkout this link http://docs.angularjs.org/api/ng.directive:ngCloak
The way you do it is perfectly fine (I prefer using state='loading' and keep things a little bit more flexible.)
Another way of approaching this problem are promises and $routeProvider resolve property.
Using it delays controller execution until a set of specified promises is resolved, eg. data loaded via resource services is ready and correct. Tabs in Gmail work in a similar way, ie. you're not redirected to a new view unless data has been fetched from the server successfully. In case of errors, you stay in the same view or are redirected to an error page, not the view, you were trying to load and failed.
You could configure routes like this:
angular.module('app', [])
.config([
'$routeProvider',
function($routeProvider){
$routeProvider.when('/test',{
templateUrl: 'partials/test.html'
controller: TestCtrl,
resolve: TestCtrl.resolve
})
}
])
And your controller like this:
TestCtrl = function ($scope, data) {
$scope.data = data; // returned from resolve
}
TestCtrl.resolve = {
data: function ($q, DataService){
var d = $q.defer();
var onOK = function(res){
d.resolve(res);
};
var onError = function(res){
d.reject()
};
DataService.query(onOK, onError);
return d.promise;
}
}
Links:
Resolve
Aaa! Just found an excellent (yet surprisingly similar) explanation of the problem on SO HERE
That's how I do:
$scope.dodgson= DodgsonSvc.get();
In the html:
<div x-ng-show="dodgson.$resolved">We've got Dodgson here: {{dodgson}}. Nice hat</div>
<div x-ng-hide="dodgson.$resolved">Latina music (loading)</div>