UI Router resolve not working with component-based states - angularjs

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.

Related

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.

Lazy load angular 1.5 components

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.

Firebase Angularjs prevent user to display app pages if user not verified yet

For an angularjs web project using firebase backend, registered users shouldn't be able to display application pages if they are not verified yet. I use that code below at each page controller:
function config($stateProvider)
{
.state('app.pages_profile', {
url : '/pages/profile',
views : {
'content#app': {
templateUrl: 'app/main/pages/profile/profile.html',
controller : 'ProfileController as vm'
}
},
resolve : {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
}
At IndexController I aimed to listen auth state changes and if user is not verified yet redirect to verification warning page.
(function ()
{
'use strict';
angular
.module('xyz')
.controller('IndexController', IndexController);
function IndexController(Auth, $state)
{
var vm = this;
Auth.$onAuthStateChanged(function(firebaseUser) {
if(firebaseUser)
{
if (firebaseUser.emailVerified==false) {
$state.go ("app.pages_auth_verification");
}
}
});
}
})();
In fact it works the way I want it ( it redirects as I want) but on console log it shows also an error:
angular.js:14525 Error: transition superseded
at $StateProvider.$get (angular-ui-router.js:2909)
at Object.invoke (angular.js:5003)
at angular.js:4795
at Object.getService [as get] (angular.js:4944)
at angular-ui-router.js:3696
at Object.invoke (angular.js:5003)
at angular.js:4805
at forEach (angular.js:403)
at createInjector (angular.js:4805)
at doBootstrap (angular.js:1914) "Possibly unhandled rejection: {}
The way I use is right? And/or is there a better way to control and prevent user if not verified.

TypeScript minified breaks Angular UI Modal

I have an MVC application with lots of Angular code. I have an Angular service that creates an Angular UI modal. The pertinent code is:
export class ConfirmRenderingOfLargeResultsModalService {
static $inject = ["$uibModal"];
constructor(private $uibModal: any) {
}
public open(options: LargeResultSetModalOptions): ng.IPromise<LargeResultsModalAction> {
var modalInstance = this.$uibModal.open(
{
templateUrl: "myTemplate.html",
controller: "ConfirmRenderingOfLargeResultsModalController",
controllerAs: "confirmRenderingOfLargeResultsModalController",
backdrop: "static",
resolve: {
modalOptions: () => options
}
});
return modalInstance.result.then((action: LargeResultsModalAction) => {
return action;
});
}
}
angular.module("My.Module").service("Services.ConfirmRenderingOfLargeResultsModalService", Services.ConfirmRenderingOfLargeResultsModalService);
The controller (snippet) is:
export class ConfirmRenderingOfLargeResultsModalController {
public confirmRenderingOfLargeResultsModalController: ConfirmRenderingOfLargeResultsModalController = this;
//static $inject = ["$uibModalInstance"];
constructor(private $uibModalInstance: any, private modalOptions: LargeResultSetModalOptions) {
}
public ok(): void {
this.$uibModalInstance.close(LargeResultsModalAction.Continue);
}
// more methods not shown
}
angular.module("My.Module").controller("Services.ConfirmRenderingOfLargeResultsModalController", Services.ConfirmRenderingOfLargeResultsModalController);
Now, in my dev env, this works fine. The modal pops up, the controller has the $uibModalInstance and modalOptions parameters correctly injected. In Production, with bundling enabled, it breaks - in that nothing happens at all. No console error either.
If I uncomment the static inject line on the controller, then it breaks in dev and in Production the modal pops up but doesn't work correctly because presumably it's not the modal instance instantiated by the service as the modalOptions parameter remains undefined.
My question is, how can I minify this code and still have the correct instance of $uibModalInstance injected? Hopefully I'm missing something very simple and all the hair that has been pulled today has been a mere exercise in frustration.
I would suggest that you use what is known as safe or inline dependency injection annotation.
For example in your controller:
angular.module("My.Module")
.controller("Services.ConfirmRenderingOfLargeResultsModalController", ["uibModalInstance", "modalOptions", Services.ConfirmRenderingOfLargeResultsModalController]);
This will preserve the dependency names when you minify your code.

Cant get angular component router to work with lazyloading of components

Hello fellow stackoverflowers,
I've been trying for some time now without success to implement
lazy loading of angular components with the new component router (ngComponentRouter).
This is the code I used for the configuration of the main component:
//define main app component
app.value("$routerRootComponent","simpleTrackerApp");
//main app component config
var lazyLoaderMod = null;
var httpService = null;
app.component("simpleTrackerApp", {
/*#ngInject*/
controller:function ($ocLazyLoad,$http) {
lazyLoaderMod = $ocLazyLoad;
httpService = $http;
},
templateUrl: CONFIG_APP_SHARED_URL + 'mainComponent/simpleTrackerApp.html',
$routeConfig: [
{
useAsDefault:true,
path:"/bugs",
name:"Bugs",
loader: function () {
return lazyLoaderMod.load(CONFIG_APP_COMPONENTS_URL + 'bug/bugs/bugsCtrl.js');
}
},
{path:'/**',redirectTo:['Bugs']}
]
});
My first problem was I could'nt get it to work with ocLazyLoad injected in loader propety so I loaded it in the main controller and referenced it in the loader property.
After that when I finally got it working inside loader propety
I couldn't seem to get it to actually load the component I kept getting this error:
lib.js:2 TypeError: Cannot read property 'then' of undefined
at e._loader (http://simpletracker.co.il/client/production/app/lib.js:9:10670)
at e.resolveComponentType (http://simpletracker.co.il/client/production/app/lib.js:9:21463)
at e.recognize (http://simpletracker.co.il/client/production/app/lib.js:9:23535)
at http://simpletracker.co.il/client/production/app/lib.js:9:25949
at Array.forEach (native)
at e.recognize (http://simpletracker.co.il/client/production/app/lib.js:9:25921)
at e._recognize (http://simpletracker.co.il/client/production/app/lib.js:10:4834)
at e.recognize (http://simpletracker.co.il/client/production/app/lib.js:10:4605)
at t.e.recognize (http://simpletracker.co.il/client/production/app/lib.js:10:13656)
at http://simpletracker.co.il/client/production/app/lib.js:10:10757
Now I understand I'm obviously doing something wrong in the loader
and that I need to somehow register the component. but I couldn't find
any docs on angular for the loader and nobody else lazyloading components with the new router so I really couldn't figure how this could be achieved.
I'd really appreciate some help on this but I'm also wondering in general
if maybe it is premature to use angular component router
with its lack of documentation and support.
So I'd really like to hear from other expierenced angular programmers
their opinion on the matter.
Edit:Anyone have any insight on the matter?..
You could use $router instead of the http-service. Applied on your code:
app.component("simpleTrackerApp", {
templateUrl: 'mainComponent/simpleTrackerApp.html',
controller: ['$router', '$ocLazyLoad', function ($ocLazyLoad, $http) {
$router.config([
{
path: '/bugs/',
name: "Bugs",
loader: function () {
return $ocLazyLoad.load('/bugsFolder/bugs.component.js') //no url, set the path to your component
.then(function () {
return 'bugs' //name of your component
})
}
}
])
}],
});
And don't forget to inject your dependencies in the app.module.

Resources