Ui-router: inject service from resolved module in next resolve - angularjs

I have a state that needs to load an entire module asynchronously. The following code does that for me, and it works great (for the sake of clarity, I removed anything not needed, including annotations for automated injections):
$stateProvider.state('root.lazy', {
url: '/lazy',
resolve: {
loadModule: function($ocLazyLoad) {
return getModuleAsync().then(function(module) {
return $ocLazyLoad.load({
name: module.name
});
});
}
});
Now, this module also register a service - say lazyService which gets data asynchronously. I'd like to resolve these data once the module is loaded. So I tried:
$stateProvider.state('root.lazy', {
url: '/lazy',
resolve: {
loadModule: function($ocLazyLoad) {
return getModuleAsync().then(function(module) {
return $ocLazyLoad.load({
name: module.name
});
});
},
data: function(loadModule, lazyService) { // So ui-router knows it needs the previous resolve; but it fails trying to inject lazyService
return lazyService.getData();
}
});
But ui-router gives me the following error: Unknown provider: lazyServiceProvider <- lazyService.
Using some debugging logs, I found out that ui-router may try to inject everything needed in the resolve functions before actually resolving anything. Indeed, the error pops before any logs I put in the loadModule function. But loadModule resolves eventually, and then everything is available but the data.
Is there any way to accomplish what I want without using a super state resolving the module and a nested state resolving the data? Given the current app context, it makes sens for us to resolve both things in the same state.
EDIT: I'm using ui-router 1.0.4 with AngularJS 1.5

Related

How to use ocLazyLoad resolved file in ui-router resolve property

I want to resolve an api call before view showing it's straightforward by using ui-router resolve property but my resolve property dependent on ocLazyLoad resolved file. So, I getting this error Error: [$injector:unpr] Unknown provider: SavedFactoryProvider <- SavedFactory
This is my code
$stateProvider.state('app.saved', {
url: '/saved',
templateUrl: 'app/modules/saved/views/saved.html',
controller: 'SavedSearchCtrl',
resolve: {
loadFiles: ['$ocLazyLoad', function($ocLazyLoad) {
return $ocLazyLoad.load([{
name: 'app.saved',
files: [
'app/modules/saved/controller.js',
'app/modules/saved/factory.js',
],
cache: false
}]);
}],
searches: ['loadFiles', 'SavedFactory', function(loadFiles, SavedFactory) {
return SavedFactory.getSavedSearches();
}]
}
});
Thank you!
Route resolvers are resolved in parallel with $q.all. Since $ocLazyLoad.load(...) is asynchronous, it surely won't be completed at the moment when searches is called.
$ocLazyLoad.load(...) returns a promise which is can be chained in order to avoid race conditions, something like:
searches: function($ocLazyLoad, $injector) {
return $ocLazyLoad.load([
{
name: 'app.saved',
files: [
'app/modules/saved/controller.js',
'app/modules/saved/factory.js',
],
cache: false
}
])
.then(function () {
var SavedFactory = $injector.get('SavedFactory');
return SavedFactory.getSavedSearches();
});
}
As opposed to ngRoute, UI Router supports a hierarchy of resolvers; a graph of dependencies is being built on state change. The order in which they are resolved can be determined by their dependencies. So searches should list loadFiles as its dependency:
searches: function(loadFiles, $injector) {
var SavedFactory = $injector.get('SavedFactory');
return SavedFactory.getSavedSearches();
}
$injector.get is apparently necessary due to the fact how UI Router invokes resolver functions internally.

AngularJS: route.resolve in ES6

I am trying to refactor an existing Angular-Project to use ES6-Modules and import statements. This works for the majority of the application, but the resolve blocks in my routes are giving me trouble.
As far as I can tell the syntax should be the same as before:
$stateProvider.state('stateName'), {
template: <div></div>
controller: 'stateCtrl'
controllerAs: 'ctrl'
resolve: {
someData: ['DataService', function(DataService){
return DataService.getData();
}]
}
}
However setting a breakpoint in someData tells me that my 'DataService' is not resolved properly by angulars dependency injection (or rather: it is resolved properly, but was not initialized yet). The service gets registered on the according module, but its constructor did not get called before entering the someData-function.
Since i thought that i got the syntax wrong i experimented a bit and found that some other Services (created with the same syntax and registered on the same module as the DataService) are actually initialized and injected properly within the resolve-block.
Do you have any idea where to look or how i can troubleshoot this?
If you're using ES6, it should be like this:
/* #ngInject */
export default function config($stateProvider) {
resolve: {
/* #ngInject */
someData: (DataService) => DataService.getData()
}
}
To read a little more about ngInject see the ng-annotate github page. It does the injections for you.

how loading more than one module and dependency injection in one route in Angular app using ocLazyLoad?

I could inject lazy loaded libraries to my modules according to this Stackoverflow answers, but the answer is just describing and working with injecting one module (even with more than one file). But, I have a route and I want to lazy loading two libraries in that route (ngMap and ngFileUpload)
Here is my code with one dependency injection:
$routeProvider.when('/test-route', {
templateUrl: 'test.html',
controller: 'testController',
resolve: {
store: function ($ocLazyLoad) {
return $ocLazyLoad.load(
{
serie: true,
name: "ngMap",
files: [
"./files/ngMap.js"
]
}
);
}
}
});
But I also need ngFileUpload in this route, but I don't know how to lazy loading it. Help me please
Thank you very much
This website is verry Useful and Helpful

Resolve must contain all promises even from controller?

Probably it's just as easy as I think it is, but I cannot really find an answer to my question on the internet, so I hope you guys know the answer just by looking at a small piece of my code.
Problem: I'm using the UI router in Angular and it loads the template before all the data is loaded. So all input fields receive the correct values AFTER the template is already loaded. So the input fields are empty for a second or two....
I think my resolve is not as it should be:
So my ui-router code looks something like this (check the resolve object):
$stateProvider.state('teststate', {
url: '/test/',
templateUrl: 'app/page/template.html',
controller: 'testCtrl',
resolve: {
access: ["Access", function(Access) { return Access.isAuthenticated(); }],
UserProfile: 'UserProfile'
}
});
Now the controller contains the promise to get some data from an API url:
function TestCtrl($scope, $state, $stateParams, TestService) {
TestService.get($stateParams.id).then(function(response) {
$scope.data = response;
});
}
Now the service (which connects to the API) should return the promise to the Controller:
TestService.factory('TestService', ['Restangular', function(Restangular) {
var factory = {};
factory.get = function(id) {
return Restangular.one('api/test', id).get();
}
return factory;
}]);
Now, could the problem be, that because the TestService.get() (which connects to the API) within the Controller, gets executed NOT before the template is loaded, because it's not inside the resolve object? So the UI router doesn't resolve the call to the API? I'm just curious or I should move all methods which make API calls, to the resolve object of each stat inside the $stateProvider.
I could run a lot of tests, but if someone just directly knows the answer by just looking at this question, it helps me a lot.
Your assumptions are all correct.
If you resolve the TestService.get in routing config the data would be readily available to controller as an injectable resource
If you don't want your controller to run and your template to show before all your API calls are finished, you have to put all of them inside ui-routers resolve.
However, if API requests can take a little while it seems better UX to transition to the new page immediately and show some kind of loading indicator (e.g. block-ui) while your API call is running.

Angular "state already defined" error after running concat/uglify

I am developing an AngularJS application. To ship the code in production, I'm using this Grunt configuration/task:
grunt.registerTask( 'compile', [
'sass:compile', 'copy:compile_assets', 'ngAnnotate', 'concat:compile_js', 'uglify', 'index:compile'
]);
It's really hard to debug, and it's kind of a question to people who already ran into such problems and can point to some direction.
My main module is including those submodules:
angular
.module('controlcenter', [
'ui.router',
'ui.bootstrap',
'templates-app',
'templates-common',
'authentication',
'api',
'reports',
'interceptors',
'controlcenter.websites',
'controlcenter.users',
'controlcenter.campaigns',
'controlcenter.reports',
'controlcenter.login'
])
.run(run);
The error I get is following:
Uncaught Error: [$injector:modulerr] Failed to instantiate module controlcenter due to:
Error: [$injector:modulerr] Failed to instantiate module controlcenter.websites due to:
Error: State 'websites'' is already defined
If I remove the websites module, I get the same error for
controlcenter.users.
I am using the ui-router to handle routing inside the app.
After my build process (for integration testing), everything works just fine:
grunt.registerTask( 'build', [
'clean', 'html2js', 'jshint', 'sass:build',
'concat:build_css', 'copy:build_app_assets', 'copy:build_vendor_assets',
'copy:build_appjs', 'copy:build_vendorjs', 'copy:build_vendorcss', 'index:build', 'karmaconfig',
'karma:continuous'
]);
So maybe ngAnnotate or or concat/uglify are doing weird things here?
UPDATE 1:
It has something to do with my configuration of the modules. Here is the code:
angular
.module('controlcenter.websites',
[
'ui.router'
]
)
.config(config);
config.$inject = ['$stateProvider'];
function config($stateProvider) {
$stateProvider.state( 'websites', {
url: '/websites',
views: {
"main": {
controller: 'WebsitesController',
templateUrl: 'websites/websites.tpl.html'
}
}
});
}
When I change the name of the state to websites_2, I get an error
with 'websites_2 is already defined'.
When I remove the module completely, the next one hast the same problem inside the config file. So is the structure wrong?
Update 2:
The problem seems concat related.
It takes every JS file and adds it one after another to one, bigger file. All of my modules are at the end. The last module always has the problem with 'state already defined'. So it's not just the order of the modules appending to each other, it's something elsse...
Update 3:
I placed my code (I've excluded every Controller-Code and functions, just the scaffold) in a gist. This is the outcome after my compile process, without uglifying it.
Issue:
You have multiple files that contains a config function to configure your module, like this:
angular
.module('controlcenter.websites', [])
.config(config);
function config() {
// ...
}
The problem is that after you concatenate all files you end up with a big file with multiple declarations of config. Because of JavaScript's variable hoisting, all declarations are moved to the top and only the very last of them is evaluated, and this one is:
function config($stateProvider) {
$stateProvider.state( 'websites', {
url: '/websites',
views: {
"main": {
controller: 'WebsitesController',
templateUrl: 'websites/overview/websites.tpl.html'
}
},
data : {requiresLogin : true }
});
}
Hence, each time you .config(config) a module, you are telling Angular to configure your module with that particular configuration function, which means that it executes multiple times and tries to define the state websites more than once.
Solution:
Wrap each JavaScript file code with a closure. This way you will avoid declaring a variable/function more than once:
(function (angular) {
'use strict';
angular
.module('controlcenter.website.details', ['ui.router'])
.config(config);
config.$inject = ['$stateProvider'];
function config($stateProvider) {
$stateProvider
.state( 'websiteDetails', {
url: '/websiteDetails/:id',
views: {
"main": {
controller: 'WebsiteDetailsController',
templateUrl: 'websites/details/website.details.tpl.html'
}
},
data : {requiresLogin : true }
})
.state( 'websiteDetails.categories', {
url: '/categories',
views: {
"detailsContent": {
templateUrl: 'websites/details/website.details.categories.tpl.html'
}
},
data : {requiresLogin : true }
})
;
}
})(window.angular);
Edit:
I strongly recommend you wrap your files into closures. However, if you still don't want to do that, you can name your functions according to their respective modules. This way your configuration function for controlcenter.website.details would become controlcenterWebsiteDetailsConfig. Another option is to wrap your code during build phase with grunt-wrap.
window.angular and closures: This is a technique I like to use on my code when I'm going to uglify it. By wrapping your code into a closure and giving it a parameter called angular with the actual value of window.angular you are actually creating a variable that can be uglified. This code, for instance:
(function (angular) {
// You could also declare a variable, instead of a closure parameter:
// var angular = window.angular;
angular.module('app', ['controllers']);
angular.module('controllers', []);
// ...
})(window.angular);
Could be easily uglified to this (notice that every reference to angular is replaced by a):
!function(a){a.module("app",["controllers"]),a.module("controllers",[])}(window.angular);
On the other side, an unwrapped code snippet like this:
angular.module('app', ['controllers']);
angular.module('controllers', []);
Would become:
angular.module("app",["controllers"]),angular.module("controllers",[]);
For more on closures, check this post and this post.
If you check it in the concatenated file, do you have the states defined twice? Can it be that you are copying the files twice? Check the temporary folders from where you are taking the files (also in grunt config, what you are copying and what you are deleting...).
So I had the same problem but with the following setup:
yeoman angular-fullstack (using typescript)
Webstorm
With the angular-fullstack configuration, the closures were already implemented (as Danilo Valente suggests) so I struggled quite a bit until I found out that in Webstorm, I had the typescript compiler enabled which compiled all of my *.ts files to *.js. But since Webstorm is so 'smart', it does not show these compiled files in the working tree. Grunt however concatenated of course all files regardless if it is typescript of JS. That's why - in the end- all of my states were defined twice.
So the obvious fix: Disabled typescript compiler of webstorm and deleted all the generated *.js files and it works.

Resources