Angular ui-router resolve value as string - javascript

With ui-router, I add all resolve logic in state function like this;
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: { // <-- I feel this must define as like controller
customers: function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}
}
});
However IMO, resolve object must belong to a controller, and it's easy to read and maintain if it is defined within a controller file.
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
MyCtrl.resolve = {
customers: function(Customer, $stateParams) {
return Customer.get($stateParams.id);
};
};
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: 'MyCtrl.resolve' //<--- Error: 'invocables' must be an object.
});
However, When I define it as MyCtrl.resolve, because of IIFE, I get the following error.
Failed to instantiate module due to: ReferenceError: MyCtrl is not defined
When I define that one as string 'MyCtrl.resolve', I get this
Error: 'invocables' must be an object.
I see that controller is defined as string, so I think it's also possible to provide the value as string by using a decorator or something.
Has anyone done this approach? So that I can keep my routings.js clean and putting relevant info. in a relevant file?

It sounds like a neat way to build the resolve, but I just don't think you can do it.
Aside from the fact that "resolve" requires an object, it is defined in a phase where all you have available are providers. At this time, the controller doesn't even exist yet.
Even worse, though, the "resolve" is meant to define inputs to the controller, itself. To define the resolve in the controller, then expect it to be evaluated before the controller is created is a circular dependency.
In the past, I have defined resolve functions outside of the $stateProvider definition, at least allowing them to be reused. I never tried to get any fancier than that.
var customerResolve = ['Customer', '$stateParams',
function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}
];
// ....
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: {
customers: customerResolve
}
});

This question is about features of ui-router package. By default ui-router doesn't support strings for resolve parameter. But if you look at the source code of ui-router you will see, that it's possible to implement this functionality without making direct changes to it's code.
Now, I will show the logic behind suggested method and it's implementation
Analyzing the code
First let's take a look at $state.transitionTo function angular-ui-router/src/urlRouter.js. Inside that function we will see this code
for (var l = keep; l < toPath.length; l++, state = toPath[l]) {
locals = toLocals[l] = inherit(locals);
resolved = resolveState(state, toParams, state === to, resolved, locals, options);
}
Obviously this is where "resolve" parameters are resolved for every parent state. Next, let's take a look at resolveState function at the same file. We will find this line there:
dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state);
var promises = [dst.resolve.then(function (globals) {
dst.globals = globals;
})];
This is specifically where promises for resolve parameters are retrieved. What's good for use, the function that does this is taken out to a separate service. This means we can hook and alter it's behavior with decorator.
For reference the implementation of $resolve is in angular-ui-router/src/resolve.js file
Implementing the hook
The signature for resolve function of $resolve is
this.resolve = function (invocables, locals, parent, self) {
Where "invocables" is the object from our declaration of state. So we need to check if "invocables" is string. And if it is we will get a controller function by string and invoke function after "." character
//1.1 Main hook for $resolve
$provide.decorator('$resolve', ['$delegate', '$window', function ($delegate, $window){
var service = $delegate;
var oldResolve = service.resolve;
service.resolve = function(invocables, locals, parent, self){
if (typeof(invocables) == 'string') {
var resolveStrs = invocables.split('.');
var controllerName = resolveStrs[0];
var methodName = resolveStrs[1];
//By default the $controller service saves controller functions on window objec
var controllerFunc = $window[controllerName];
var controllerResolveObj = controllerFunc[methodName]();
return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);
} else {
return oldResolve.apply(this, [invocables, locals, parent, self]);
}
};
return $delegate;
}]);
EDIT:
You can also override $controllerProvider with provider like this:
app.provider("$controller", function () {
}
This way it becomes possible to add a new function getConstructor, that will return controller constructor by name. And so you will avoid using $window object in the hook:
$provide.decorator('$resolve', ['$delegate', function ($delegate){
var service = $delegate;
var oldResolve = service.resolve;
service.resolve = function(invocables, locals, parent, self){
if (typeof(invocables) == 'string') {
var resolveStrs = invocables.split('.');
var controllerName = resolveStrs[0];
var methodName = resolveStrs[1];
var controllerFunc = $controllerProvider.getConstructor(controllerName);
var controllerResolveObj = controllerFunc[methodName]();
return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);
} else {
return oldResolve.apply(this, [invocables, locals, parent, self]);
}
};
Full code demonstrating this method http://plnkr.co/edit/f3dCSLn14pkul7BzrMvH?p=preview

You need to make sure the controller is within the same closure as the state config. This doesn't mean they need to be defined in the same file.
So instead of a string, use a the static property of the controller:
resolve: MyCtrl.resolve,
Update
Then for your Controller file:
var MyCtrl;
(function(MyCtrl, yourModule) {
MyCtrl = function() { // your contructor function}
MyCtrl.resolve = { // your resolve object }
yourModule.controller('MyCtrl', MyCtrl);
})(MyCtrl, yourModule)
And then when you define your states in another file, that is included or concatenated or required after the controller file:
(function(MyCtrl, yourModule) {
configStates.$inject = ['$stateProvider'];
function configStates($stateProvider) {
// state config has access to MyCtrl.resolve
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: MyCtrl.resolve
});
}
yourModule.config(configStates);
})(MyCtrl, yourModule);
For production code you will still want to wrap all these IIFEs within another IIFEs. Gulp or Grunt can do this for you.

If the intention is to have the resolver in the same file as the controller, the simplest way to do so is to declare the resolver at the controller file as a function:
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
var resolverMyCtrl_customers = (['Customer','$stateParams', function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}]);
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: resolverMyCtrl_customers
});

