AngularJS, ui-router, nested states - angularjs

I'm building the outline of an app using ui-router. I've defined the states and it seems to work, but I can't help but feel there might be a better way in terms of best practice.
My states:
'main' is an abstract state with a header, footer, and a middle content area.
'main.home' is what comes up by default. The content file 'home.tpl.html' is a sort of splash page with srefs to other areas of the app, like 'main.wizard.step1'.
'main.wizard' is an abstract state representing a multi step info gathering wizard.
'main.wizard.step1', 'main.wizard.step2' are steps in the wizard
What I'm doubting is my use of the 'views' object having values of "#" and "". Does this look reasonable, or would you do something else?
$urlRouterProvider.otherwise('/');
var app = {
name: 'main',
abstract: true,
url: '',
views: {
'header': {
templateUrl: 'header.tpl.html'
},
'footer': {
templateUrl: 'footer.tpl.html'
}
}
};
var home = {
name: 'main.home',
url: '/',
views: {
"#": {
templateUrl: 'home/home.tpl.html'
}
}
};
var wizard = {
name: 'main.wizard',
abstract: true,
url: '/wizard',
views: {
"#": {
templateUrl: 'wizard/wizard.tpl.html'
}
}
};
var step1 = {
name: 'main.wizard.step1',
url: '/step1',
views: {
"": {
templateUrl: 'wizard/step1.tpl.html'
}
}
};
/** repeat similarly for step2, step3 & step 4 **/
$stateProvider.state(app);
$stateProvider.state(home);
$stateProvider.state(wizard).state(step1).state(step2).state(step3).state(step4);

What '#' will mean in the definition of the template of the view will be injected into the ui-view of the root state which is usually your index.html.
If you want the wizard to go into your middle area of your main page you should add something like this to your index.html:
<ui-view name='content'>Optional placeholder text</ui-view>
And in the definition of the view you should do something like
var wizard = {
name: 'main.wizard',
abstract: true,
url: '/wizard',
views: {
"#content": {
templateUrl: 'wizard/wizard.tpl.html'
}
}
};
You can actually drop that # in #content because main is the parent of wizard and you don't have to resolve it in an absolute manner.
In any case if you want to have steps in your wizard(of course) don't put any views in your wizard virtual state. Have your steps sub states have their own view and target wizard#. I don't see that you need a separate view wrapper for your wizard (Maybe if you want to do "step x of y" or a progress bar).
Hope it helps.

You are going good. But you can improve. Here is the code which shows how I had done it,I have taken this approach from a sample app in UI-Router Guide.
var states = [
{ name: 'hello', url: '/loginnn', component: 'loginComponent' },
{ name: 'home', url: '/hommm', component: 'homeComponent' },
{
name: 'home.dumm1',
url: '',
component: 'dummyComponent',
},
{
name: 'home.dumm2',
url: '/dumm1',
component: 'dummyComponent1',
},
{
name : 'home.dashboard',
url: '/dashboard',
component: 'dashboardComponent',
}
]
// Loop over the state definitions and register them
states.forEach(function(state) {
$stateProvider.state(state);
});
});

Related

implement back button functionality using $state

