implementing angular-ui-router 'otherwise' state - angular-ui-router

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.

Related

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.

Page error on page refresh Ember app

I have an ember app that I am developing and everything is working fine except when I refresh the page whilst on a nested route eg. /projects/1 I receive the following error:
Assertion Failed: You may not passundefinedas id to the store's find method
I am fairly sure this is to do with the way I have setup my routing but can't seem to reorganise them to fix it.
They look like so:
App.Router.reopen({
location: 'auto',
rootURL: '/'
});
App.Router.map(function() {
this.route('projects', { path: '/'});
this.route('project', { path: '/projects/:id' });
});
Any help would be awesome! Thanks.
My first suggestion for you would be to change :id segment do :project_id - it's how they define it in documentation. So your router code looks like:
App.Router.map(function() {
this.route('projects', { path: '/'});
this.route('project', { path: '/projects/:project_id' });
});
If this doesn't help, create:
App.ProjectRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('project', params.project_id);
},
});
If you still get error with passing undefined to store.find try to console.log(params) and see if project_id is defined there and matches value from URL.

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

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.

BackboneJS + Codeigniter pushState true not working

So I have this Backbone App where I use Codeigniter for the Backend. For some reason, pushState:true does not work.
So, my main.js of my backbone app has this:
Backbone.history.start({ pushState: true, root: App.ROOT });
My app.js has this:
var App = {
ROOT: '/projects/mdk/'
};
and my navigation module, which renders the menulinks, each item has this:
this.insertView(new ItemView({
model: new Navigation.ItemModel({
href: App.ROOT + 'home',
class: 'home',
triggers: 'home',
route: this.route
})
}));
and the model for it:
Navigation.ItemModel = Backbone.Model.extend({
defaults: {
href: '',
text: '',
triggers: [],
route: ''
}
});
All I get from this is "Page not found"...
Add: When I in the view change it to href:'#news' - it works, but it dont really makes sense...
Anyone who knows the issue here?
From the documentation (http://backbonejs.org/#History):
Note that using real URLs requires your web server to be able to
correctly render those pages, so back-end changes are required as
well. For example, if you have a route of /documents/100, your web
server must be able to serve that page, if the browser visits that URL
directly.
The problem is that your server isn't responding to whatever URL your app is on. For every URL that your Backbone app can reach, your server MUST return a valid HTML page (contianing your Backbone app).
ok I found a solution by myself:
I made this hack:
$(document).on('click', 'a:not([data-bypass])', function (evt) {
var href = $(this).attr('href');
if (href && href.indexOf('#') === 0) {
evt.preventDefault();
Backbone.history.navigate(href, true);
}
});
and then I made:
href: '#home',
That solved the problem, now evereythings runs fluently..

Resources