$resolve not added to $scope until after controller creation - javascript

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.

Related

AngularJS: Keeping a promise around to guarantee something has loaded

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.

Why does $timeout affect how $stateParams is resolved in a ui-bootstrap modal?

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

Accessing factory properties in AngularJS + Chrome Apps APIs

I'm attempting to build a Chrome App using AngularJS, and one of the abilities I need is to monitor the available network interfaces through chrome.system.network.getNetworkInterfaces. Currently, I am trying to store this data in a factory and inject it into the view controller:
(pared down as much as possible)
Factory:
exampleApp.factory('exampleFactory', ['$http', function ($http) {
var service = {};
service.network_interfaces = 'Detecting network connections...';
chrome.system.network.getNetworkInterfaces(function(interfaces){
service.network_interfaces = interfaces;
})
// This outputs the expected array containing interface data
// in service.network_interfaces
console.log(service)
return service;
}]);
Controller:
exampleApp.controller('MainCtrl', ['$scope', '$http', 'exampleFactory', function($scope, $http, exampleFactory) {
// This outputs the expected array in network_interfaces, identical
// to the console output within the factory
console.log(exampleFactory)
// But then this outputs the initial 'Detecting network connections...'
// string set in the factory
console.log(exampleFactory.network_interfaces)
// And {{network_interfaces}} and {{factory_state}} in the view keep
// 'Detecting network connections...' as the value for network_interfaces
// permanently
$scope.factory_state = exampleFactory;
$scope.network_interfaces = exampleFactory.network_interfaces;
}]);
So:
The factory seems to be returning a good service object, but I'm not sure why exampleFactory and exampleFactory.network_interfaces would have the different states they do between the controller and factory, and especially within the controller itself (regardless of the order they're called in).
I've attempted a lot of different solutions with the hypothesis that it's an asynch issue, but I would think there'd be no appreciable latency on the getNetworkInterfaces method and if there were that everything is set up correctly for Angular to update the {{network_interfaces}} and {{factory_state}} view bindings once data is returned.
I've also tried wrapping various functions in the factory with $rootScope.$apply as a shot in the dark, but with the same results as above.
I've searched around a lot to discover whatever concept it is I've obviously missed, but I think I'm overlooking something fundamental. How to I get the getNetworkInterfaces() data into my controller in a useful state?
Your assumption in #2 is the problem. There will always be a spin of the JavaScript event loop in between a call to an asynchronous method and the invocation of its callback. If there weren't, all sorts of things would subtly break in clients calling the method. This means you are encountering a common problem in Angular development: that you don't get notified that something changed, because the change didn't happen within the context of Angular's digest cycle.
To fix this: try setting up a watch, then calling $scope.apply() within the getNetworkInterfaces() callback. The apply() is guaranteed to happen outside the digest cycle so you shouldn't get an apply-in-apply error.
Alternatively, post a message to yourself when then callback is done. This is better if you're a student of the "if you're using apply() your code is broken" school of thought.
Finally, consider a Promise that you call after the callback. This doesn't quite fit how you've set up your code.
(Also, why call the async method at all in the factory?)
Try watching on network_interfaces to know when it's modified
exampleApp.controller('MainCtrl', ['$scope', '$http', 'exampleFactory', function($scope, $http, exampleFactory) {
// This outputs the expected array in network_interfaces, identical
// to the console output within the factory
console.log(exampleFactory)
// But then this outputs the initial 'Detecting network connections...'
// string set in the factory
console.log(exampleFactory.network_interfaces)
// And {{network_interfaces}} and {{factory_state}} in the view keep
// 'Detecting network connections...' as the value for network_interfaces
// permanently
$scope.factory_state = exampleFactory;
$scope.network_interfaces = exampleFactory.network_interfaces;
$scope.$watch('factory_state.network_interfaces', function() {
console.log($scope.factory_state)
});
}]);

Angular Service Injection

