AngularJS and UI-Router simplify states - angularjs

I am fairly new to Angular (only been using it a couple of months) and although I have learnt so much since I read my first book, there are a few concepts or best practices that elude me.
This might be one of them. I have a project at the moment which has a set of states, basically it has
collections - which it lists, in this case I have made it a stand alone state, but it requires a centerId parameter (because collections belong to a center).
Create collection - Because this requires the same centerId, this "could" be a child state of collections, but I have found that in doing so, whenever a collection is created I have to update the parent's collections. So I have not made this a child of the collections state, instead it has it's own centerId parameter which I pass.
Edit collection - Again this is the same as Create collection, this state uses the same controller and indeed the same template as create collection.
Deliver - Deliver collection is a state that allows a user to update a collection with a set of specific information (POD, signature, etc) so this has it's own state and template
Collect - Collect is the same as deliver except instead of a POD, it has a POC (Proof of collection), so this uses the same controller and template as deliver
Import - Import is used to import a list of collections via csv.
Now, all these states have one thing in common, they all require the centerId. Like I have previously said, I could make them all children of the collections state, but there is a complication when updating the collections list on the parent state. It is grouped, so the resolved collection actually goes through some code before being presented to the view.
Here is my current state layout:
// Now set up the states
$stateProvider.state('collections', {
url: '/collections/:centerId',
templateUrl: '/assets/tpl/collections/index.html',
controller: 'CollectionsController',
controllerAs: 'controller',
resolve: {
// Resolve our collections before the state loads
collections: ['$stateParams', 'CollectionsService', function ($stateParams, service) {
// Get our center id
var centerId = $stateParams.centerId;
// Return our collections
return service.get(centerId);
}]
},
data: {
requireLogin: true,
pageTitle: 'Collections'
}
}).state('savingCollections', {
url: '',
abstract: true,
resolve: {
// Resolve our statuses
statuses: ['Api', function (api) {
// Return our statuses
return api.get('/api/statuses');
}]
}
}).state('savingCollections.import', {
url: '/collections/:centerId/import',
views: {
'#': {
templateUrl: '/assets/tpl/collections/import.html',
controller: 'ImportCollectionsController',
controllerAs: 'controller',
}
},
data: {
requireLogin: true,
pageTitle: 'Import your collections'
}
}).state('savingCollections.create', {
url: '/collections/:centerId/create',
views: {
'#': {
templateUrl: '/assets/tpl/collections/save.html',
controller: 'SaveCollectionController',
controllerAs: 'controller'
}
},
resolve: {
// Creates a blank collection so that the injection works
collection: function () {
// Return an empty object
return {};
}
},
data: {
requireLogin: true,
pageTitle: 'Create your collection'
}
}).state('savingCollections.edit', {
url: '/collections/:centerId/edit/:id',
views: {
'#': {
templateUrl: '/assets/tpl/collections/save.html',
controller: 'SaveCollectionController',
controllerAs: 'controller'
}
},
resolve: {
// Gets a collection from the API
collection: ['$stateParams', '$q', 'CollectionsService', function ($stateParams, $q, service) {
// Get our ids
var id = $stateParams.id,
centerId = $stateParams.centerId;
// return our collection
return service.get(centerId, id);
}]
},
data: {
requireLogin: true,
pageTitle: 'Update your collection'
}
}).state('savingCollections.receiveCollections', {
url: '',
abstract: true,
params: {
selected: null
},
resolve: {
selected: ['$stateParams', function ($stateParams) {
// Get our selected items
var selected = $stateParams.selected;
// If we have something in our parameters
if (selected) {
// Save them into our session
sessionStorage.selected = angular.toJson(selected);
}
// Return our selected items
return angular.fromJson(sessionStorage.selected);
}]
},
}).state('savingCollections.receiveCollections.collect', {
url: '/collections/:centerId/collect',
views: {
'#': {
templateUrl: '/assets/tpl/collections/receive.html',
controller: 'CollectCollectionsController',
controllerAs: 'controller'
}
},
params: {
collecting: true
},
data: {
requireLogin: true,
pageTitle: 'Collect'
}
}).state('savingCollections.receiveCollections.deliver', {
url: '/collections/:centerId/deliver',
controller: 'CollectCollectionsController',
controllerAs: 'controller',
views: {
'#': {
templateUrl: '/assets/tpl/collections/receive.html',
controller: 'CollectCollectionsController',
controllerAs: 'controller'
}
},
data: {
requireLogin: true,
pageTitle: 'Deliver'
}
});
I hope this is enough information.
If you are unsure what I am trying to do, basically I want to make my states easy to read and understand. I would like it so that if someone looks at the states, they will know exactly what is going on.
Also, I would like to refrain from ugly ui-srefs such as this:
ui-sref="savingCollections.receiveCollections.deliver({ centerId: controller.centerId, selected: [collection]})"
any help would be greatly appreciated.

