Lazy load angular 1.5 components - angularjs

I'm building an angular app and I want to load my components only when that route has been requested. I thought something like this might work
const states = [{
name: 'login',
url: '/login?sessid',
component: 'login',
resolve: {
login: () => import('./components/login.js').then(login => {
login.default.register()
}),
},
}];
Where the login component is
export default {
register() {
angular.module('trending')
.component('login', () => ({
template: 'Hello!'
})
)
}
};
But I get an error
angular.js?10a4:13708 Error: [$injector:unpr] Unknown provider:
loginDirectiveProvider <- loginDirective
Presumably because the login component isn't registered before the state is. I realize that this is abusing the purpose of the resolve property, I just hoped it would work for my needs. What's a better way to fix this?

After the app gets bootstrapped, you can't declare a component anymore. So, in order to register a component at run time, you have to expose, somehow, the respective factories. The code bellow exposes all factories on the main app module, but you can use only the component's if you prefer.
const app = angular.module('trending', []);
app.config(($controllerProvider, $compileProvider, $filterProvider, $provide) => {
app.register = {
component: $compileProvider.component,
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
});
This way, you just have to call app.register.component to access the exposed factory. For example, taken from your question:
export default {
register() {
angular.module('trending')
.register
.component('login', () => ({
template: 'Hello!'
})
)
}
};
You might find a fancier way to register your modules, this is just a demonstration for the sake of the answer, you can either change the nomenclature used or even the exposing mechanism to fit better on your fancy es6+ components.

Related

UI Router resolve not working with component-based states

Background:
On an app I am working on, we have a component that is being used in two places. In one place it's being called from the Material Design bottomSheet system. In another, we are using the component directly via the ui-router state system.
Here's the setup that's causing trouble. I've already got an angular.module statement that has all the proper package dependencies set up - I've been working on this app for months with my team, the problem is specifically the code below, which is what I've just added.
routes.ts
namespace Main {
RouteConfig.$inject = ['$stateProvider'];
function RouteConfig($stateProvider) {
$stateProvider
.state('main.myAwesomeFeature', {
url: '^/myawesomefeature',
component: 'awesomefeature',
resolve: {
awesomeDefaults: () => new Controllers.AwesomeDefaults(1, 2, 3)
}
});
// Other routing minutiae, unimportant to the question
}
angular.module('app').config(RouteConfig)
}
awesomefeature.ts
namespace Controllers {
export class AwesomeDefaults {
public constructor(
number1: number,
number2: number,
number3: number
) {
}
}
export class AwesomeFeatureCtrl {
public static $inject: string[] = [
'awesomeDefaults'
];
public controller(
public awesomeDefaults: AwesomeDefaults
) {
}
// ...Other methods and irrelevant stuff...
}
angular
.module('app')
.controller('awesomeFeatureCtrl', AwesomeFeatureCtrl);
}
namespace Components {
export var awesomeFeatureCmpt: ng.IComponentOptions = {
bindings: {},
controller: 'awesomeFeatureCtrl',
controllerAs: '$ctrl',
templateUrl: '(Irrelevant, as is the HTML)'
};
angular
.module('app')
.component('awesomefeature', awesomeFeatureCmpt);
}
Problem:
Whenever I try to navigate directly to the 'Awesome Feature', not only does my HTML not render, I receive the following console error:
angular.js:14525 Error: [$injector:unpr] Unknown provider: awesomeDefaultsProvider <- awesomeDefaults <- awesomeFeatureCtrl
http://errors.angularjs.org/1.6.4/$injector/unpr?p0=awesomeDefaultsProvider%20%3C-%20awesomeDefaults%20%3C-%20awesomeFeatureCtrl
at angular.js:66
at angular.js:4789
at Object.getService [as get] (angular.js:4944)
at angular.js:4794
at getService (angular.js:4944)
at injectionArgs (angular.js:4969)
at Object.invoke (angular.js:4995)
at $controllerInit (angular.js:10866)
at nodeLinkFn (angular.js:9746)
at angular.js:10154
It appears that for whatever reason, $stateProvider.state({resolve}) isn't properly resolving my awesomeDefaults and injecting the value into the awesomeFeatureCtrl.
Question:
Why isn't resolve working as I recall that it should?
To my understanding, the resolve object takes each named index on it, runs whatever function is on it, and then resolves it into the controller of the thing in the route, as per the UI Router Documentation. It's obvious I'm mis-remembering or mis-understanding something.
After looking at your error more closely, I’ve run into this issue before. Try changing this
resolve: {
awesomeDefaults: () => new Controllers.AwesomeDefaults(1, 2, 3)
}
To this
resolve: {
awesomeDefaults: /** ngInject */ () => new Controllers.AwesomeDefaults(1, 2, 3)
}
To properly inject awesomeDefaults.

Unit Testing AngularJS 'config' Written in TypeScript using Jasmine

I'm currently trying to Unit Test the config of a new AngularJS component. We are using ui-router to handle the routing in our application. We have been able to successfully test it for all our previous components, but the code for all of them was written in plain Javascript. Now that we switched to TypeScript we are having some issues.
This is the TypeScript code where we make the configuration of the module:
'use strict';
// #ngInject
class StatetiworkpaperConfig {
constructor(private $stateProvider: ng.ui.IStateProvider) {
this.config();
}
private config() {
this.$stateProvider
.state('oit.stateticolumnar.stateticolumnarworkpaper', {
url: '/stateticolumnarworkpaper',
params: { tabToLoad: null, groupTabId: null, jurisdiction: null, showOnlyItemsWithValues: false, showOnlyEditableItems: false},
template: '<stateticolumnarworkpaper-component active-tab-code="$ctrl.activeTabCode"></stateticolumnarworkpaper-component>',
component: 'stateticolumnarworkpaperComponent',
resolve: {
onLoad: this.resolves
}
});
}
//#ngInject
private resolves($q, $stateParams, ColumnarWorkpaperModel, ChooseTasksModel, localStorageService) {
// Some not important code
}
}
angular
.module('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper')
.config(["$stateProvider", ($stateProvider) => {
return new StatetiworkpaperConfig($stateProvider);
}]);
This is the Spec file, which is written in Javascript:
describe('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper', function () {
beforeEach(module('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper'));
beforeEach(module('oit'));
var state = 'oit.stateticolumnar.stateticolumnarworkpaper';
it('has a route', inject(function ($state) {
var route = $state.get(state);
expect(route.url).toBe('/stateticolumnarworkpaper');
}));
});
My issue is when executing the line var route = $state.get(state), as the route variable is always null. I could verify that the config() method is being executed, but I'm simply out of ideas as to why route is always null on my test.
Just for reference, this is the configuration of another component, but using Javascript
'use strict';
angular
.module('oit.components.binders.binder.dom_tas.taxaccountingsystem.stateworkpapers.stateworkpapersreview')
.config(stateworkpapersreviewConfig);
function stateworkpapersreviewConfig($stateProvider) {
$stateProvider
.state('oit.binder.taxaccountingsystem.stateworkpapersreview', {
url: '/stateworkpapersreview?reviewType&binderId&year&jurisdiction&chartId&withBalance',
templateUrl: 'components/binders/binder/dom_tas/taxaccountingsystem/stateworkpapers/stateworkpapersreview/stateworkpapersreview.tpl.html',
controller: 'StateworkpapersreviewController',
controllerAs: 'stateworkpapersreviewCtrl',
resolve: {
onLoad: resolves
}
});
function resolves($q, $stateParams, StateTiBinderJurisdictionsModel, WorkpaperModel, localStorageService, StateTiFiltersModel) {
// Some not important code
}
}
As you can see the code is basically the same, but still, I can successfully test this component's config in the way I described, but when I try with the one written in TypeScript I get the error I mentioned.
PD: I'm aware of several similar posts (like this one), but none of them deal with TypeScript, which is my issue.
There is huge difference between the TS snippet and the JS one.
I’m not sure why you are using a class to elite a function? .config suppose to get a function.
You can write the same code as in JS just with .ts suffix, it is a valid TS code.
Then you just can import that config function, pass it all the injectables and test it.

Protect routes and redirect user based on some conditions with Angular ui-router

The scenario is:
For some routes in my app, I want to force user to complete their profile first.
There are a few ways to achieve that:
Redirect inside the user resolve (where we load the user data)
Listen to $stateChangeStart event, load user from server, check if profile is complete
With method 1, is it possible to reuse the user resolve for other routes (given the fact that user resolve pending on auth resolve)?
With method 2, I feel like this method is bug-prone and hard to keep track.
Is there any best practice to this problem?
You can use service in the resolve to reduce the code in the routes and reuse the resolve function
Here is an example implementation of it.
$stateProvider
.state('inbox', {
...
resolve: {
profile: function (UserService) {
return UserService.getUser();
},
}
})
.state('dashboard', {
...
resolve: {
profile: function (UserService) {
return UserService.getUser();
},
}
});
Your controller can be,
angular
.module('app')
.controller('DashboardCtrl', DashboardCtrl);
function DashboardCtrl(profile,$scope) {
this.user_profile = profile;
}
HEre is the reference
Update:
You can use the redirection in the resolve like,
resolve: {
profile: function (UserService,$state) {
return UserService.getUser().then(function(profile){
if (profile.id) {
return profile;
} else {
$state.go('/')
}
},
}

How to change data into Angular 1.5 Component via an external Service?

I make an app with Angular 1.5 Components. I provide a data into component via resolve parameter, in that way I can display any data from different sources in the same component. But I don't understand, how to change some data in my component.
For example, I have a User service, which works with users through the API. In my state I load a component and use method Users.get(). I use UI Router.
//...
$stateProvider
.state('users', {
url: '/users',
component: 'formPage',
resolve: {
values: function(Users) {
return Users.get();
});
//...
//...
component('formPage', {
bindings: {
values: '<'
},
//...
I have a form in this component and want to change the data. I want to call Users.update() method, when form will be submitted. But the component don't know anything about Users service and that's right.
How i may specify that component must use Users.update() for update the data in this state? And how I call this method in the component when form will be submitted?
resolve: {
values: function(Users) {
return Users.get();
},
onUpdate: function(){
return Users.update.bind(Users);
}
);
bindings: {
values: '<',
onUpdate: '<'
},
and you can call it like $ctrl.onUpdate(data).then(...

Where should I place code to be used across components/controllers for an AngularJS app?

Should it be associated with the app module? Should it be a component or just a controller? Basically what I am trying to achieve is a common layout across all pages within which I can place or remove other components.
Here is what my app's structure roughly looks like:
-/bower_components
-/core
-/login
--login.component.js
--login.module.js
--login.template.html
-/register
--register.component.js
--register.module.js
--register.template.html
-app.css
-app.module.js
-index.html
This might be a bit subjective to answer but what I personally do in a components based Angular application is to create a component that will encapsulate all the other components.
I find it particularly useful to share login information without needing to call a service in every single component. (and without needing to store user data inside the rootScope, browser storage or cookies.
For example you make a component parent like so:
var master = {
bindings: {},
controller: masterController,
templateUrl: 'components/master/master.template.html'
};
function masterController(loginService) {
var vm = this;
this.loggedUser = {};
loginService.getUser().then(function(data) {
vm.loggedUser = data;
});
this.getUser = function() {
return this.loggedUser;
};
}
angular
.module('app')
.component('master', master)
.controller('masterController', masterController);
The master component will take care of the routing.
index.html:
<master></master>
master.template.html:
<your common header>
<data ui-view></data>
<your common footer>
This way, every component injected inside <ui-view> will be able to 'inherit' the master component like so:
var login = {
bindings: {},
require: {
master: '^^master'
},
controller: loginController,
templateUrl: 'components/login/login.template.html'
};
and in the component controller
var vm=this;
this.user = {};
this.$onInit = function() {
vm.user = vm.master.getUser();
};
You need to use the life cycle hook $onInit to make sure all the controllers and bindings have registered.
A last trick to navigate between components is to have a route like so (assuming you use ui-router stable version):
.state('home',
{
url : '/home',
template : '<home></home>'
})
which will effectively route and load your component inside <ui-view>
New versions of ui-router include component routing.

Resources