I am working on a trivial task (that I got working) which adds an an .active class to dom elements in a tab nav. Simple. Got it working by following https://stackoverflow.com/a/12306136/2068709.
Reading over the answer provided and comparing their controller to example controllers on angular's website I came across something interesting that I don't quite understand.
On the AngularJS website, $scope and $location (and other services) are injected into the controller along with an anonymous function which defines the controller. But on the answer, the services are not injected, rather they are passed via the anonymous function. Why does this work? Shouldn't the service always be injected?
In terms of an example: Why does this
angular.module('controllers', [])
.controller('NavigationController', ['$scope', '$location', function($scope, $location) {
$scope.isActive = function(route) {
return route === $location.path();
};
}])
and this
angular.module('controllers', [])
.controller('NavigationController', function($scope, $location) {
$scope.isActive = function(route) {
return route === $location.path();
};
})
both work?
This may be trivial but its throwing my brain for a loop that I can't figure out.
The two examples are equivalent - they just make use of different syntax. The first example uses what they call "inline array annotation" (see here). The purpose of this alternate syntax is just to allow a convenient way to make the injected variable names different than the name of the dependency.
So for example, if you wanted the variable names to be "s" and "l", then you could write:
angular.module('controllers', [])
.controller('NavigationController', ['$scope', '$location', function(s, l) {
s.isActive = function(route) {
return route === l.path();
};
}]);
Actually they are injected in both cases, the difference between those two cases is in the first scenario you define and name the dependency this could be useful if you minify your js code and that way you are declaring explicitly the dependency for examply it could be:
angular.module('controllers', [])
.controller('NavigationController', ['$scope', '$location', function($s, $l) {
$s.isActive = function(route) {
return route === $l.path();
};
}])
that way angular will know which dependency to inject on which parameter without looking at the naming for each parameter.
the other case you need to be explicit and declare which dependency you'll inject by setting up the name.
I hope that helps.
This is how angular handles code minification. by keeping strings intact it can keep mapping vars when they are renamed.
if you take a look at the code of the controller https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L48
you'll see that the constructor can accept both function and array.

angularjs passing variables into controller

I have my angular controller setup like most of the examples shown in the docs such that it is a global function. I assume that the controller class is being called when the angular engine sees the controller tag in the html.
My issue is that i want to pass in a parameter to my controller and i don't know how to do that because I'm not initializing it. I see some answers suggesting the use of ng-init. But my parameter is not a trivial string - it is a complex object that is being loaded by another (non-angular) part of my js. It is also not available right on load but takes a while to come along.
So i need a way to pass this object, when it finally finishes loading, into the controller (or scope) so that the controller can interact with it.
Is this possible?
You can use a service or a factory for this, combined with promises:
You can setup a factory that returns a promise, and create a global function (accessible from 3rd-party JS) to resolve the promise.
Note the $rootScope.$apply() call. Angular won't call the then function of a promise until an $apply cycle. See the $q docs.
app.factory('fromoutside', function($window, $q, $rootScope) {
var deferred = $q.defer();
$window.injectIntoAngularWorld = function(obj) {
deferred.resolve(obj);
$rootScope.$apply();
};
return deferred.promise;
});
And then in your controller, you can ask for the fromoutside service and bind to the data when it arrives:
app.controller('main', function($scope, fromoutside) {
fromoutside.then(function(obj) {
$scope.data = obj;
});
});
And then somewhere outside of Angular:
setTimeout(function() {
window.injectIntoAngularWorld({
A: 1,
B: 2,
C: 3
});
}, 2000);
Here's a fiddle of this.
Personally, I feel this is a little bit cleaner than reaching into an Angular controller via the DOM.
EDIT: Another approach
Mark Rajcok asked in a comment if this could be modified to allow getting data more than once.
Now, getting data more than once could mean incremental updates, or changing the object itself, or other things. But the main things that need to happen are getting the data into the Angular world and then getting the right angular scopes to run their $digests.
In this fiddle, I've shown one way, when you might just be getting updates to an Array from outside of angular.
It uses a similar trick as the promise example above.
Here's the main factory:
app.factory('data', function($window) {
var items = [];
var scopes = [];
$window.addItem = function(item) {
items.push(item);
angular.forEach(scopes, function(scope) {
scope.$digest();
});
};
return {
items: items,
register: function(scope) { scopes.push(scope); }
};
Like the previous example, we attach a function to the $window service (exposing it globally). The new bit is exposing a register function, which controllers that want updates to data should use to register themselves.
When the external JS calls into angular, we just loop over all the registered scopes and run a digest on each to make sure they're updated.
In your non-angular JavaScript, you can get access to the scope associated with a DOM element as follows:
angular.element(someDomElement).scope().someControllerFunction(delayedData);
I assume you can find someDomElement with a jQuery selector or something.

Categories

Resources