Minifier confusing variable name - javascript

I have an angular router that uses the resolve feature and calls a service MyService.
var Router = function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state(home, {
resolve:{
myFactory: 'MyFactory',
checkUser: function(myFactory){
return myFactory.getUser().then(function(data){
// some logic with data
});
}
},
});
};
module.exports = ['$stateProvider', '$urlRouterProvider', Router];
When i run it through the minifier, i end up with:
var e = function(t, e) {
t.state(home, {
url: "/home",
resolve: {
myFactory: "MyFactory",
checkUser: function(t){
return t.getUser().then(function(e){
});
}
},
})
}
;
t.exports = ["$stateProvider", "$urlRouterProvider", e]
The problem here is variables t and e.
At the top level:
t = $stateProvider
e = $urlRouterProvider
However, within my resolve, t and e are used again, but:
t = $stateProvider but should = myFactory
e = $urlRouterProvider but should = data
And so t.getUser or t.getProducts do not actually exist and so my app fails to load.
UPDATE:
Annotated version:
var Router = function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state(home, {
resolve:{
myFactory: 'MyFactory',
checkUser: ['myFactory', function(myFactory){
return myFactory.getUser().then(function(data){
// some logic with data
});
}]
},
});
};
module.exports = ['$stateProvider', '$urlRouterProvider', Router];

myFactory is local dependency in this resolver. As any other dependency, it should be annotated in order to be injected properly in minified JS.
checkUser: ['myFactory', function(myFactory){
return myFactory.getUser().then(function(data){
// some logic with data
});
}]
Strict mode may be used to avoid this kind of bugs in production.

Yeap. You need NG-Annotate.
For Gulp for example I used this one.

This is essentially an issue with accessing objects and their properties from within a "method" of the object, as it is written in-properly.
I've made you a Fiddle Example of Object Access which shows how you can access objects properties and how to differentiate between accessing it's properties and the arguments passed to the functions.
To correct your particular error, your resolve would probably need to look like this, but I'd advise you to check the fiddle example and try to figgure it out yourself. I have 0 experience with angular, from what I undestand you can reference $scope there, which would act as a replacement for this but as I said I am not familiar, hope this helps:
resolve:{
self: this,
myFactory: 'MyFactory',
checkUser: function(){
return self.myFactory.getUser().then(function(data){
// some logic with data
});
}
},

It's standard minifier approach, to make code smaller it also changes variable names to smaller.
You can solve it in three ways :
a) in minifier you have to specify no to change variable names. (for example mangle: false in uglify plugin)
b) use an angular array notation, example :
angular.service(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { .... } ]);
c) use $inject property, example:
var Router = function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state(home, {
resolve:{
myFactory: 'MyFactory',
checkUser: function(myFactory){
return myFactory.getUser().then(function(data){
// some logic with data
});
}
},
});
};
Router.$inject = ['$stateProvider', '$urlRouterProvider'];
module.exports = ['$stateProvider', '$urlRouterProvider', Router];
In your specific scenario only a) and c) will work

Related

oc.lazyload not working with ui-router, js loaded but controller not initialized