Your state declarations aren't bad. I find them fairly readable. One bit of advice I have to make things more readable is to to move the resolve functions out of the state declarations to another place and simply reference them by name. You can put them below your state code or you can place them in a different service entirely.
I put mine in a different service, like so:
var resolveFunctions = {
resolveData: function($q, $stateParams, dataService) {
//getData returns a promise
return dataService.getData($stateParams.id);
}
}
angular.module("myModule").constant("resolveFunctions", resolveFunctions);
This resolve function retrieves some data based on an ID in the state parameters.
Then I can use the resolveFunctions service in the place I declare my states:
//Inject resolveFunctions service as a dependency wherever you are defining your states
resolve: {
data: resolveFunctions.resolveData
}
I hope that makes sense.
As for switching between states, I use state parameters to pass data between states. The resolve functions for the new state can then read from the state parameters like in the resolveFunctions example above.
You can use $state.go() in your controller to switch between states with state parameters (you can add an ng-click handler with a $state.go call).
The following code goes to the "savingCollections" state and passes a center ID as a state parameter. Your can either extract the centerId in the controller or in one of the resolve functions using $stateParams.centerId
$state.go("savingCollections", { centerId: centerId });
I haven't done so personally, but I think you can also do that in ui-sref as well.
ui-sref="savingCollections({ centerId: controller.centerId })"
If that ui-sref works, that would be easier than calling $state.go()

Related

How to use multiple URLs for one state without duplicating its children states?

After searching about this problem, I have found multiple questions that have the same concern. However, none of them gave me an appropriate answer for what I am trying to achieve.
In my application using AngularJS 1.5.8, I have a view displaying a document that can belong to a User or a Project. Either way, the view and the controller are the same. I want the state of this view to be accessible via multiple URLs. I want this to be possible because, the way I built my application, every state that has a URL starting with /projects/{idProject}/ will display a custom navigation bar with shortcuts. But when the user is accessing his own document, he is not in a project section, so the navbar shouldn't display the specific buttons.
I had encountered the same issue earlier with another page, but I simply created two states so that two different URLs could lead to it with different parameters.
However, I don't find this solution suitable for this page for one reason : the state has children states that correspond to modals allowing for different CRUD operations. This means that I would need to duplicate the children states as well and I would end up with dix states instead of three, all identical except for the URL and the parent states.
Here's an edited sample of my code to help visualize :
.state('view-document', {
parent: 'app',
url: '/view/document/{idDocument}', // <-- This should also be accessed via /projects/{idProject}/view/document/{idDocument}
views: {
'content#': {
templateUrl: 'app/core/view/document/view-document.html',
controller: 'ViewDocumentController',
controllerAs: 'vm'
}
}
})
.state('view-document.edit-field', {
parent: 'view-document',
url: '/edit-field',
params: {
field: null
},
onEnter: ['$stateParams', '$state', '$mdDialog', function ($stateParams, $state, $mdDialog) {
$mdDialog.show({
templateUrl: 'app/core/view/document/edit-field/view-document-edit-field-dialog.html',
controller: 'ViewDocumentEditFieldController',
controllerAs: 'vm',
resolve: {
...
}
}).then(function () {
$state.go('view-document', null, {reload: false});
}, function () {
$state.go('^');
});
}]
})
.state('view-document.delete-field', {
parent: 'view-document',
url: '/field/{idField}/delete',
params: {
field: null
},
onEnter: ['$stateParams', '$state', '$mdDialog', function ($stateParams, $state, $mdDialog) {
$mdDialog.show({
templateUrl: 'app/core/view/document/delete-field/view-document-delete-field-dialog.html',
controller: 'ViewDocumentDeleteFieldController',
controllerAs: 'vm'
resolve: {
...
}
}).then(function () {
$state.go('view-document', null, {reload: false});
}, function () {
$state.go('^');
});
}]
});
Any idea on how I could achieve this without duplicating what could be even more than two children states ? This question could also be : How can two different states share the same children states ?

Access scope variable from the other controller within the nested state

