UI-Router Child State For Multiple Parent States - angular-ui-router

I have a project that requires parent routes that are separated, in order to isolate URLs, controllers and API calls.
/boardgames/:id/review/
/boardgames/:id/setup/
The issue I am trying to solve is how to have a common child route, for all of the parent routes?
/boardgames/:id/review/rules/
/boardgames/:id/setup/rules/
Here is a simplified version of the current UI-Router config:
.state('app.frontend.view', {
abstract: true,
url: '/boardgames/:id',
views: {
'page#': {
templateUrl: 'public/html/game/view/index.html',
resolve: {},
controller: 'View'
}
}
})
.state('app.frontend.view.review', {
url: '/review/',
views: {
'tab#app.frontend.view': {
templateUrl: 'public/html/game/tabs/review/index.html',
resolve: {},
controller: 'ViewReview'
}
}
})
.state('app.frontend.view.setup', {
url: '/setup/',
views: {
'tab#app.frontend.view': {
templateUrl: 'public/html/game/tabs/setup/index.html',
resolve: {},
controller: 'ViewSetup'
}
}
});
Here is the HTML structure: (public/html/game/view/index.html)
<div class='item'>
<div ui-view='tab'></div>
</div>
ALL IS WORKING UP TO THIS POINT.
I was thinking of passing another parameter to create a dynamic state... and then chaining the child route:
.state('app.frontend.view.{{:path}}', {
url: '/:path/',
views: {
'tab#app.frontend.view': {
templateUrl: 'public/html/game/tabs/:path/index.html',
resolve: {},
controller: ':path' (format :path into friendly name)
}
}
})
.state('app.frontend.view.{{:path}}.rules', {
url: '/rules/',
views: {
'rules#app.frontend.view': {
templateUrl: 'public/html/game/tabs/rules/index.html',
resolve: {},
controller: 'ViewRules'
}
}
});
Is this the best way to go forward?