I'm trying to use ui-router with lazyload, from chrome I can see the required js is loaded but angular throws an error.
From the error I can tell the controller is not initialised, attached the router code:
var app = angular.module('aaaaa', ['oc.lazyLoad', 'ui.router']);
app.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/pages/dashboard');
$stateProvider.state('index', {
url: '/pages/:name',
templateUrl: function($stateParams) {
return 'templates/' + $stateParams.name + '.html';
},
controllerProvider: function($stateParams) {
return $stateParams.name;
},
resolve: {
loader: ['$ocLazyLoad', '$stateParams', function($ocLazyLoad, $stateParams) {
var url = 'templates/controllers/' + $stateParams.name + '.js';
console.log(url);
return $ocLazyLoad.load({
name: 'aaaaa',
files: [url]
});
}]
}
});
});
Am I missing something?
I would refactor your resolve.loader function as such:
loader: ['$ocLazyLoad', '$stateParams', function($ocLazyLoad, $stateParams) {
var url = 'templates/controllers/' + $stateParams.name + '.js';
return $ocLazyLoad.load(url).then(function () {
return $stateParams.name;
});
}]
Now, when ocLazyLoad has successfully loaded your controller code, we return the name of your controller (I'm assuming it's actual name matches that of $stateParams.name, as that is how you are referring to it in the controllerProvider).
And then in the controllerProvider:
controllerProvider: function (loader) {
return loader;
}
controllerProvider nowadays accept the resolved values, not just stateParams - as per the following commit: ui-router#851f8e46.
This was introduced in 0.2.14 and has since been changed again, it now has support for resolved values in the templateUrl function aswell.
So as an aside, you could pass loaded into your templateUrl function as opposed to `$stateParams.name if you upgrade to 0.2.15. One value to rule 'em all?
For the heck of it, I would put a debugger statement at the top of your controllerProvider function beforehand to inspect the $stateParams.name and ensure it is the same as that of $stateParams.name inside the resolve.loader function. For the heck of it.
Also, it could prove very helpful to see the error thrown, the dev console output as well as the implementation details of your controller / module.
Problem resolved.
In the separate controller file, you can't write this,
app.controller ('xxx', [ ... ]);
Instead, use
angular.module('xxxx').controller ('xxx', [ ... ])
I don't know why yet, but it worked.

Angular ui-router resolve value as string

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];
};
}

angular empty $state name when testing routing with jasmine

Like many folks, I'm new to testing Angular with Jasmine and am struggling to get this right. I use ui-router to do my routing and right now, the problem I'm having is that the $state.current.name in the test is an empty string and I have no idea why it does that.
This is the code in my routing module:
var cacRouteViewMod = angular.module('cacRouteViewMod', ['ui.router', 'cacLib']);
cacRouteViewMod.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('countries', {
url: '/countries',
templateUrl: 'countries/countries.html',
controller: 'countriesCtrl',
resolve : {
countries: ["getCountry", function(getCountry) {
return getCountry();
}]
}
});
}]);
and the test I wrote is this:
describe('cac_app_views (routing)', function() {
var $rootScope,
$state,
$injector,
getCountryMock,
state = 'countries';
beforeEach(function() {
module('cacRouteViewMod', function($provide, $urlRouterProvider) {
$urlRouterProvider.deferIntercept();
$provide.value('getCountry', getCountryMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
$templateCache.put('countries/countries.html', '');
})
});
// Test 1
it('should respond to URL', function() {
expect($state.href(state)).toEqual('#/countries');
});
// Test 2
it('should resolve getCountry', function() {
getCountryMock = jasmine.createSpy('getCountry').and.returnValue('nanana');
$rootScope.$apply(function() {
$state.go('countries');
});
expect($state.current.name).toBe('countries');
expect($injector.invoke($state.current.resolve.countries)).toBe('nanana');
});
});
Test 1 is fine, but test 2 is the issue. The test fails because it expected '' to be 'countries'.
When I log $state.current to the console it gives
Object {name: "", url: "^", views: null, abstract: true}
I'm getting pretty desperate at this point. Could anyone help me understand/solve this problem?
I solved this in this manner:
By reading similar stockoverflow posts, I put a listener for $stateChangeError and it got triggered. I logged out the error data and saw that it's a typeError: getCountry is not a function. This caused the $state not to be updated, and therefore still contains the original(empty) $state.
I fixed the $provide.value to such:
$provide.value('getCountry', getCountryMock = function() {return 'nanana';});
which says "whenever getCountry is called, provide getCountryMock instead, which is a function that returns a string 'nanana'.
Now the tests all work the way I want them to.
Note: I found that getCountryMock = jasmine.createSpy..... line of code to be obsolete with my other change to $provide.value() so I commented it out.
According to the documentation $state.go returns a promise.
You should use done() function from Jasmine in order to test such a code: http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine/

Pass sub-state from sub-module to the main module in angular.js with angular-ui-router

I design my SPA like this:
angular.module('app', ['submodule0', 'submodule1']);
Main module:
$stateProvider.state("sub0index", {
url: "/sub0",
// pass states defined in submodule0, is that possible?
}).state("sub1index", {
url: "/sub1",
// pass states defined in submodule1
})
And here are some states defined in submodule0
$stateProvider.state("index", {
url: "/index",
templateUrl: "template/index.html"
}).state("info", {
url: "/info",
templateUrl: "template/info.html"
})
So is that possible that I pass sub-state from sub-module to the main module? I ask this because now I define all my state in my main module, I think it may be more elegant to define the state of one submodule in the submodule itself.
And another question is: I'm not sure my module design is reasonable or not, is my submodules not necessary? Or just keep my whole app logic to one module? Thanks.
====Edited====
And here is the problem I've met.
var app = angular.module('test', ['ui.router', 'app.sub']);
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: "/a",
views: {
"general": {
templateUrl: "/template.html"
}
},
resolve: {
data: 'GetDataService'
}
});
}
The service GetDataService is defined in my submodule app.sub, and here is the service:
angular.module('app.sub',['ui.router'])
.service('GetDataService', ['$stateParams', function($stateParams) {
console.log($stateParams);
return null; // return null just for demo
}]);
The output of console.log($stateParams) is an empty object. But if use the service which is defined in its own module, the current state can be get correctly. So whats the issue?
===Edit===
Thanks for the example, it works fine if give a factory to data directly. But how about I give it a string?
I check the document of ui-router, and there is something about map object in resolve:
factory - {string|function}: If string then it is alias for service.
So if I use the code like this:
resolve: {
data: "GetDataService"
}
And the definition of GetDataService:
.service('GetDataService', ['$stateParams', function($stateParams) {
console.log($stateParams);
return null;
}])
But output of console.log($stateParams) is always an empty object.
Do I have some misunderstanding about the api document?
===Edit again===
If I use code like this:
resolve: {
// data: "GetDataService"
data: ['$stateParams', function($stateParams) {
console.log($stateParams);
return null;
}]
}
I can get the params object.
I would say, that modules should not stop us... we can split the app into many if needed.
But I would suggest: Services should be independent on $state.current. We should pass to them function parameters as needed, but these should be resolved outside of the Service body.
Bette would be to show it in action - there is one working example
This is the service:
angular.module('app.sub',['ui.router'])
.service('DataService', ['$state', function($state) {
return {
get: function(stateName, params){
console.log(stateName);
console.log(params);
return stateName;
}
}
}]);
And here is some adjsuted state def:
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: "/a/{param1}",
views: {
"general": {
templateUrl: "tpl.html"
}
},
resolve: {
data: ['DataService','$stateParams'
, function(DataService,$stateParams, $state){
return DataService.get('index', $stateParams)
}],
},
});
}])
Hope it helps a bit. The plunker link
Because this approach is ready to test service without any dependency on some "external" $state.current. We can just pass dummy, testing params

Injecting services/constants into provider in Angularjs

The problem here is I am able to access the getRoutes(), but I am unable to access the injected constant -"configuration". What am I missing? Thanks.
(function () {
'use strict';
var app = angular.module('app');
app.constant('configuration', {
PARTIAL_PATH: "/app/components/partials"
});
app.module('app', [
'routeService'
]);
var routeServiceModule = angular.module('routeService', ['common']);
routeServiceModule.provider('routeConfig',function () {
this.getRoutes = function () {
return [
{
url: '/login',
config: {
title: 'admin',
templateUrl: 'app/components/login/login.html'
}
}, {
url: '/',
config: {
templateUrl: 'app/components/dashboard/dashboard.html',
title: 'Dashboard'
}
}
];
};
this.$get = ['configuration', function (configuration) {
var service = {
getRoutes: getRoutes(),
configuration: configuration.PARTIAL_PATH
};
return service;
}];
app.config(['$routeProvider', 'routeConfigProvider', function ($routeProvider, routeConfigProvider) {
//Unable to get the configuration value
console.log(routeConfigProvider.configuration);
//Console is returning as "undefined"
routeConfigProvider.getRoutes().forEach(function(r) {
$routeProvider.when(r.url, r.config);
});
$routeProvider.otherwise({ redirectTo: '/' });
}
]);
})();
Created a plunkr demo : http://plnkr.co/edit/2TIqgxMxBJEPbnk2Wk6D?p=preview
(Regarding your last comment, with the plnkr)
The result is expected.
At config time (within app.config() ), you access raw providers, as you defined them, which allows you to call "private" methods or fields (testItem1) and to configure it for run time use. "private" because they won't be accessible at run time.
At run time (within app.run() and the rest of your app), when you ask for a dependency for which you wrote a provider, the angular injector hands you the result of the $get method of your provider, not the provider itself, so you can't access the "private" function.
This page was my path to enlightenment : AngularJS: Service vs provider vs factory
I think you may be over complicating the route stuff. You may have a very good reason for it but as I do not know it may I suggest keeping it simple with something more like this:
MyApp.config(function ($routeProvider) {
$routeProvider.when('/home', {
templateUrl: 'home.html',
controller: 'HomeController',
activeTab: 'home'
})
};
MyApp.controller('HomeController', function ($route) {
console.log($route.current.activeTab);
});
I would be interested in knowing why you may not able to use this routing pattern or purposely chose something different.
I think it has to do with the way you are creating your initial module. Try this:
var app = angular.module('app', []);
app.constant('configuration', {
PARTIAL_PATH: "/app/components/partials"
});
var routeServiceModule = angular.module('routeService', ['app']);

Categories