How to access other state scope variable from another state ctrl?
If in 'session.index.detail' state, I want to access leftPanel scope variable from rightPanel Ctrl is it possible? What I want to do is I have a form in rightPanel, when I click save, I want to add the new data object back to leftPanel list and refresh it after it is successfully added to the database.
.state('session', {
abstract: true,
templateUrl: '/schedule/_session.html'
})
.state('session.index', {
url: '/sessionmang',
views: {
'leftPanel#session': {
controller: 'SessionCtrl',
controllerAs: 'vm',
templateUrl: '/schedule/_sessionPanel.html'
},
'rightPanel#session': {
templateUrl: '/schedule/_sessionDetailPanel.html'
}
}
})
.state('session.index.detail', {
params: {
sessionObj: null,
sessionTypeObj: null
},
views: {
'rightPanel#session': {
controller: 'SessionDetailCtrl',
controllerAs: 'vm',
templateUrl: '/schedule/_sessionDetailPanel.html',
resolve: {
sessionObj: ['$stateParams', function ($stateParams) {
return $stateParams.sessionObj;
}],
sessionTypeObj: ['$stateParams', function ($stateParams) {
return $stateParams.sessionTypeObj;
}]
}
}
}
});
I think the best way is to create a service that holds the data you want to share.
And share it between the controllers.
Or use broadcasting to talk between the controllers and pass data over that way
the simplest way is, you could use events like this.
when you click save, you will do
$scope.$broadcast('myevent', data);
from the rightCtrl and from the leftCtrl you could do
$scope.$on('myevent', function(event,data) {
//do your updates here
})

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));
});

Angular UI router default parameters wind up with URL http://example.com/#/deliverables////. A way to avoid consecutive slashes?