After researching this topic further, I found that:
State naming should be locked in during the config phase.
.state('app.frontend.view.{{:path}}', {}
Would equal:
.state('app.frontend.view.image', {}
The Url would include the parameter '/:path/, templateUrl and controller can all have locked in names and use the parameter differently:
In the router resolve it would be:
resolve: {
tabResolve: ['$stateParams', 'game', function($stateParams, game) {
return game.show({
id: $stateParams.id,
tab: $stateParams.tab
}).$promise;
}]
}
Only one controller is needed now, since the data being passed in is different for every parameter (review, setup).

Related

can't get nested views to work with ui-router

I have been able to switch out my angular router code with ui-router and it works fine with a basic setup. But on my page I have a partial that I want to reuse in multiple views and I can't get that working. Can anyone see an issue with my code below.
routes....
$stateProvider
.state( 'jobs', {
url: '/jobs',
templateUrl: templates.jobs,
views: {
'jobs': {
templateUrl: templates.jobList,
controller: 'JobListController'
},
'alert': {
templateUrl: templates.alert,
controller: 'AlertController'
}
}
})
templates.jobs file...
<div ui-view="jobs"></div>
<div ui-view="alert"></div>
and then the templates.jobList and templates.alert are just regular html blocks.
I have a main page with just a where my app initially loads. It's within this ui-view that I want to load the templates.jobs view along with it's nested views.
I've found that if I remove the templateUrl: templates.jobs from my jobs state and then move the two ui-views from that jobs file to my main html file it works. However, my main file needs to be able to load many potential views - the ui-view on the main html is just a placeholder where the rest of the application lives. So I can't have those two ui-views in my main file. Is there a way to make this work? Thanks.
Also set the parent view of jobs state in the views object and use absolute targeting:
$stateProvider.state('jobs', {
url: '/jobs',
views: {
'#': {
template: '<div ui-view="jobs"></div><div ui-view="alert"></div>',
controller: function () {}
},
'jobs#jobs': {
template: '<h2>Joblist</h2>',
controller: function () {}
},
'alert#jobs': {
template: '<h2>alert</h2>',
controller: function () {}
}
}
});
Working example on Plunker: http://plnkr.co/edit/AR8RK1Ik1xeGL0xUBV6e?p=preview
Reference: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views#view-names---relative-vs-absolute-names

How to set mandatory route parameters

I want to make a route with has a mandatory parameter. If not, it should fall into
$urlRouterProvider.otherwise("/home");
Current route:
function router($stateProvider) {
$stateProvider.state("settings", {
url: "^/settings/{id:int}",
views: {
main: {
controller: "SettingsController",
templateUrl: "settings.html"
}
}
});
}
Currently both the routes below are valid:
http://myapp/settings //Should be invalid route
http://myapp/settings/123
Any ideas?
Use a state change start listener to check if params were passed:
$rootScope.$on('$stateChangeStart',
function (event, toState, toParams, fromState, fromParams) {
if(toState.name==="settings")
{
event.preventDefault(); //stop state change
if (toParams.id===undefined)
$state.go("home");
else
$state.go(toState, toParams);
}
});
The following solution is valid for ui-router 1.0.0:
.config(($stateProvider, $transitionsProvider) => {
//Define state
$stateProvider.state('verifyEmail', {
parent: 'portal',
url: '/email/verify/:token/:optional',
component: 'verifyEmail',
params: {
token: {
type: 'string',
},
optional: {
value: null,
squash: true,
},
},
});
//Transition hooks
$transitionsProvider.onBefore({
to: 'verifyEmail',
}, transition => {
//Get params
const params = transition.params();
//Must have token param
if (!params.token) {
return transition.router.stateService.target('error', {
type: 'page-not-found',
});
}
});
})
The above will make the :token parameter mandatory and the :optional parameter optional. If you try to browse to the page without the token parameter it will fail the transition and redirect to your error page. If you omit the :optional parameter however, it will use the default value (null).
Remember to use squash: true on the trailing optional parameters, because otherwise you'll also get a 404 if you omit the trailing / in the URL.
Note: the hook is required, because if you browse to email/verify/ with a trailing slash, ui-router will think the token parameter is an empty string. So you need the additional handling in the transition hook to capture those cases.
In my app I had to make required parameters for a lot of routes. So I needed a reusable and DRY way to do it.
I define a constants area in my app to access global code. I use for other things as well.
I run this notFoundHandler at app config time. This is setting up a router state for handling errors. It is setting the otherwise route to this error route. You could define a different route for when a required parameter is missing, but for us this was defined as being the same as a 404 experience.
Now at app run time I also define a stateChangeErrorHandler which will look for a rejected route resolve with the 'required-param' string.
angular.module('app')
.constant('constants', constants)
.config(notFoundHandler)
.run(stateChangeErrorHandler);
// use for a route resolve when a param is required
function requiredParam(paramName) {
return ['$stateParams', '$q', function($stateParams, $q) {
// note this is just a truthy check. if you have a required param that could be 0 or false then additional logic would be necessary here
if (!$stateParams[paramName]) {
// $q.reject will trigger the $stateChangeError
return $q.reject('required-param');
}
}];
}
var constants = {
requiredParam: requiredParam,
// define other constants or globals here that are used by your app
};
// define an error state, and redirect to it if no other route matches
notFoundHandler.$inject = ['$stateProvider', '$urlRouterProvider'];
function notFoundHandler($stateProvider, $urlRouterProvider) {
$stateProvider
//abstract state so that we can hold all our ingredient stuff here
.state('404', {
url: '/page-not-found',
views: {
'': {
templateUrl: "/app/error/error.tpl.html",
}
},
resolve: {
$title: function () { return 'Page Not Found'; }
}
});
// redirect to 404 if no route found
$urlRouterProvider.otherwise('/page-not-found');
}
// if an error happens in changing state go to the 404 page
stateChangeErrorHandler.$inject = ['$rootScope', '$state'];
function stateChangeErrorHandler($rootScope, $state) {
$rootScope.$on('$stateChangeError', function(evt, toState, toParams, fromState, fromParams, error) {
if (error && error === 'required-param') {
// need location: 'replace' here or back button won't work on error page
$state.go('404', null, {
location: 'replace'
});
}
});
}
Now, elsewhere in the app, when I have a route defined, I can make it have a required parameter with this route resolve:
angular.module('app')
.config(routeConfig);
routeConfig.$inject = ['$stateProvider', 'constants'];
function routeConfig($stateProvider, constants) {
$stateProvider.state('app.myobject.edit', {
url: "/:id/edit",
views: {
'': {
template: 'sometemplate.html',
controller: 'SomeController',
controllerAs: 'vm',
}
},
resolve: {
$title: function() { return 'Edit MyObject'; },
// this makes the id param required
requiredParam: constants.requiredParam('id')
}
});
}
I'd like to point out that there shouldn't be any problem with accessing the /settings path, since it doesn't correspond to any state, unless you've used inherited states (see below).
The actual issue should happen when accessing the /settings/ path, because it will assign the empty string ("") to the id parameter.
If you didn't use inherited states
Here's a solution in plunker for the following problem:
accessing the /state_name/ path, when there's a state with url /state_name/:id
Solution explanation
It works through the onBefore hook (UI router 1.x or above) of the Transition service, which prevents transitioning to states with missing required parameters.
In order to declare which parameters are required for a state, I use the data hash like this:
.state('settings', {
url: '/settings/:id',
data: {
requiredParams: ['id']
}
});
Then in app.run I add the onBefore hook:
transitionService.onBefore({}, function(transition) {
var toState = transition.to();
var params = transition.params();
var requiredParams = (toState.data||{}).requiredParams || [];
var $state = transition.router.stateService;
var missingParams = requiredParams.filter(function(paramName) {
return !params[paramName];
});
if (missingParams.length) {
/* returning a target state from a hook
issues a transition redirect to that state */
return $state.target("home", {alert: "Missing params: " + missingParams});
}
});
If you used inherited states
You could implement the same logic via inherited states:
function router($stateProvider) {
$stateProvider
.state('settings', {
url: '/settings'
})
.state('settings.show", {
url: '/:id'
});
}
then you'd need to add the abstract property to the parent declaration, in order to make /settings path inaccessible.
Solution explanation
Here's what the documentation says about the abstract states:
An abstract state can never be directly activated. Use an abstract state to provide inherited properties (url, resolve, data, etc) to children states.
The solution:
function router($stateProvider) {
$stateProvider
.state('settings', {
url: '/settings',
abstract: true
})
.state('settings.show", {
url: '/:id'
});
}
Note: that this only solves the issue with /settings path and you still need to use the onBefore hook solution in order to also limit the access to /settings/.
it is not very well documented, but you can have required and optional parameters, and also parameters with default values.
Here is how you can set required params:
function router($stateProvider) {
$stateProvider.state("settings", {
url: "^/settings/{id:int}",
params: {
id: {}
},
views: {
main: {
controller: "SettingsController",
templateUrl: "settings.html"
}
}
});
}
I never used params with curly brackets, just with the semicolon, like this url: "^/settings/:id", but from what I read, those are equivalent.
For other types of parameters, please see the other half of my answer here: AngularJS UI Router - change url without reloading state
Please note that when I added that answer, I had to build ui-router from source, but I read that functionality has been added to the official release by now.