My application has multiple states.
The workflow is such that from the landing page I go into Alert view page that has list of alerts in a tabular format. From that list I select an alert which takes the me into the Alert Detail page, that gives detailed information of that particular alert. Within that alert Detail state, there are subviews in the form of navigation tabs that displays various aspect of that alert when clicked on the particular tab.
AlertView.config.js:
$stateProvider.state({
name: "alert-view",
parent: "root",
url: "/alert/view?id&name&criteria&start&end&targets&sort",
data: {
pageTitle: "Alert View"
},
views: {
"main.content#": {
templateUrl: "components/alerts/alert-view/alert-view.html",
controller: "AlertViewsController",
controllerAs: "vm"
}
}
Alert Detail Page.config.js
$stateProvider.state({
name: "alert-details",
parent: "root",
abstract: true,
url: "/alert/details/:id",
params: {
// this will make both links /alert/details & /alert/details/ work
id: {squash: true, value: null}
},
data: {
pageTitle: "Alert Details"
},
views: {
"main.content#": {
templateUrl: "components/alerts/alert-details/alert-details.html",
controller: "AlertDetailsController as vm"
},
"alertViewDetail#alert-details": {
templateUrl: "components/alerts/alert-details/overview/overview.html",
controller: "AlertDetailsOverviewController as vm"
//#todo parent/child controller.
}
},
}).state({
name: "alert-details.overview",
parent: "alert-details",
url: "/overview",
params: {
// this will make both links /alert/details & /alert/details/ work
id: {squash: true, value: null}
},
data: {
pageTitle: "Alert Details"
},
views: {
"alertViewDetail#alert-details": {
templateUrl: "components/alerts/alert-details/overview/overview.html",
controller: "AlertDetailsOverviewController as vm",
controllerAs: "vm"
}
}
}).state({
name: "alert-details.history",
parent: "alert-details",
url: "/history",
params: {
// this will make both links /alert/details & /alert/details/ work
id: {squash: true, value: null}
},
data: {
pageTitle: "Alert Details"
},
views: {
"alertViewDetail#alert-details": {
templateUrl: "components/alerts/alert-details/history/history.html",
controller: "AlertDetailsHistoryController",
controllerAs: "vm"
}
}
}).state({
name: "alert-details.trigger",
parent: "alert-details",
url: "/trigger-events",
params: {
// this will make both links /alert/details & /alert/details/ work
id: {squash: true, value: null}
},
data: {
pageTitle: "Trigger Events"
},
views: {
"alertViewDetail#alert-details": {
templateUrl: "components/alerts/alert-details/trigger-events/trigger-events.html",
controller: "AlertDetailsTriggerEventsController",
controllerAs: "vm"
}
}
}).state({
name: "alert-details.all-fields",
parent: "alert-details",
url: "/all-fields",
params: {
// this will make both links /alert/details & /alert/details/ work
id: {squash: true, value: null}
},
data: {
pageTitle: "All Alert Fields"
},
views: {
"alertViewDetail#alert-details": {
templateUrl: "components/alerts/alert-details/all-fields/all-fields.html",
controller: "AlertDetailsAllAlertFieldsController",
controllerAs: "vm"
}
}
});
alertDetail.html
<button ng-click="vm.goBack()" class="btn btn-default"><i class="icon icon-long-arrow-left"></i><span translate>Back</span></button>
alertDetail.controller.js
vm.goBack = function () {
history.back();
};
My aim is to implement the back button in the alert detail page such that , no matter in whichever subview I am, if clicked on the back button, it directs me to Alert View Page. I tried using history.back() to do so. But it traverses through all the previous state rather than routing to Alert View page directly.
How should I utilise the state information of the Alert View page and Alert Detail page, in order to implement the required scenario. Please help.
You can store all states in a rootscope array.
$rootScope.statesInfo = ['alert-details.overview','alert-details.history',....];
When user press back button then check the current state and go on the previous state.
You can get current state using this: $state.current.name
Simple you will compare and go back in previous state;
angular.forEach($rootScope.statesInfo,function (val,key) {
if ($state.current.name == val) {
$state.go($rootScope.statesInfo[key-1]);
}
});
To solve the problem of saving the current URL and use it later, I created a service that would take in the the query string of the URL as a single object. Then when i need to go to that URL in that same state, I would use that query string object and pass it as a parameter on $state.
so vm.goBack function would look like this:
vm.goBack = function () {
$state.go("alert-view", {name: alertHistory.getHistory().name, criteria: alertHistory.getHistory().criteria, sort: alertHistory.getHistory().sort});
};
the service that i created was:
(function () {
"use strict";
angular
.module("app.alerts")
.service("alertHistory", alertHistory);
alertHistory.$inject = [
"$location",
"logger"];
function alertHistory ($location, logger) {
var historyURL = {
init: null,
saveURL: saveURL,
getHistory: getHistory
};
return historyURL;
function saveURL (urlObj) {
historyURL.currentURL = urlObj;
}
function getHistory () {
return historyURL.currentURL;
}
}
})();
in controller I obtain the query string using $location.search() :
alertHistory.saveURL($location.search());
This might not be the ideal solution. But it works for what i need :) Any suggestions are welcome!

Angular Js - ui-router with Breadcrumbs

I have multi pages with One View .I want use Single View for All pages, with Logical structure Angular, In addition Breadcrumbs for navigation, in head of Home page.
config code:
$stateProvider
.state('otherwise', {
url: '*path',
controller: function ($state) {
var lastUrl = sessionStorage.lastUrl
if (lastUrl && lastUrl != 'otherwise')
$state.go(lastUrl);
else if (!lastUrl)
$state.go('Companies');
},
data: {
requireLogin: false
}
})
.state('Create', {
controller: 'myControl',
url: '/',
templateUrl: 'Create.html',
data: {
requireLogin: true
}
})
.state('Jobs', {
controller: 'myControl',
url: '/',
templateUrl: 'JobsList.html',
data: {
requireLogin: true
}
})
.state('Companies', {
controller: 'myControl',
url: '',
templateUrl: 'CompaniesList.html',
data: {
requireLogin: false,
breadcrumbProxy: 'Companies.CarsList'
}
})
.state('Companies.CarsList', {
controller: 'myControl',
params: { id: ':id', companyName: ':companyName' },
url: '',
templateUrl: 'CarsList.html',
data: {
requireLogin: false,
displayName: 'List'
}
})
.state('Companies.CarsInfo', {
controller: 'myControl',
templateUrl: "CarInfo.html",
data: {
requireLogin: false,
displayName: 'Info'
}
})
html:using single VIEW in home page
<div ui-view></div>
You have a Solution for my Config?!
Single view
To handle multiple views, the ui-router provides rules to target views when you have multiple <div ui-view></div> in your templates.
By default, a state takes place in the unique <div ui-view></div> in parent state. So, given your configuration, the Companies.CarsList template will be inserted in the <div ui-view></div> of the Companies state (in CompaniesList.html).
To override that, just wrap templateUrl and controller of your second-level states (Companies.CarsList and Companies.CarsInfo) in a views object the ui-router to place the view in the unique <div ui-view></div> of the root state (in index.html), like this:
.state('Companies.CarsList', {
params: { id: ':id', companyName: ':companyName' },
url: '',
views: {
"#": { // rule for absolutely targetting the unnamed view in root unnamed state.
controller: 'myControl',
templateUrl: 'CarsList.html',
}
},
data: {
requireLogin: false,
displayName: 'List'
}
})
Breadcrumb
Have a look on angular-breadcrumb, it generates a breadcrumb based on the ui-router configuration. All you have to do is to give a name to the states (like you seems to do already with data > displayName).
The module can handle the absolute view targeting I described above. See the docs for details

angularjs workout required parameters for any state

I an trying to list out all the URL's that exist in the stateprovider (angularjs 1.2.26).
given the example below, (very much cut down state list):
angular.module('app')
.config(function ($stateProvider) {
$stateProvider
.state('app.vendors', {
url: '/vendors',
templateUrl: 'app/vendor/list.html',
controller: 'Vendor.ListController as vm',
})
.state('app.vendor', {
url: '/vendor/{vendorId}',
templateUrl: 'app/vendor/details.html',
controller: 'Vendor.DetailsController as vm',
data: {
subnav: [
{ title: 'Details', icon: 'fa-align-left', state: 'app.vendor', permissions: 'get-vendor', exactStateOnly: true },
{ title: 'Sites', icon: 'fa-archive', state: 'app.vendor.sites', permissions: 'get-site' },
{ title: 'NCRs', icon: 'fa-copy', state: 'app.vendor.ncrs', permissions: 'get-vendor' }
],
requiredPermissions: ['get-vendor']
}
})
.state('app.vendor.sites', {
url: '/sites',
templateUrl: 'app/vendor/site/list.html',
controller: 'Vendor.Site.ListController as vm',
data: {
requiredPermissions: ['get-site']
}
})
.state('app.vendor.site', {
url: '/site/{siteId}',
templateUrl: 'app/vendor/site/details.html',
controller: 'Vendor.Site.DetailsController as vm',
data: {
requiredPermissions: ['get-site']
}
})
.state('app.vendor.ncrs', {
url: '/ncrs',
templateUrl: 'app/vendor/ncr/ncrList.html',
controller: 'Vendor.NCR.NCRListController as vm',
data: {
requiredPermissions: ['get-vendor']
}
});
});
to get to a particular vendor you would use state:
app.vendor({vendorId: 1})
to get to its site
app.vendor.site({vendorId: 1, siteId: 2})
if I pass in the $state object to a controller I can list all the states with state.get().
If I list them the urls only contain the last part (i.e. what is in the config, and relative to its parent). I can use $state.href('app.vendor.site') which will give me almost the whole url, but misses out the parameters. I am trying to find a way at runtime to know what or at least how many parameters it requires.
My goal is to try and create a basic smoke test for every page in our Angular app to ensure it loads something and doesn't through errors in the console. I dont want to have to manually maintain a list of urls with params. (all our params are int IDs so I can simply use "1" in the params to test the url).
The private portion of the state contains params and ownParams objects. You can use a decorator to access those internal variables. See my previous answer regarding exposing the entire internal state object using a decorator: UI-Router $state.$current wrapper for arbitary state
After decorating your state objects, use the $$state() function to retrieve the private portion. Then query the state for its params and generate the href.
angular.forEach($state.get(), function(state) {
var paramKeys = state.$$state().params.$$keys();
var fakeStateParams = {};
angular.forEach(paramKeys, function(key) { fakeStateParams[key] = key; });
console.log($state.href(state, fakeStateParams));
});

AngularJS. Default child state with named views

I am using UI Router for my application. Their FAQ page covers default child state question, but they are not using named views and i can't figure out how to get this working.
Here are samples of my code:
index.html
<a ui-sref="/">Home</a>
<a ui-sref="topic.basics">Basics</a>
<a ui-sref="topic.payments">Payments</a>
<div ui-view="container" class="container"></div>
app.js
$stateProvider
.state("/", {
url: "/"
})
.state("topic", {
url: "/topic/",
abstract: true,
// ?
})
.state("topic.basics", {
url: "basics/",
views: {
"container": {
templateUrl: "views/basics.html"
}
}
})
.state("topic.payments", {
url: "payments/",
views: {
"container": {
templateUrl: "views/payments.html"
}
}
});
$urlRouterProvider
.when("/topic/", "/topic/basics/")
.otherwise("/");
You are almost there, but because the view target is not a parent, but index.html, we have to use aboslute naming
.state("topic.basics", {
url: "basics/",
views: {
// instead of this, which targets the parent
// "container": {
// we need this, where string empty after # means root/index.html
"container#": {
templateUrl: "views/basics.html"
}
}
})
.state("topic.payments", {
url: "payments/",
views: {
// "container": {
"container#": {
templateUrl: "views/payments.html"
}
See
View Names - Relative vs. Absolute Names
Behind the scenes, every view gets assigned an absolute name that follows a scheme of viewname#statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. You can also choose to write your view names in the absolute syntax.
For example, the previous example could also be written as:
.state('report',{
views: {
'filters#': { },
'tabledata#': { },
'graph#': { }
}
})
Make the base home state abstract and remove the / in front of the /topic you already use this in the base route.
$stateProvider
.state("home", {
name: 'home',
abstract: true,
url: "/"
})
.state("topic", {
name: 'home.topic',
url: "topic/"
})
.state("topic.basics", {
url: "basics/",
name: 'home.topic.basics'
views: {
"container": {
templateUrl: "views/basics.html"
}
}
})
.state("topic.payments", {
url: "payments/",
name: 'home.topic.payments',
views: {
"container": {
templateUrl: "views/payments.html"
}
}
});
$urlRouterProvider.otherwise("/");

Angular UI Router: How to stop reloading based on query parameters change?

Here is my ui-router configuration:
$stateProvider
.state('search.filter.results', {
url: '/results?profileId&keywords',
views: {
'profiles#': {
templateUrl: 'profile-list/profile-list.html',
controller: 'ProfileListCtrl'
},
'profile-summary#search.filter.results': {
templateUrl: 'profile-list/profile-summary/profile-summary.html'
},
'profile-details#search.filter.results': {
templateUrl: 'profile-list/profile-details/profile-details.html',
controller: 'ProfileDetailsCtrl'
}
}
})
ProfileListCtrl has the following method:
$scope.showProfileDetails = function(profileId) {
$state.go('search.filter.results', { profileId: profileId });
};
The problem is, when this method is executed, ProfileListCtrl is instantiated and the view is reloaded.
I would like the reload to happen only if the keywords query parameter changes, but not when the profileId parameter changes.
What would be the best way to achieve this?
I would suggest: split this state into 2 states.
The "parent" keeping the profileId,
the "child" working with keywords.
Until the profileId is not changed, the parent controller won't be re-instantiated.
I created this example with this simplified states definition:
.state('profile', {
url: '/results/:profileId',
views: {
'profiles#': {
//templateUrl: 'profile-list/profile-list.html',
template: '<div>'
...
'<div ui-view=""></div></div>',
controller: 'ProfileCtrl'
},
}
})
.state('profile.results', {
url: '/?keywords',
views: {
'': {
//templateUrl: 'profile-list/profile-list.html',
template: ...
controller: 'ProfileListCtrl'
},
}
})
NOTE: state defintion here is simplified, to just show the split. Not all the details. Also, the child state target parents unnamed view ui-view="", because these should be nested. Finally, parent could even be abstract
This approach also will require some small adjustments of the url. It is not working well if the child url defintion would start with ? like: url: '?keywords'. So I used different pattern:
/results/profileId-value/?kewords=abc

Resources