This should work.
//my-ctrl.js
var MyCtrl = function($scope, customer) {
$scope.customer = customer;
};
//routing.js
$stateProvider
.state('customers.show', {
url: '/customers/:id',
template: template,
resolve: {
customer: function(CustomerService, $stateParams){
return CustomerService.get($stateParams.id)
}
},
controller: 'MyCtrl'
});
//service.js
function CustomerService() {
var _customers = {};
this.get = function (id) {
return _customers[id];
};
}

Related

way to abstract an angular ui router state with resolve into a constructor function

I have this constructor function:
function State(url, templateUrl, controller, controllerAs, factory, resolveProp){
this.url = url
this.templateUrl = templateUrl
this.controller = controller
this.controllerAs = controllerAs
}
and in my router callback I have this:
$stateProvider
.state('archetypes', new State('/admin/archetypes', './resources/features/admin/archetypes/index.html', 'archetypes', 'aaVM'))
This works fine. I have another route, however, that contains a resolve to get some back end data.
I thought I might be able to do something like this for my constructor:
function State(url, templateUrl, controller, controllerAs, factory, resolveProp){
this.url = url
this.templateUrl = templateUrl
this.controller = controller
this.controllerAs = controllerAs
if(factory){
this.resolve = {}
this.resolve[resolveProp] = function(factory){
return factory.getAll()
}
}
}
and then instantiate a state in this way:
.state(
'archetypes',
new State(
'/admin/archetypes',
'./resources/features/admin/archetypes/index.html',
'archetypes',
'aaVM',
ArchetypesFactory,
'archetypes'
)
)
But I think the .state method calls the controller in order to process the resolve object, such that when I try the above state instantiation, it errors out because ArchetypesFactory is clearly undefined.
Note that when i write my state in this way:
.state('archetypes', {
url: '/admin/archetypes',
templateUrl: './resources/features/admin/archetypes/index.html',
controller: 'archetypes',
controllerAs: 'aaVM',
resolve: {
archetypes: function(ArchetypesFactory){
return archetypesFactory.getAll()
}
}
})
It works fine.
Is there any way I can abstract the state configuration object with a resolve into a constructor function or ES6 class?
The resolver function is invoked by the AngularJS injector. The injector needs an explicit form of annotation. One way is to use Inline Array Annotation:
function State(url, templateUrl, controller, controllerAs, factory, resolveProp){
this.url = url;
this.templateUrl = templateUrl;
this.controller = controller;
this.controllerAs = controllerAs;
if(factory){
this.resolve = {}
this.resolve[resolveProp] = [factory, function(factory){
return factory.getAll();
}];
};
}
Then specify a string for the factory parameter:
.state(
'archetypes',
new State(
'/admin/archetypes',
'./resources/features/admin/archetypes/index.html',
'archetypes',
'aaVM',
//ArchetypesFactory,
//USE string
'ArchetypesFactory',
'archetypes'
)
)
Inline Array Annotation is the preferred way to annotate application components. When using this type of annotation, take care to keep the annotation array in sync with the parameters in the function declaration.

AngularJS tests - inject -> module -> inject