ui-router nested states with parameters throw extention

I'm trying to make some nested states with dynamic options.
This kind of states works fine: app, app.process, app.process.step2
But my situation is little different because I want to pass some data in URL.
Here is my states
.state('app.process/:type', {
url: "/process/:type",
views: {
'menuContent1': {
templateUrl: "templates/intro.html",
controller: 'IntroCtrl',
}
}
})
.state('step/:type/:step', {
url: "/process/:type/:step",
parent: 'app.process',
views: {
'proiew': {
templateUrl: "templates/processes/increase.html",
controller: "increaseCtrl",
}
}
})
While trying to run this
$state.go('step/:type/:step', {type:$stateParams.type, step:2});
I get an error
Error: Could not resolve 'new/:type/:step' from state 'app.process/:type'
at Object.transitionTo (http://localhost:8100/lib/ionic/js/ionic.bundle.js:33979:39)
at Object.go (http://localhost:8100/lib/ionic/js/ionic.bundle.js:33862:19)
at Scope.$scope.goNext (http://localhost:8100/js/controllers/IntroCtrl.js:11:18)
at http://localhost:8100/lib/ionic/js/ionic.bundle.js:18471:21
at http://localhost:8100/lib/ionic/js/ionic.bundle.js:43026:9
at Scope.$eval (http://localhost:8100/lib/ionic/js/ionic.bundle.js:20326:28)
at Scope.$apply (http://localhost:8100/lib/ionic/js/ionic.bundle.js:20424:23)
at HTMLButtonElement.<anonymous> (http://localhost:8100/lib/ionic/js/ionic.bundle.js:43025:13)
at http://localhost:8100/lib/ionic/js/ionic.bundle.js:10478:10
at forEach (http://localhost:8100/lib/ionic/js/ionic.bundle.js:7950:18)
any suggestions?
there are many different ways to pass parameters to a state, first of all you can simply set the parameters on the click of the link and then take them from the scope.
but if we are talking on something more reliable and readable i suggest this post, and specifically i like using Resolve. i'll put here an example of resolve, but i encourage you to read and find out what works best for you:
$stateProvider
.state("customers", {
url : "/customers",
templateUrl: 'customers.html',
resolve: {
// A string value resolves to a service
customersResource: 'customersResource',
// A function value resolves to the return
// value of the function
customers: function(customersResource){
return customersResource.query();
}
},
controller : 'customersCtrl'
});

Ionic ui-router

I am having an issue using the ui-router with ionic tabs.
When I attempt to transition to a state that is nested within a tab from a separate tab it appears to resolve it in the stateProvider, in terms of entering the resolve statements, but never actually enters the state.
The applicable states are here:
.state('index', {
abstract: true,
templateUrl:'app/main.html',
controller: 'MainCtrl'
})
.state('index.got', {
url: "/got",
views: {
'got-tab': {
templateUrl: "app/got/main.html",
controller: 'GotCtrl'
}
}
})
.state('index.got.listing', {
url: "/listings/:id",
templateUrl: "app/got/listing/listing.html",
controller: "GotListingCtrl",
resolve: {
listing: function(Listing, $stateParams) {
return Listing.get({id: $stateParams.id});
}
},
})
.state('index.feed', {
url: "/feed",
views: {
'feed-tab': {
templateUrl: "app/home/feed/feed.html",
controller: 'FeedCtrl',
resolve: {
listings: function(CurrentUser) {
return CurrentUser.feed()
}
}
}
}
And the tabs
<ion-tabs class="tabs-striped tabs-color-positive tabs-icon-top">
<ion-tab title="Got" ui-sref="index.got.listings">
<ion-nav-view name="got-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="Home" ui-sref="index.feed">
<ion-nav-view name="feed-tab"></ion-nav-view>
</ion-tab>
</ion-tabs>
The call I make to transfer states is
var index = $state.get('index')
$state.transitionTo('.got.listing', {id: '1'}, {relative: index})
I assume the issue is because I am dealing with nested names views, but any help would be much appreciated thanks
your resolves Should just be returning the Parameter Information for the current state, so for listing tab:
.state('index.got.listing', {
url: '/listings/:id,
templateUrl: '/app/got/listing/listing.html',
controller: 'GotListingCtrl',
resolve: {
listing: ['$stateParams', function($stateParams){
return $stateParams.id;
}]
}
})

ui-router Abstract State Combination

I created two json files where I'm getting the data through a controller homeCtrl and articleCtrl. Than I have a state to display all my data places and articles
.state('overview', {
abstract: true,
url: '/overview',
templateUrl: '_/partial/overview/overview.html',
controller: 'homeCtrl',
resolve: {
places: ['$http', function($http) {
return $http.get('_/api/place.json').then(function(response) {
return response.data;
})
}]
}
})
.state('overview.all', {
url: '/all',
templateUrl: '_/partial/overview/overview-all.html',
controller: 'homeCtrl',
resolve: {
places: ['$http', function($http) {
return $http.get('_/api/place.json').then(function(response) {
return response.data;
})
}]
}
})
.state('overview.articles', {
url: '/articles',
templateUrl: '_/partial/overview/overview-articles.html',
controller: 'articleCtrl',
resolve: {
articles: ['$http', function($http) {
return $http.get('_/api/article.json').then(function(response) {
return response.data;
})
}]
}
})
Afterwards I created two states to get the data by id
.state('overview.detail-place', {
url: '/:id/place',
templateUrl: '_/partial/details/detail-place.html',
controller: function($scope, $stateParams){
$scope.place = $scope.places[$stateParams.id];
}
})
.state('overview.detail-article', {
url: '/:id/article',
templateUrl: '_/partial/details/detail-article.html',
controller: function($scope, $stateParams){
$scope.article = $scope.articles[$stateParams.id];
}
})
I have no problem going to the detail page of a place but when I want to go to an article I get this error, 99 being the id of the article.
TypeError: Cannot read property '99' of undefined
at new $stateProvider.state.state.state.state.state.state.state.state.controller (http://localhost:8888/%203.0v/_/js/app.js:86:53)
at d (http://localhost:8888/%203.0v/_/lib/angular.min.js:34:265)
at Object.instantiate (http://localhost:8888/%203.0v/_/lib/angular.min.js:34:394)
at http://localhost:8888/%203.0v/_/lib/angular.min.js:66:112
at http://localhost:8888/%203.0v/_/lib/angular-ui-router.min.js:7:15323
at J (http://localhost:8888/%203.0v/_/lib/angular.min.js:53:345)
at f (http://localhost:8888/%203.0v/_/lib/angular.min.js:46:399)
at http://localhost:8888/%203.0v/_/lib/angular.min.js:46:67
at j (http://localhost:8888/%203.0v/_/lib/angular-ui-router.min.js:7:14566)
at http://localhost:8888/%203.0v/_/lib/angular-ui-router.min.js:7:14835 <div class="main-container overview ng-scope" ui-view="">
The issue here is, that state 'overview.articles' does initiate the articles collection
.state('overview.articles', {
...
controller: 'articleCtrl',
resolve: {
articles: ....
So most- likely the 'articleCtrl' does place the articles into the $scope.articles.
BUT, this state: 'overview.detail-article' in fact does not have access to that collection:
.state('overview.detail-article', {
...
controller: function($scope, $stateParams){
$scope.article = $scope.articles[$stateParams.id];
}
And why? because the 'overview.detail-article' is not child state of the 'overview.articles'
But this still does not have to be enough with ui-router, check this:
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.
So we need the detail state to be a child of articles and also nested in the parent view:
.state('overview.articles.detail-article', { ...
A plunker with working example...

Resources