Angularjs UI router submodules and URLs - angularjs

I have an app where a module looks at the URL. If the URL has anything past the "Document" section then one state is set using the information beyond that URL. If the URL has nothing beyond that point, then another state is set. So the two URLs are ...
www.xyz.com/Document/
and
www.xyz.com/Document/someData
I am currently solving the problem as below. This works, but I really need the two states to be in the same module and I can't figure out how to make that happen.
So, instead of the second state applying to app.documentEmpty, I want it to apply to app.document.empty.
angular
.module('app.document', [
'app.document.worksheet',
'app.document.tableOfContents',
'app.document.properties',
'app.document.bibliography',
'app.document.inputs',
'app.document.datasets',
'app.document.fileAsFunction',
'app.document.importedFunctions',
'app.document.directory'
])
.config(config);
/** #ngInject */
function config($stateProvider, $translatePartialLoaderProvider, msApiProvider, msNavigationServiceProvider)
{
$stateProvider.state('app.document', {
url : '/Document/{path:.*}/',
views : {
'content#app': {
templateUrl: 'app/main/apps/document/worksheet/worksheet.html',
controller : 'DocumentController as vm'
}
},
resolve : {
Documents: function (msApi)
{
return msApi.resolve('document.documents#get');
},
emptyDocuments: function (msApi)
{
return msApi.resolve('document.emptyList#get');
}
},
bodyClass: 'worksheet'
}).state('app.documentEmpty', {
url : '/Document/',
views : {
'content#app': {
templateUrl: 'app/main/apps/document/documentEmpty.html',
controller : 'DocumentController as vm'
}
},
resolve : {
Documents: function (msApi)
{
return msApi.resolve('document.documents#get');
},
emptyDocuments: function (msApi)
{
return msApi.resolve('document.emptyList#get');
}
},
bodyClass: 'document'
});
The problem is that whenever I remove the second state from above and replace it with something like what's below in the directory submodule, the URL is not recognized and the proper page is not loaded. There are several other submodules that do not depend upon URL and they work fine.
$stateProvider.state('app.document.directory', {
url : '/Document/',
views : {
'content#app': {
templateUrl: 'app/main/apps/document/directory/directory.html',
controller : 'DocumentController as vm'
}
},
bodyClass: 'directory'
});
Is it not possible to route to submodules via URLs?

You are confused with state and module.
The dot notation in ui-router indicates child states. So when you are doing this: $stateProvider.state('app.document.directory',{}), the views will only be populated IF your app.document exists and it has a ui-view directive in it. Added to that, the url parameters set in the state object will be appended AFTER the url defined in app.document state.
Say for your example above, you have defined a state like this:
$stateProvider.state('app.document', {
url : '/Document/{path:.*}/',
//... omitted for brevity
Now, this will work:
.state('app.documentEmpty', {
url : '/Document/',
//... omitted for brevity
because app.document and app.documentEmpty are sibling states.
On the other hand, this will not work:
$stateProvider.state('app.document.directory', {
url : '/Document/',
//... omitted for brevity
because app.document.directory is the child state of app.document (child states are defined by the dot notation, i.e .directory)
So in order to work, you have to rethink of your states hierarchy. In your app.document.directory module, define a state app.documentDirectory (note that there is no dot after the document):
angular.module("app.document.directory",[])
.config('$stateProvider',function($stateProvider){
$stateProvider.state('app.documentDirectory',{ //not app.document.directory, because this will make it a child state of app.document
url:'/directory'
//omitted for brevity
})
});

Related

if condition on $stateProvider in config

In my project I have displayed my navigation menus using :
function config($stateProvider, msNavigationServiceProvider){
$stateProvider.state('app.contacts', {
url : '/contacts',
views : {
'content#app': {
templateUrl: 'app/main/apps/contacts/contacts.html',
controller : 'ContactsController as vm'
}
},
resolve: {
}
});
//For Navigation
msNavigationServiceProvider.saveItem('apps.contacts', {
title : 'Contacts',
icon : 'icon-account-box',
state : 'app.contacts',
weight: 10
});
}
Now I want to show navigation on a condition that when super admin has assigned the sub user its permission. So what I should do in that condition?
Until now I tried adding this $stateProvider.state and msNavigationServiceProvider.saveItem in if condition, where in if contained a response from a service (a service which defines which items should appear in navigation), but this isn't working, calling of service is not allowed in config.
So what should I do for this?

Angular UI Router child resolves

I am trying to create a parent-child relationship in my UI router config with using different resolves and it doesn't seem to be working. I want to have a common parent state with child states that control whether the state is in edit mode or new mode. Depending on the mode, the resolve of the state is different.
What I have is essentially this:
.state('main.details', {
url: "/details",
templateUrl: "modules/details.html",
abstract: true
})
.state('main.details.new', {
controller: "DetailsCtrl as detailsCtrl",
resolve : {
detail: ['$stateParams', 'NewService', function ($stateParams, NewService) {
return NewService.getDetail($stateParams.detailId, true);
}]
}
})
.state('main.details.edit', {
controller: "DetailsCtrl as detailsCtrl",
resolve : {
detail: ['$stateParams', 'EditService', function ($stateParams, EditService) {
return EditService.getDetail($stateParams.detailId, true);
}]
}
})
Doing it this way, I get this:
Error: [$injector:unpr] Unknown provider: detailProvider <- detail <- DetailsCtrl
Is there a way to have a parent state that just defines the URL and templateUrl and then have child states with varying resolves?
One option to help with this would be to pull out the repeating resolve on the routes and put it into a factory that the controller can call on

How to call onEnter when changing from Substate to Parentstate?

I wonder why the onEnter function is not called, when changing from substate to parentstate? Is there any other function that gets called? I tried this:
$stateProvider.state('stateA', {
url: '/stateA',
views: {
'content': {
templateUrl: 'views/Aview.html',
controller: 'ControllerA'
},
'dialog': {
template: "<div ui-view='dialog'></div>"
}
},
onEnter: function ($state, olSelectInteractionService){
// IS NOT getting called
},
onExit: function (olSelectInteractionService){
}
});
$stateProvider.state('stateA.sub', {
url: '/sub/:id',
views: {
'dialog': {
templateUrl: 'views/Bview.html',
controller: 'ControllerB', // This Controller does something like $state.go("stateA");
resolve: {
ad: function (AdLoader, $stateParams) {
return AdLoader($stateParams.id);
}
}
}
}
});
Well, the answer is:
when navigating from a child to its parent - onEnter cannot be triggered - because we already are in the parent state. When child state is init, its parent must be as well
But having in place comment in your code:
...
controller: 'ControllerB', // This Controller does something like $state.go("stateA");
resolve: {
...
Especially this:
This Controller does something like $state.go("stateA");
We can adjust that call:
$scope.goToParent = function(){$state.go('parent', null, {reload : true});}
There is a working plunker. It shows that when we are in child state, this won't re-trigger onExit:
<a ui-sref="parent"> // we already are in parent state
But this will:
<button ng-click="goToParent()">
See: go(to, params, options) (mostly reload)
... options (optional) object
Options object. The options are:
location - {boolean=true|string=} - If true will update the url in the location bar, if false will not. If string, must be "replace", which will update url and also replace last history record.
inherit - {boolean=true}, If true will inherit url parameters from current url.
relative - {object=$state.$current}, When transitioning with relative path (e.g '^'), defines which state to be relative from.
notify - {boolean=true}, If true will broadcast $stateChangeStart and $stateChangeSuccess events.
reload (v0.2.5) - {boolean=false}, If true will force transition even if the state or params have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd use this when you want to force a reload when everything is the same, including search params.
Check it here

Controlling order of operations with services and controllers

I have two services - one to store user details and the other to make a call to retrieve those details:
userService stores user details to be used across the entire app (i.e. injected in controllers, services, etc.)
function userService($log) {
var id = '';
var username = '';
var isAuthenticated = false;
var service = {
id: id,
username: username,
isAuthenticated: isAuthenticated
};
return service;
}
authService is used (hopefully just once) to retrieve the user details from a Web API controller:
function authService($log, $http, userService) {
$log.info(serviceId + ': Inside authService method');
var service = {
getUserDetails: getUserDetails
};
return service;
function getUserDetails() {
$log.info(serviceId + ': Inside getUserDetails method');
return $http.get('api/authentication', { cache: true });
}
}
Initially, I had the call to the authService fire in a .run block like so:
.run(['$log', 'authService', 'userService', function ($log, authService, userService) {
authService.getUserDetails()
.then(querySucceeded);
function querySucceeded(result) {
userService.id = result.data.Id;
userService.username = result.data.username;
}
}]);
But the problem was that the getUserDetails-returned promise did not resolve until after I my controllers fired and, thus, too late for me. The user data was not ready.
I then looked at the resolve option in the $stateProvider (for UI-Router):
.state('dashboard', {
url: '/dashboard',
views: {
header: {
templateUrl: 'app/partials/dashboard/header.template.html',
controller: 'DashboardHeaderController',
controllerAs: 'dashboardHeaderVM',
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
}
}
})
The assumption is that the view won't be rendered until the promise in the resolve section is, well, resolved. That seems to work fine.
Here's the (relevant part of the) controller where I use the returned user property:
function DashboardHeaderController($log, user) {
var vm = this;
// Bindable members
vm.firstName = user.data.firstName;
}
However, I have two routes (more to come) and a user can navigate to either one. Do I need to have a resolve property in each state section for the authService? I want to fire the call to authService.getUserDetails just once no matter which route is served and have it available after that for any route, controller, etc.
Is there a better (best practice) way to do this?
Not sure about better or best practice, but here is a plunker with my way.
The point is to move resolve into some parent root state. The one who is ancestor of all states in the application:
$stateProvider
.state('root', {
abstract : true,
// see controller def below
controller : 'RootCtrl',
// this is template, discussed below - very important
template: '<div ui-view></div>',
// resolve used only once, but for available for all child states
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
})
This is a root state with resolve. The only state with resolve. Here is an example of its first child (any other would be defined similar way:
$stateProvider
.state('index', {
url: '/',
parent : 'root',
...
This approach will work out of the box. I just would like to mention that if the 'RootCtrl' is defined like this:
.controller('RootCtrl', function($scope,user){
$scope.user = user;
})
we should understand the UI-Router inheritance. See:
Scope Inheritance by View Hierarchy Only
small cite:
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states...
More explanation could be found in this Q & A
So, what does it mean?
Our root view can pass the resolved stuff into child state only - if their views are nested.
For example, the $scope.user will be inherited in child states/views/$scopes only if they are nested like this
.state('index', {
url: '/',
parent : 'root',
views: {
'' : { // the root view and its scope is now the ancestor
// so $scope.user is available in every child view
templateUrl: 'layout.html',
controller: 'IndexCtrl'
},
'top#index' : { templateUrl: 'tpl.top.html',},
'left#index' : { templateUrl: 'tpl.left.html',},
'main#index' : { templateUrl: 'tpl.main.html',},
},
Check it here
If I correctly understand you want that on page load you would have user info before any controller or service request it.
I had similar task in my current project.
To solve the problem I manually requested current user info before app bootstapping & store it in localStorage.
Then after app bootstrapping all controllers/services have accesss to current user info.
TIP: to get user info before app bootstrap you can still use $http service by manually injecting it:
angular.injector(['ng']).get('$http');

How to manage state with the ui-router with multiple modules

I have an app page with 3 columns. The middle column is the main activity and is always displayed. The two side columns are widget lists, that have their own controller and states, and can be either hidden or unhidden, and have multiple views within them as well. Ideally, I'd imagine url routes like the following:
/app - main activity is shown, both panels hidden
/app/1234 - main activity is shown, but shows info for 1234 entity
/app/1234/leftpanel - main activity is shown with 1234 entity, and leftpanel is open
/app/1234/leftpanel/list - main activity is shown with 1234 entity, and leftpanel is showing the list view
/app/leftpanel/list - main activity is showing default state, leftpanel is still showing the list
Is this possible to setup with ui-router? I've seen this example:
https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions
which shows how to use the $stateProvider between multiple modules, but I'm still not seeing how to make this scenario work-
I did resolve the issue. I also posted about this on the angular-ui github, and their response was basically, "well, that case isn't really what the router has been designed for, so if you want "fancier" state management, put the data in parameters and look at them yourself and implement whatever logic you need". I kinda felt that this was what the ui-router was designed for, so I extended it a bit (no source code changes) to accomplish this. The solution is a combination of abstract states, a fake "off" state, parameters in the router urls, and extending the $urlRouterProvider service.
First extend the $urlRouterProvider:
urlRouterProvider.when(/^((?!leftpanel).)*$/, ['$state', '$location', function ($state, $location) {
//we've got just /app or /app/3434, nothing that contains /leftpanel, so turn off
$state.transitionTo("off");
}]);
Then add that "off" state:
$stateProvider.state('off',{
//url: //there is no url
views:{
container:{
template: 'blank',
controller:['$scope', '$stateParams', function($scope, $stateParams){
console.log("off yay"); //just for sanity, not necessary
}]
}
}});
Then setup the rest of the app routing:
appModule.constant('LEFT_PANEL_STATES', function() {
var leftPanelRoot = {
name: 'root.leftpanel', //mandatory
template: '',
url: "/app/:primaryId/leftpanel",
views:{
'container#': {
templateUrl: "partials/leftpanel_container_partial.html",
controller:"LeftPanelRootCtrl",
resolve: {
//etc
}
}
},
"abstract":true //makes this view only viewable from one of its child states
};
var leftPanelItemsList = {
name: 'root.leftpanel.itemslist', //mandatory
parent: leftPanelRoot, //mandatory
url: "/items-list",
views:{
'childview#root.leftpanel': {
templateUrl: "partials/leftpanel_items_list.html",
controller:"LeftPanelItemsListCtrl",
resolve: {
//etc
}
}
}};
var leftPanelListDetail = {
name:"root.leftpanel.itemslist.detail",
parent:leftPanelItemsList,
url:"/:id/detail",
views:{
"detail":{
templateUrl:"partials/leftpanel_item_detail.html",
controller:"LeftPanelItemListDetailCtrl"
}
}};
var leftPanelExtendedDetailList = {
name:"root.leftpanel.itemslist.extendedlist",
parent:leftPanelItemsList,
url:"/:id/extendedDetail/list",
views:{
"extendeddetaillist":{
templateUrl:"partials/leftpanel_extended_detail_list.html",
controller:"LeftPanelExtendedDetailListCtrl"
}
}};

Resources