Here are a couple states from my app:
angular.module("app").config([
'$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
$httpProvider.defaults.withCredentials = true;
//#region Routing
$urlRouterProvider.otherwise("/login");
$stateProvider.state('client1', {
onEnter: function () { },
views: {
"index": {
templateUrl: 'indexView.html'
}
}
}).state('client1.deliverables', {
url: '/deliverables/:taxYear/:entity/:deliverable/:status',
onEnter: function () { },
views: {
"nav": {
templateUrl: 'Nav/nav.html',
controller: 'NavController'
},
"content": {
templateUrl: 'deliverables/deliverables.html',
controller: 'DeliverablesController'
},
"footer": {
templateUrl: 'footer.html',
},
reloadOnSearch: false
}
On occasion, I want to load the deliverables state with default parameters. I'd prefer not to use dummy strings... Is there a better way to do this?
$state.go('^.deliverables', {
taxYear: null,
entity: null,
status: null
}); // Navigate to dashboard
But then I wind up with three slashes on my URL. http://example.com/#/deliverables////. Is there a way to make a more friendly URL without using dummy parameters? Is there a better way to do this without winding up with three slashes in a row? It's not the end of the world, but it looks foreign.
If I create two different states, then I have to replicate all of the state information twice.
EDIT: Sometimes I want to load this page with all nullable parameters. But when I do so, I get three consecutive slashes. Other times, I want to change to the state and provide actual values. That works fine. Is there a way to handle this without duplicating the state information into two states?
EDIT2: The page has 4 inputs that filter a jquery datatable. On first page load of the deliverables page, I would not like to filter by any of the inputs. I would also be able to do the angular equivalent of deep linking into the page. These deep links would then filter the table. This is why I have the parameters on the URL
Ui-router 0.2.12 supports squashing of default param values.
See this issue for the RFC:
https://github.com/angular-ui/ui-router/issues/1501
See this plunk: http://plnkr.co/edit/VMPc8D7oUG0B1R3QuiCE?p=preview for a demo.
You can specify how each param's default value is squashed.
Given the following states:
$stateProvider.state({
name: "user",
url: "user/:username",
parent: 'top',
params: {
username: {
value: function(Session) { return Session.username; }
}
},
resolve: {
user: function($stateParams, users) { return users[$stateParams.username]; },
galleries: function($stateParams, photos) { return photos[$stateParams.username] }
},
templateUrl: 'user.html',
controller: function($scope, $state, user, galleries) { $scope.user = user; $scope.galleries = galleries; }
});
$stateProvider.state({
name: "user.gallery",
url: "/gallery/:galleryid",
resolve: {
photos: function($stateParams, galleries) { return galleries[$stateParams.galleryid]; }
},
params: { galleryid: { value: "favorites" } },
templateUrl: 'gallery.html',
controller: function($scope, $state, $stateParams, photos) {
$scope.gallery = $stateParams.galleryid;
$scope.photos = photos;
}
});
$stateProvider.state({
name: "user.gallery.photo",
url: "/photo/:photoid",
resolve: {
photo: function($stateParams, photos) {
return photos.filter(function(photo) { return photo.id === $stateParams.photoid; })[0];
}
},
templateUrl: 'photo.html',
controller: function($scope, $state, $stateParams, photo) {
$scope.gallery = $stateParams.galleryid;
$scope.photo = photo;
}
});
These params:
{
"username": "christopherthielen",
"galleryid": "favorites",
"photoid": "CN0ZRJw"
}
Will be squashed in the URL like so:
{
false: "/user/christopherthielen/gallery/favorites/photo/CN0ZRJw",
"-": "/user/-/gallery/-/photo/CN0ZRJw",
"~": "/user/~/gallery/~/photo/CN0ZRJw",
"": "/user//gallery//photo/CN0ZRJw",
true: "/user/gallery/photo/CN0ZRJw"
}
I am not aware of a best practice for the specific situation you're describing. I think it would fall into two main use cases, that I can envision:
1) The core logic in the controllers and html in your partials of your 3 views is the same whether it has null for the parameters or valid values. In this case, what you have works fine, it just looks a little strange to have the consecutive slashes. There is no problem with that; however, if that bothers you, consider passing "0"s instead of Nulls (assuming 0 is an invalid value for your parameters).
2) If your views are actually different depending on whether there are values in the parameters or not, then you'd probably be better off having a separate state for just the "/deliverables" route. Yes, you'd have to duplicate the views definitions; however, if you have if/then blocks in your controller(s) (and/or ng-if/ng-show/ng-hide blocks in your partials) to handle the cases of null parameters, then you'd really just split that logic out into separate controllers/partials, so that they become simpler and the state definitions wouldn't be duplicative, as the two states would be using different controllers/partials (essentially splitting the existing controllers/partials into two different files each, with only the applicable logic in each).

Angular ui-router to accomplish a conditional view

I am asking a similar question to this question: UI Router conditional ui views?, but my situation is a little more complex and I cannot seem to get the provided answer to work.
Basically, I have a url that can be rendered two very different ways, depending on the type of entity that the url points to.
Here is what I am currently trying
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$state.transitionTo('home.first');
} else {
$state.transitionTo('home.second');
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I set up a Resolve to fetch the actual entity from a restful service.
Every thing seems to be working until I actually get to the transitionTo based on the type.
The transition seems to work, except the resolve re-fires and the getEntity fails because the id is null.
I've tried to send the id to the transitionTo calls, but then it still tries to do a second resolve, meaning the entity is fetched from the rest service twice.
What seems to be happening is that in the onEnter handler, the state hasn't actually changed yet, so when the transition happens, it thinks it is transitioning to a whole new state rather than to a child state. This is further evidenced because when I remove the entity. from the state name in the transitionTo, it believes the current state is root, rather than home. This also prevents me from using 'go' instead of transitionTo.
Any ideas?
The templateUrl can be a function as well so you check the type and return a different view and define the controller in the view rather than as part of the state configuration. You cannot inject parameters to templateUrl so you might have to use templateProvider.
$stateProvider.state('home', {
templateProvider: ['$stateParams', 'restService' , function ($stateParams, restService) {
restService.getEntity($stateParams.id).then(function(entity) {
if (entity.Type == 'first') {
return '<div ng-include="first.html"></div>;
} else {
return '<div ng-include="second.html"></div>';
}
});
}]
})
You can also do the following :
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$timeout(function() {
$state.go('home.first');
}, 0);
} else {
$timeout(function() {
$state.go('home.second');
}, 0);
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I ended up making the home controller a sibling of first and second, rather than a parent, and then had the controller of home do a $state.go to first or second depending on the results of the resolve.
Use verified code for conditional view in ui-route
$stateProvider.state('dashboard.home', {
url: '/dashboard',
controller: 'MainCtrl',
// templateUrl: $rootScope.active_admin_template,
templateProvider: ['$stateParams', '$templateRequest','$rootScope', function ($stateParams, templateRequest,$rootScope) {
var templateUrl ='';
if ($rootScope.current_user.role == 'MANAGER'){
templateUrl ='views/manager_portal/dashboard.html';
}else{
templateUrl ='views/dashboard/home.html';
}
return templateRequest(templateUrl);
}]
});

Resources