I'm trying to test a service documentViewer that depends on some other service authService
angular
.module('someModule')
.service('documentViewer', DocumentViewer);
/* #ngInject */
function DocumentViewer($q, authService) {
// ...
this.view = function(doc) {
//...
}
}
This is what my test looks like at the moment
it('test', inject(function($q) {
var doc = {
view: function() {
return $q.resolve(0);
}
};
var auth = {
refreshProfileData: function() {
return $q.resolve(0);
},
};
var viewer = createViewer(auth);
}));
function createViewer(auth) {
var viewer;
module({
authService: auth
});
inject(function(documentViewer) {
viewer = documentViewer;
});
return viewer;
}
The problem is I need to call inject to grab a $q, then use it to create my mocks, register my mocks with module, and then call inject again to grab the unit under test.
This results in
Error: Injector already created, can not register a module! in bower_components/angular-mocks/angular-mocks.js (line 2278)
I've seen lots of answers here on SO saying you can't call module after inject, but they don't offer any alternative to a scenario like the above.
What's the correct approach here?
PS: I'd like to avoid using beforeEach, I want each test to be self-contained.
module is used to define which modules will be loaded with inject and cannot be called after inject, this is chicken-egg situation.
The object accepted by module is used to define mocked services with $provide.value:
If an object literal is passed each key-value pair will be registered on the module via $provide.value, the key being the string name (or token) to associate with the value on the injector.
There can be no more than 1 function like createViewer that calls both module and inject. If this means that this kind of self-contained test is an antipattern, there is nothing that can be done about that. Angular testing works best with usual habits, including beforeEach and local variables.
In order to eliminate the dependency on $q, mocked service can be made a factory.
it('test', function () {
var authFactory = function ($q) {
return {
refreshProfileData: function() {
return $q.resolve(0);
},
};
};
// mocks defined first
module(function ($provide) {
$provide.factory('authService': authFactory);
});
var viewer;
inject(function(documentViewer) {
viewer = documentViewer;
});
// no module(...) is allowed after this point
var $q;
inject(function(_$q_) {
$q = _$q_;
});
var doc = {
view: function() {
return $q.resolve(0);
}
};
});

Provide a dynamic value for Angular Constant in Jasmine test

I have a constant which is injected into a controller and I need to write a test which changes this constant and expects different results. I can use $provide to mock the constant but according to articles I've found online, I need to do it in the module declaration, which I believe is like this:
beforeEach(module("someModule"));
beforeEach(function () {
module(function ($provide) {
$provide.constant('someConstant', false);
});
});
I later load the controller like this:
function createController() {
view = $controller(
"someController",
{
$scope: $injector.get("$rootScope").$new()
});
}
Where $controller, $scope and $injector are all injected in my main beforeEach
This does provide the constant and it does change if I change the value in my beforeEach. But only for the entire test suite. I want to change this constant in a describe or an it but I'm not sure how. If I move the $provide down to a describe or it, I get the error:
Error: Injector already created, can not register a module!
I could just create a new file and that is probably what I'm going to do but is there a way I can $provide a dynamic value?
Lets consider such a code
controller
angular.module('someModule', [])
.controller('someController', function($scope, someConstant) {
$scope.someProvidedValue = someConstant;
})
and test for it
controller spec
describe('module', function () {
beforeEach(module("someModule"));
var createController;
beforeEach(inject(function (_$controller_, _$injector_) {
scope = _$injector_.get("$rootScope").$new()
createController = function createController(scope, obj) {
_$controller_("someController", {
$scope: scope,
someConstant: obj.someConstant
});
}
}))
it('someConstant', function () {
expect(scope.someProvidedValue).toBe(undefined)
createController(scope, {
someConstant: false
})
expect(scope.someProvidedValue).toBe(false)
})
it('someConstant', function () {
expect(scope.someProvidedValue).toBe(undefined)
createController(scope, {
someConstant: true
})
expect(scope.someProvidedValue).toBe(true)
})
})
In the mean time I'm looking for looks nicer solution.

How to pass dynamically generated data to a different state in Angular UI-Router

