ui-router nested states with parameters throw extention - angular-ui-router

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

Related

implementing angular-ui-router 'otherwise' state

If a user types myURL/ or myURL/#/ or even myURL/#/foo they get to my index page.
But if they type myURL/foo, they get a 404. This is terrible. They should instead be redirected to /.
I am trying to implement this and am not having a lot of luck.
(function() {
'use strict';
angular
.module('myApp')
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('index', {
name: 'index',
url: '/',
templateUrl: 'js/views/page1.html',
controllerAs: 'page1Controller',
data: { pageTitle: 'Main' }
})
.state('page2', {
name:'page2',
url: '/page2/:id',
templateUrl: 'js/views/page2.html',
controllerAs: 'page2Controller',
data: { pageTitle: 'page2' }
})
$urlRouterProvider.otherwise('/');
}]);
})();
I have looked at dozens of articles, and nowhere do I seem to be able find this simple case handled.
On the official docs it is mentioned that you can pass $injector and $location to the function otherwise.
Their example looks like this:
app.config(function($urlRouterProvider){
// if the path doesn't match any of the urls you configured
// otherwise will take care of routing the user to the specified url
$urlRouterProvider.otherwise('/index');
// Example of using function rule as param
$urlRouterProvider.otherwise(function($injector, $location){
... some advanced code...
});
})
What you can do to achieve your goal is to create a state, and whenever something it's not matched and enters otherwise fct, send it to that state.
$urlRouterProvider.otherwise(function($injector, $location){
$injector.get('$state').go('404');
});
I have not tested this but should work.

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.

Deferred Promises with AJAX in Angular

I'm trying to send data to my view from an AJAX call to my API. I am able to successfully hit my API and get data, but I was having problems with the view rendering before the AJAX call came back.
I'm trying to wrap my AJAX call in a Promise but it's not working. Here's my layout
Controller
.controller('DashCtrl', function($scope, Tweets) {
$scope.tweets = Tweets.all()
})
Factory doing ajax call
.factory('Tweets', function($http) {
$http.get('http://localhost:3000/tweets')
.success(function(data) {
var tweets = data
debugger
})
return {
all: function() {
//should return the results of the AJAX call when it's complete
}
}
});
I've tried making wrapping the ajax call into a function and using .then(function(payload){ return payload.data }) - Payload.data has my data but its never returned when I call the function. I'm new to angular, so I would appreciate any help or insight.
You should define your factory as
.factory('Tweets', function($http) {
return {
all: function() {
return $http.get('http://localhost:3000/tweets')
.then(function(response) {
return reponse.data;
})
}
}
});
Then change your controller to
.controller('DashCtrl', function($scope, Tweets) {
Tweets.all().then(function(data) {
$scope.tweets = data;
});
})
Use the $resource service. The docs don't mention it, but comments in the source do.
$resolved: true after first server interaction is completed (either with success or rejection), false before that.
So in the controller:
$scope.tweets = $resource('/tweets').query()
And in the view:
<div ng-if="tweets.$resolved">
Loading data with ngResource or from factory promise callback are viable options, but there's one more way nobody mentioned yet: resolve data to controller via route definition. This approach allows to write simplistic controllers that don't know how to load data at all. In most cases it will be more than enough if you don't need to load data dynamically, like pagination or infinite scroll.
You will need to define route and resolve function:
angular
.module('app', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'ctrl',
controllerAs: 'view',
templateUrl: 'view.html',
resolve: {
tweets: function (Tweets) {
return Tweets.all();
}
}
})
})
The tweets property on resolve will inject loaded data into controller as tweets, all you have to do is just assign received data:
.controller('ctrl', function (tweets) {
this.tweets = tweets;
});
In addition, here's how Tweets service might look like:
.factory('Tweets', function ($timeout) {
function all () {
return $timeout(function () {
return ["hey", "there"];
});
}
return {
all: all
};
})
Basically, it exposes methods that return promise, returning some data ($timeout returns promise too, so I've used it instead of $http for example purpose).
Full example on JS Bin.

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...

Angular UI Router state.go parameters not available in stateChangeStart

I'm using Angular UI Router and I need to send a parameter with the state.go method. Like this:
$state.go('myState', { redirect : true });
I also need to check that parameter in the event stateChangeStart. Like this:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
//redirect parameter is not available here.
//should be in toParams right?
}
Edit: here's my statedefinition:
$stateProvider.state('customer.search', {
url: '/search',
views: {
"right-flyover#": {
templateUrl: 'customer/search/search.tpl.html',
controller: 'CustomerSearchCtrl'
}
},
data: {
pageTitle: 'Sök användare',
hidden: true
}
});
The ui-router will pass parameters which were defined for a state hierarchy - we are navigating to. Please check:
URL Parameters (cite:)
Often, URLs have dynamic parts to them which are called parameters. There are several options for specifying parameters. A basic parameter looks like this:
$stateProvider
.state('contacts.detail', {
url: "/contacts/:contactId",
templateUrl: 'contacts.detail.html',
controller: function ($stateParams) {
// If we got here from a url of /contacts/42
expect($stateParams).toBe({contactId: 42});
}
})
So, if you want to work with param redirect, your state should look like this:
$stateProvider.state('customer.search', {
url: '/search/:redirect',
...
});
then we can use:
$state.go('customer.search', { redirect : true });
and that would be part of $statePrams
But maybe you try to use sent options, not parameters:
$state.go(to [, toParams] [, options]) (cite:)
options
Object - If Object is passed, object is an options hash. The following options are supported:
location Boolean or "replace" (default true), If true will update the url in the location bar, if false will not. If string "replace", will update url and also replace last history record.
inherit Boolean (default true), If true will inherit url parameters from current url.
relative stateObject (default $state.$current), When transitioning with relative path (e.g '^'), defines which state to be relative from.
notify Boolean (default true), If true will broadcast $stateChangeStart and $stateChangeSuccess events.
reload v0.2.5 Boolean (default 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.
And that would be then the third param (reload instead of redirect):
$state.go('myState', null, { reload: true });
I recently solved the problem by using cache attribute is stateProvider and set it to false
like
.state('home.stats', {
cache: false,
url:'/stats',
templateUrl: 'views/stats.html',
controller: 'StatsCtrl',
controllerAs: 'stats'
})
$state.go actually returns a promise, so you can do something like:
$state.go('myState').then(() => {
if (redirect) {
// do something
}
});
I realize this isn't a generalized solution to the problem which is what I was looking for as well, but it allowed me to get the job done. You could easily wrap this in a method on the $state object if you need to reuse the logic repeatedly.

Resources