Modularize UI Router - javascript

I'm currently working on a project using UI Router. My code currently defines states as part of the app config ( example below ) but the code is growing. Is there a good way to modularize this code both for organization and unit testing? For me the best solution would be to define states as an external service.
.state('page', {
url: '/page/{id}',
params: {
id: ['$q', function ($q) {
// Code
return defaultValue;
}],
},
templateUrl: 'page.html',
'controller': 'CatalogDetailsController',
'controllerAs': 'details',
resolve: {
categories: ['$q', function ($q) {
// Code
return promise;
}],
},

I would start by defining the objects separately instead of in-line
( do note that this does make the code less readable)
.state('page', {
url: '/page/{id}',
params: myparamsObj // defined somewhere else.
templateUrl: 'page.html',
'controller': 'CatalogDetailsController',
'controllerAs': 'details',
resolve: myResolveObj, // defined somewhere else.
If your app.config is becoming too big , you could use the approach mentioned in [refactor large AngularJS module config into separate files ] question to split your config part.

My proposal is based on ES6 modules.
Long story short. Each state has a separate folder, for instance contacts/one/edit. In this folder I have the following files:
controller.js, controller.spec.js
state.js, state.spec.js
state.html
state.js holds the state definition object:
import controller from './edit.controller';
import template from './edit.state.html';
// State name is exported so we can use it in the corresponding tests
export const name = 'contacts.one.edit';
export default {
name,
url: '/edit',
template,
controller,
controllerAs: 'ctrl'
};
This configuration could be activated in the module's configuration block:
import oneState from './one/one.state';
export function states($stateProvider) {
'ngInject';
$stateProvider
.state({
parent: 'app',
name: 'contacts',
abstract: true,
url: '/contacts',
template: '<div ui-view autoscroll="true"></div>'
})
.state(oneState)
.state(oneEditState)
// etc...
}
Here you will find the complete example https://github.com/lucassus/angular-webpack-seed/tree/ce4e9b91ce9ed47ca74073d754b0cbacff8cb65f/src/app/contacts/one/edit

Related

angularjs route to resolve

I have a big number of angularjs routes in my app. I would like to set access to these route based on some user permission levels.
angular.module('myApp').run(['$rootScope', 'someAuthFactory', function($rootScope, someAuthFactory) {
$rootScope.$on('$stateChangeStart', function (event, toState) {
$rootScope.permissions = someAuthFactory.getPermssionLevels();
$rootScope.specialRights = $rootScope.permissions.indexOf('superRole') > -1;
...
and here is one of my routes:
.state("dashboard.overview", {
url: "/dashboard",
templateUrl: "app/dashboard.html",
resolve: {
roles: ['rootScope', function (rootScope) {
return $rootScope.specialRights;}]
},
so this code works, but if i want to add this:
resolve: {
roles: ['rootScope', function (rootScope) {
return $rootScope.specialRights;}]
}
to every route, it is gonna be duplicate code, or if I want to lookup some other role, it is gonna be boring.
Could we make the resolve part much smaller and much cleaner?
create a variable in config function on top of the routes like this
var resolveRoles = ['rootScope', function (rootScope) {
return $rootScope.specialRights;}]
}
and use it in every route like this,
.state("dashboard.overview", {
url: "/dashboard",
templateUrl: "app/dashboard.html",
resolve: {
roles: resolveRoles
},
});
You can create a service/factory, which shares $rootScope.specialRights. Just init $rootScope.specialRights in the service in the first resolve of your route e.g.
.state("dashboard", {
url: ...
templateUrl: ...
resolve: {
roles: ['rootScope', function (rootScope) {
YourServiceOrFactory.setSpecialRights($rootScope.specialRights);
return $rootScope.specialRights
}]
},
And there where you need it
YourServiceOrFactory.getSpecialRights()
It does not have to be in other resolves, just in your controller of your route or in your directive/component. Much cleaner and performanter than multiple resovles.
Since you're already setting this property in $rootScope, it is an overkill to use resolve in all of your routes, just to get a variable value, and of course, resolve is not designed for such purposes.
Instead, just inject the $rootScope in your controller and use $rootScope.specialRights in the controller.
Example:
In your controller, service, directive, or component, you can inject the $rootScope like this:
angular.module('your_module')
.controller('your_controller', ['$rootScope', function($rootScope) {
// Access $rootScope.permissions or $rootScope.specialRights here
}];

angular uirouter opens wrong url

I'm new to Angular 1 and have to implement a new feature on an existing webapp. The app uses jhipster to generate some parts of the backend and frontend (Angular 1 and uirouter).
So I tried to use my own route and state like this which is mostly copy and pasted from existing components of the webapp:
(function() {
'use strict';
angular
.module('artemisApp')
.config(stateConfig);
stateConfig.$inject = ['$stateProvider'];
function stateConfig($stateProvider) {
$stateProvider
.state('model-comparison-exercise-for-course', {
parent: 'entity',
url: '/course/{courseid}/model-comparison-exercise',
data: {
authorities: ['ROLE_ADMIN', 'ROLE_TA'],
pageTitle: 'artemisApp.modelComparisonExercise.home.title'
},
views: {
'content#': {
templateUrl: 'app/entities/model-comparison-exercise/model-comparison-exercise.html',
controller: 'ModelComparisonExerciseController',
controllerAs: 'vm'
}
},
resolve: {
translatePartialLoader: ['$translate', '$translatePartialLoader', function ($translate, $translatePartialLoader) {
$translatePartialLoader.addPart('modelComparisonExercise');
$translatePartialLoader.addPart('exercise');
$translatePartialLoader.addPart('global');
return $translate.refresh();
}],
courseEntity: ['$stateParams', 'Course', function ($stateParams, Course) {
return Course.get({id: $stateParams.courseid}).$promise;
}]
}
});
}
})();
Then I try to open this route with the following code:
<a ui-sref="model-comparison-exercise-for-course({courseid:course.id})"
data-translate="artemisApp.course.modelComparisonExercises"></a>
By clicking on that link a http get request is fired which returns a http status code 404: http://localhost:8080/app/entities/model-comparison-exercise/model-comparison-exercise.html
Actually, the url that should be opened is http://localhost:8080/#/course/1/model-comparison-exercise
Any idea what I could have configured wrong?
Please try changing 'content#' to 'content#artemisApp'.
As explained here:
The symbol before the # is the name of the view you want to match, and the symbol after the # is a reference to the state in which the template the ui-view directive should exist in.
And the <a> tag is not being closed:
<a ui-sref="model-comparison-exercise-for-course({courseid:course.id})"
data-translate="artemisApp.course.modelComparisonExercises"></a>
Searching through the code, I found that model-comparison-exercise.html does not exist in folder model-comparison-exercise. Besides model-comparison-exercises.html exist.

Angular app config crash after add second view

I have two different login views login.html and adminLogin.html so in Angular app.js I added first one as:
$stateProvider
.state('login', {
url: "/login.html",
templateUrl: "../login.html",
controller: "login",
authenticate: false,
resolve: {
deps: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'myapp',
insertBefore: '#ng_load_plugins_before', // load the above css files before a LINK element with this ID. Dynamic CSS files must be loaded between core and theme css files
});
}]
}
})
and it runs correctly, problem is when I try to add second one below this one as:
.state('login', {
url: "/adminLogin.html",
templateUrl: "../adminLogin.html",
controller: "adminLogin",
authenticate: false,
data: { pageTitle: 'INICIAR SESIÓN' },
resolve: {
deps: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'myapp',
insertBefore: '#ng_load_plugins_before', // load the above css files before a LINK element with this ID. Dynamic CSS files must be loaded between core and theme css files
}]
}
})
When I try to acces first one or new one it only show blank page
Note**: if I comment last code, first one runs correctly again.
Some one know what is wrong there? Help is very appreciated. Regards
You have two states with the same name: login. The UI-Router has no idea what to do in that situation. Try re-naming your second state to something like adminLogin.

angularjs throws Error: [ng:areq] in resolving the lazyloading the scripts and adding a view controller separately

I don't know why this first state works but the second one doesn't:
Working state:
.state('app.pages.invoice', {
url: '/invoice',
templateUrl: "assets/views/pages_invoice.html",
title: 'Invoice',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
Not working state, throwing the Error: [ng:areq]:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
.state('app.form.validation', {
controller: "validationCtrl",
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
This second one works only when the controller is injected via this:
resolve: loadSequence('validationCtrl')
that is (controller is moved into the resolve and there is not currentAuth anymore):
.state('app.form.validation', {
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: loadSequence('validationCtrl')
})
and then I don't know how to integrate my currentAuth element into the resolve again. How can I inject the validationCtrl.js via resolve and add the currentAuth element also into resolve?
here is the loadsequence function:
// Generates a resolve object previously configured in constant.JS_REQUIRES (config.constant.js)
function loadSequence() {
var _args = arguments;
return {
deps: ['$ocLazyLoad', '$q',
function ($ocLL, $q) {
var promise = $q.when(1);
for (var i = 0, len = _args.length; i < len; i++) {
promise = promiseThen(_args[i]);
}
return promise;
function promiseThen(_arg) {
if (typeof _arg == 'function')
return promise.then(_arg);
else
return promise.then(function () {
var nowLoad = requiredData(_arg);
if (!nowLoad)
return $.error('Route resolve: Bad resource name [' + _arg + ']');
return $ocLL.load(nowLoad);
});
}
function requiredData(name) {
if (jsRequires.modules)
for (var m in jsRequires.modules)
if (jsRequires.modules[m].name && jsRequires.modules[m].name === name)
return jsRequires.modules[m];
return jsRequires.scripts && jsRequires.scripts[name];
}
}]
};
}
My first state doesn't have any controller, so I am fine resolving the currentAuth alone. But when the view has a controller, add the currentAuth causes the controller not to work anymore.
note:
my currentAuth is taken from here.
update:
herer is the validationCtrl.js:
app.controller('ValidationCtrl', ["$scope", "$state", "$timeout", "SweetAlert", "$location",
function ($scope, $state, $timeout, SweetAlert, $location) {
...
update 2:
basically the question is to allow only currently signed in users to view the pages which are children of app.; so my parent view is like this: so basically I am looking to inject the currentAuth factory into the main parent view and the children should inherit this. They cannot be viewed unless the currentAuth in the parent is resolved.
$stateProvider.state('app', {
url: "/app",
templateUrl: "assets/views/app.html",
resolve: loadSequence('modernizr', 'moment', 'angularMoment', 'uiSwitch', 'perfect-scrollbar-plugin', 'toaster', 'ngAside', 'vAccordion', 'sweet-alert', 'chartjs', 'tc.chartjs', 'oitozero.ngSweetAlert', 'chatCtrl'),
abstract: true
})
edit 1:
I have put the question in other words as well here and trying to find an answer to multiple resolve states.
edit 2:
here is the main.js: pastebin url
and the validationCtrl.js pastebin url.
actually, the validationCtrl is just an example controller among other controllers I have.
Question is how to block view permission for child views unless the parent currentAuth is resolved? given that I don't know how to handle multiple resolve with loadsequence and a singleton factory.
Assuming you are using ui router as a routing framework for your SPA app.
Error: [ng:areq]
the error you get:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
is due to the declaration of the controller within the state, the name of the controller function is not resolved because "ValidationCtrl" is not equal to "validationCtrl" then correct state is:
.state('app.form.validation', {
controller: "ValidationCtrl",
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
Abstract States - Nested States
to answer the second question, a useful example for your case may be this:
$stateProvider.state('app', {
url: "/app",
templateUrl: "assets/views/app.html",
resolve: {
scripts: loadSequence('modernizr', 'moment', 'angularMoment', 'uiSwitch', 'perfect-scrollbar-plugin', 'toaster', 'ngAside', 'vAccordion', 'sweet-alert', 'chartjs', 'tc.chartjs', 'oitozero.ngSweetAlert', 'chatCtrl').deps,
currentAuth: function(Auth){ return Auth.$requireSignIn();}
},
abstract: true
})
.state('app.pages.invoice', {
// url will become '/app/invoice'
url: '/invoice',
templateUrl: "assets/views/pages_invoice.html",
title: 'Invoice'
})
.state('app.form.validation', {
controller: "ValidationCtrl",
// url will become '/app/validation'
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate'
})
As you can see from the example in the resolve of the abstract state you can define different factory functions, ui router will wait until all dependencies are resolved before resolving the children states.
Resolve property explanation :
The resolve property is a map object. The map object contains key/value pairs of:
key – {string}: a name of a dependency to be injected into the controller.
factory - {string|function}:
If string, then it is an alias for a service.
Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before the controller is instantiated and its value is injected into the controller.
for more details I refer you to ui router doc.
As said in my comment i suggest you to try the following :
.state('app.form.validation', {
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
controller: "validationCtrl",
resolve:{
"myCtrl": loadSequence('validationCtrl'),
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
The other part of my comment was about the fact that child states inherits parent's resolve and children can override it.
So you can just do the following :
.state('app', {
// all states require logging by default
resolve:{
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
// i'm guessing we're redirecting toward app.login if not logged
}]
}
})
.state('app.login', {
resolve:{
"currentAuth": ["Auth", function(Auth) {
return true;// just be sure to not do infinite redirections
}]
}
})
Note if you have some trouble because Auth isn't yet loaded with lazy loading, you should be able to load it in a angular.run.
Firstly coming to the error
Error: [ng:areq]:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
controller: "validationCtrl", change it according to the main controller
that is
This means there is no validationCtrl function.
I may be wrong but i think there is a small typo' in this line in your controller controller: "validationCtrl", change it according to the **main controller** controller: "ValidationCtrl"
that is
This error happens due to either defining two angular.modules with the same name in different files containing different arguments as you may be trying to implement the dependancy injection.
It causes the problem as the script loaded inyour main html file won't know which angular.module to be configured.
To resolve this define the angular.modules with different names.
How to block view permission for child views unless the parent currentAuth is resolved
You can install this package angular middlewareThis middleware package contains some pre-defined route functions or you can also create your own functions.Along with this $http documentation using the success and callback functions,you can create your own middleware and the auth service while using a singleton factory
OR
Assuming that you are using node.js as your backend you can use [middleware][3] routing in your server using express and map it to the frontend routes.
Here is a perfect tutorial for middleware authentication in nodejs

Angular UI Router - How to Pass Resolve Data to a Nested View from Parent State

I'm currently working on an Angular web application which will display various types of financial events for the user. These events are tied together under one ID, organized into different groups. Each of these groups needs to be displayed in their own separate HTML <table>.
That financial data is retrieved using an Angular Service, and resolved within the Angular UI Router configuration. All of the data is also stored within that same Service.
Below is the current Router configuration:
import EventsController from './events.controller';
import EventsService from './events.service';
import EventsTableController from './events-tables/events-table.controller';
import eventsView from './events.html';
import eventsDetailsTableView from './events-tables/details-table.html';
export default function routes($stateProvider) {
$stateProvider
.state('events', {
url: '/events/{payDate:string}',
template: eventsView,
controller: EventsController,
controllerAs: 'events',
resolve: {
eventsData: ['$http', '$stateParams', ($http, $stateParams) => {
return new EventsService($http, $stateParams);
}]
},
views: {
'details#': {
template: eventsDetailsTableView,
controller: EventsTableController,
controllerAs: 'details',
resolve: {
eventData: 'eventsData.details'
}
}
}
}
);
}
routes.$inject = ['$stateProvider'];
In the future, the EventsController will be used to filter which <table>s will be displayed based on the user's preference.
The EventsTableController is a generic Controller which stores both the type of data being displayed (in this example, "details"), and the data itself, stored as a two-dimensional array.
export default class EventsTableController {
constructor(eventData) {
this.name = eventData.name;
this.data = eventData.data;
console.log(this)
}
}
Also for reference, here's an example of the data being returned from the Service:
{
'details': [[
"Event-ID",
"Company-CUSIP",
"Company-Name"]
]
}
Each <table> will correspond to a different field within that object.
I'm having trouble, however, passing the data from the 'events' state's resolve into the nested details# view. The above configuration returns an error, stating that Angular UI Router is unable to find the specific dependency: 'eventsData.details'.
My question is this: how do I pass individual object fields into the nested views, such that they can all be independently displayed? If there is any more source information that I can provide, please let me know and I'll amend this post for clarity.
By default resolves from parent are available in child states, and named views.
If you have multiple named views inside a state, and they need to use one of the state's resolves, you can inject them normally in the controller, for example:
EventsTableController.$inject = ['eventsData
function EventsTableController(eventsData) {
console.log(eventsData);
}
Child states also inherit resolved dependencies from parent state(s). For example (resA):
$stateProvider.state('parent', {
resolve:{
resA: function(){
return {'value': 'A'};
}
},
controller: function($scope, resA){
$scope.resA = resA.value;
}
})
.state('parent.child', {
resolve:{
resB: function(resA){
return {'value': resA.value + 'B'};
}
},
controller: function($scope, resA, resB){
$scope.resA2 = resA.value;
$scope.resB = resB.value;
}

Categories

Resources