Question: how do I access a dynamically generated data in scope B, when I go from scope A and generate this data in scope A, using angular's ui-controller. Data is not available when the scope is initialized.
Note: I am fine with showing request data in the URL. I'm looking for the simplest way for new state to read data it needs and pass it to server and properly generate its contents.
When the page loads, it fetches data from server and populates scope "tests" with new data. This new data is shown on the page. I create links to scope "test" with this data. Links look like this:
<a ui-sref="test({id:test._id})">{{test.name}}</a>
On a rendered page it looks like this:
<a ui-sref="test({id:test._id})" class="ng-binding" href="#/test/57adc0e30a2ced3810983640">A test</a>
The href is correct and points to a database reference of an item. My goal is to have this reference as a variable in scope "test". My state provider:
$stateProvider
.state('tests', {
url: '/tests/',
templateUrl: 'test/index.html',
controller: 'Test.IndexController',
controllerAs: 'vm',
data: { activeTab: 'tests' }
})
.state('test', {
url: '/test/{id}',
templateUrl: 'test/item.html',
controller: 'Test.ItemController',
controllerAs: 'vm',
data: {
activeTab: 'tests',
testId: '{id}'
}
});
So far no matter what I tried I couldn't access "testId" in the "test" scope. It was either "undefined", created errors or returned "itemId: {id}".
My Item.Controller:
(function () {
'use strict';
function Controller(TestService) {
var vm = this;
vm.test = null;
function getTest(id) {
TestService.GetTestById(id).then(function(test) {
vm.test = test;
});
}
function initController() {
getTest(...);
}
initController();
}
angular
.module('app')
.controller('Test.ItemController', Controller);
})();
TestService provides http get methods for getting data from server.
(function () {
'use strict';
function Service($http, $q) {
var service = {};
function handleSuccess(res) {
return res.data;
}
function handleError(res) {
return $q.reject(res.data);
}
function GetTestById(_id) {
var config = {
params: {
testId: _id
}
};
return $http.get('/api/tests/:testId', config).then(handleSuccess, handleError);
}
service.GetTests = GetTests;
service.GetTestById = GetTestById;
return service;
}
angular
.module('app')
.factory('TestService', Service);
})();
I tried $scope - scope is not defined. I tried a number of other techniques, shown by other users with similar success - either "undefined" or error of some sort.
This is based on another person's code so there may be obvious mistakes, please let me know if you find any. If you need more code, let me know - I'll upload it to github (its a messy work in progress at the moment so I'm not sure what should be uploaded).

AngularJS $scope inheritance service

I'm having some trouble with my code. I can't pass nor console.log the inherited $scope.user in my data service. As I'm having this problem also in another situation which looks the same I guess it's because of the callback.
The main Controller creates the user
.controller('mainCtrl', function ($scope, dataService) {
dataService.getUser(function (response) {
$scope.user = response.data[0];
})
The dataservice
.service('dataService', function ($http) {
this.getUser = function (callback) {
$http.get('mock/user.json')
.then(callback)
};
The navigation controller (child of mainCtrl):
.controller('navCtrl', function ($scope, dataService) {
//$scope.user = "test";
console.log ($scope.user);
dataService.getNavItems($scope.user,function (response) {
$scope.navItems = response.data;
});
As you can guess if I set $scope.user manually it works just fine.
The promise hasn't resolved yet when navCtrl is instantiated. What you can do is return the promise from $http.get instead of setting scope.user directly in the callback. And then just wrap the call to getNavItems in the promise.
This is assuming, navCtrl is a child of MainCtrl
.service('dataService', function ($http) {
this.getUser = function () {
return $http.get('mock/user.json');
}};
.controller('mainCtrl', function ($scope, dataService) {
$scope.userPromise = dataService.getUser();
})
.controller('navCtrl', function ($scope, dataService) {
$scope.userPromise.then(function(response) {
var user = response.data[0];
dataService.getNavItems(user, function (response) {
$scope.navItems = response.data;
});
});
})
The Scope will be different for the two Controllers, therefore it won't be defined in one if you defined it in the other. If you want it to work in both, just use the dataService.
.service('dataService', function ($http) {
this.getUser = function () {
$http.get('mock/user.json').then(function(data) {
this.user = data;
)}
};
Then access it in each controller separately, it will be available to both.
Seems like controllers 'mainCtrl' and 'navCtrl' have different scopes. If the scope of 'navCtrl' is a child of the scope of 'mainCtrl', you can access it with $scope.$parent.user
To trigger logging when the promise is resolved $scope.$parent.$watch('user', fucntion(newVal){console.log(newVal)})
If not, I would suggest to have some kind of context, where you would store the data used by different controllers.
To find a scope you can use angular.element('[ng-controller="mainCtrl"]').scope() even in browser console

Categories