Ember Router - How to handle 404 (Not Found) routes? - ember.js

I'm trying to figure out how to handle invalid routes within my application using Ember.Router.
Currently if I enter an invalid route, e.g. myapp.com/#FooBarDoesntExist, it will redirect to the index route ('/'). I'd like it if I could define a notFound or 404 state that it would route to so I can inform the user what happend. As opposed to them getting dumped on the home page.

A good way to handle this problem is to declare a route who map all possible urls in addition to your routes. You can an example here : http://jsfiddle.net/mbreton/r3C9c/
var App = Ember.Application.create();
App.Router.map(function(){
this.route('detail', {path: "detail"});
this.route('missing', { path: "/*path" });
});
App.MissingRoute = Em.Route.extend({
redirect: function () {
Em.debug('404 :: redirection to index');
this.transitionTo("index");
}
});
App.ApplicationView = Em.View.extend({
didInsertElement:function(){
$('#missingLink').on('click', function (e){
window.location.hash = "#/pepepepepep";
return false;
});
}
});
In this example all unknown urls are redirect to index route.

Ember.Router in its current version does not provide means to handle unknown routes. Time to hack!
Solution 1 - Quick and dirty
The idea here is the following. We have the Ember.Router.route(path) method, which is invoked with the requested (potentially unknown) path. After the invocation of this method, the path of the router is guaranteed to be known. So, if we compare the requested path and the actual path and they differ - then the requested path is invalid and we may redirect a user to the 404 page.
App.Router = Ember.Router.extend({
route: function(path) {
this._super(path);
var actualPath = this.get("currentState").absoluteRoute(this);
if (path !== actualPath) {
this.transitionTo("404page");
}
}
});
This solution is quite expensive. For example, if the current state is "/a/b/c", and a user wants to navigate to "/b/d/e/unknown", the router will dutifully enter known states "b", "d" and "e", and only then we discard the path as unknown. It would be nice if we can tell this before the actual routing starts.
Solution 2 - Fiddling with private methods
Here we check the validity of the given path, and only then tell the router to proceed:
App.Router = Ember.Router.extend({
checkPath: function (path) {
path = path.replace(this.get('rootURL'), '').replace(/^(?=[^\/])/, "/");
var resolvedStates = this.get("states.root").resolvePath(this, path);
var lastState = resolvedStates.get("lastObject");
return lastState.match.remaining == "";
},
route: function(path) {
if (this.checkPath(path)) {
this._super(path);
} else {
this.transitionTo("404page");
}
}
});
This solution also has its drawback - it uses the resolvePath method which is marked as private. Nevertheless, I'd use this solution, since it is more effective than the first one.

The recommended way to do this in Ember 1.13 is to create a catch-all route:
Router.map(function () {
this.route('home');
this.route('login');
this.route('404', { path: '/*path' }); // capture route in path
});
Then put your 404 template in 404.hbs.

Related

Is it possible to have (an undefined number of) optional segments in the Ember router?

I would like to collect extra segments from the Ember router in an array.
Here is a made-up path to illustrate what I mean:
this.route('group', {path: 'group/:group_id(/:segments[])*'}, function() {
Is it possible to use a request like this:
GET /group/123/some/path/segments
And have them collected in an array?
group.id = 123
segments = ['some', 'path', 'segments']
Or is there any way to define optional segments, so I can just add many and collect them manually?
Under the hood, the router is using route-recognizer to determine the routes. There is the notion of star-segments
router.add([{ path: "/pages/*path", handler: page }]);
result = router.recognize("/pages/hello/world");
result === [{ handler: page, params: { path: "hello/world" } }];
This seems like what you're looking for
As a side note, this is my usual 404 approach:
Router.map(function() {
...very last route
this.route('not-found', { path: "/*path"});
});
routes/not-found.js:
export default Route.extend({
model(params){
return params.path
}
});
not-found.hbs
404: /{{model}} not found!
such that /foo/bar yields: 404: /foo/bar not found!

ember default route when url has non-existing route specified [duplicate]

How can I handle the error
Uncaught Error: No route matched the URL '...'
and show a custom 404 page?
Note: This question was asked before and answered several months ago - but does not work anymore.
App.Router.map(function() {
//set up all of your known routes, and then...
this.route("fourOhFour", { path: "*path"});
});
.. where you have your FourOhFourRoute defined to show the "no route found" message of your choosing. You will be able to access the originally requested path in the fourOhFour route as the path parameter.
EDIT: just for clarity -- this answer came after the others were reported not to work anymore.
EDIT 2: I've updated the answer to reflect Yehuda Katz's comment (if I have it wrong, please LMK).
Here is an example:
I define the last route in my router using a wildcard route see: http://emberjs.com/guides/routing/defining-your-routes/#toc_wildcard-globbing-routes
I have a /not-found route, see last route defined in my router /*path to catch any text string, see: https://github.com/pixelhandler/blog/blob/master/client/app/router.js#L19
Router.map(function () {
this.route('about');
this.resource('posts', function () {
this.resource('post', { path: ':post_slug' });
});
this.resource('admin', function () {
this.route('create');
this.route('edit', { path: ':edit_id' });
});
this.route('not-found', { path: '/*path' });
});
That route does a redirect to /not-found, see: https://github.com/pixelhandler/blog/blob/master/client/app/routes/not-found.js
import Ember from 'ember';
export default Ember.Route.extend({
redirect: function () {
var url = this.router.location.formatURL('/not-found');
if (window.location.pathname !== url) {
this.transitionTo('/not-found');
}
}
});
Also any route having a hook (e.g. model, beforeModel, afterModel) that results in a rejected promise, can use the error action to transition to the 404.
actions: {
error: function (error) {
Ember.Logger.error(error);
this.transitionTo('/not-found');
}
}
Which renders a not-found template, see: https://github.com/pixelhandler/blog/blob/master/client/app/templates/not-found.hbs
<h1>404 Not Found</h1>
<p>
Perhaps you have a link that has changed, see {{#link-to 'posts'}}Archives{{/link-to}}.
</p>
Here is my 404 page: http://pixelhandler.com/not-found
You could try adding a catch-all route at the end of your router:
App.Router.map(function() {
this.resource('post', ...);
this.resource('user', ...);
this.route('catchAll', { path: '/*' });
});
App.CatchAllRoute = ...
In Ember 2.x
Inside the App.Router.map function, put code below the the end of the callback function.
this.route('your_handler_route_name', { path: '/*path' });
Now every route does NOT catche by the previous defined routes will be catched by your_handler_route_name route.
Solution 1
To display 404 content:
App.Router.reopen({
handleURL: function (url) {
try {
return this._super(url);
} catch (error) {
if (error.message.match(/No route matched the URL/)) {
return this._super('/404');
}
}
}
});
If you want to URL changes to 404 as well:
App.Router.reopen({
location: locationImplementation,
handleURL: function (url) {
try {
return this._super(url);
} catch (error) {
if (error.message.match(/No route matched the URL/)) {
this.transitionTo('404');
return this._super('/404');
}
}
}
});
To understand what happened here see line 22636 in ember rc2.
Solution 2
Parse current URL and check if route or resource exist using App.Router.router.recognizer.hasRoute('route.path.goes.here');

Redirect in Ember Router

I have a situation where we used to have a login page but now we use a separate oAuth page. I'm trying to clean out a bunch of code, but I need to worry about people who have bookmarked the login route. Ideally, I'd like to be able to do something like this:
Router.map(function () {
this.route('login', {redirectTo: 'index'});
})
and then be able to get rid of the logic in my loginRoute:
var LoginRoute = Ember.Route.extend({
beforeModel: function (transition) {
var result = this._super(transition);
if (transition.isActive === false) {
// Route canceled for auth
return result;
} else {
return this.transitionTo('index');
}
}
});
Is that possible or do I absolutely have to keep my login route?
You can use ember-redirect addon which lets you do what you want:
let Router = Ember.Router.extend({
location: config.locationType,
redirects: {
login: 'index'
}
});

Adding and retrieving metadata from Ember routes

For reporting purposes, I want to associate some metadata with an Ember route, and would prefer to do it as follows:
this.route('list', { path: '/list', description: 'Master List' });
then access this description property from places like the route itself, or from elsewhere, such as the didTransition hook on the application router. I've reviewed the source for Router and Route and cannot say I really understand it, certainly not well enough to understand how to retrieve custom properties specified in this way. I see there is an object called DSL, which is apparently the this of the this.route specified in the map method on Router, but cannot see how to get from here to there. From within a subclass of Ember.Route, I see properties called this.router, and this.router.router, but am unclear on what these point to.
Or, the following would also work if that allowed me to do what I wanted:
this.route('list', { path: '/list' }, function() {
this.description = "Master List";
});
Can I associate custom properties with a route specified in Router#map, and if so how?
There doesn't appear to be an elegant way to set metadata about a route when it is defined in the router, but maybe try this ugly solution within your application controller:
currentPathChange: function () {
switch(this.get('currentPath')){
case 'test.index':
console.log('test.index is the foo!');
break;
case 'test.new':
console.log('test.new is the bar!');
break;
}
}.observes('currentPath')
JSBin DEMO
Without extending Ember's Router, one option is to have a separate object that maintains route metadata. Simple example:
this.route('list', { path: '/list' });
routeMetaData['list'] = 'Master List';
To access the metadata in the didTransition hook:
didTransition: function() {
var metadata = routeMetaData[this.routeName];
}
I ended up solving this along the following lines, writing my own "route" function which records data I need, then passes it along to the DSL:
var myRouteData = {};
function route(dsl, name, options, fn) {
if (typeof options === 'function') fn = options, options = {};
var routeName = dsl.parent ? dsl.parent + '.' + name : name;
myRouteData[routeName] = { options.myRouteOption };
dsl.route(name, options.fn);
}
Usage:
this.resource('foo', function() {
route(this, 'bar', {myRouteOption: true});
});

ember.js authentication

I would like to implement authentication with ember.js.
So, when the application start, before the Router handles the requested url, I want to check the user status. If the user isn't authenticated, I want to save the requested url and redirect to a specific url (/login).
I tried to implement this overloading Ember.Route but I don't think it's a good practice.
For example, if i do:
var AuthRoute = Ember.Route.extend({
redirect: function() {
var controller = App.userController;
if (!controller.get("userAuth")) {
controller.set("lastFilter", this.routeName);
this.transitionTo("index");
}
}
}
});
If, the url is '/admin/foobar', the admin route will redirect instead of foobar.
Can I process redirection before the Router to start?
I use something like this
Ember.SecureRoute = Ember.Route.extend({
role: null,
redirect: function (model) {
if (!this.controllerFor('login').get('authenticated')) {
this._routeToLogin();
}
var role = this.get('role');
if (!Ember.isEmpty(role) && !this.controllerFor('login').hasRole(role)) {
this._routeToLogin();
}
},
_routeToLogin: function () {
var infos = this.get('router.router.currentHandlerInfos');
this.router.router.didTransition(infos);
var routeName = !this.router.router.hasRoute(this.routeName) ? this.routeName + '.index' : this.routeName;
var params = infos.filter(function (item, index, enumerable) { return item.context !== undefined; }).map(function (item) { return item.context; })
var url = Ember.Router.prototype.generate.apply(this.router, params.insertAt(0, routeName))
this.router.location.setURL(url);
this.transitionTo("login");
}
});
in your loginController you can then use the browser history to go back to your original route
APP.LoginController = Ember.Controller.extend({
//other stuff
authenticate: function (username, password) {
//do the authentication
history.go(-1);
}
});
The way that I do it is I pass down the authenticated user with my data. and I have an initConfiguration function inside of my main App
so inside of index file (in this case I am showing you jade) I have this:
// initialize ember settings
script(type='text/javascript')
App.initConfiguration('!{currentUser}')
and inside of my App file I have (coffeescript here)
window.App = Ember.Application.create
currentUser: null
initConfiguration: (currentUser) ->
if currentUser? and currentUser.length > 0
#set 'currentUser', currentUser
If you are using ember-data, then you have to change the application file to
window.App = Ember.Application.create
currentUser: null
tempCurrentUser: null
initConfiguration: (currentUser) ->
##
# set the tempCurrentUser to the currentUser passed in, this is then
# set in the ApplicationRoute to the App.currentUser
# and destroyed (this is necessary as the ember store is not initialzed yet
# and just setting it here will result in an error)
if currentUser? and currentUser.length > 0
#set 'tempCurrentUser', currentUser
and then inside your application route do the following
App.ApplicationRoute = Ember.Route.extend
setupController: (controller) ->
if App.tempCurrentUser?
App.setCurrentUser(App.tempCurrentUser)
Ember has a fantastic guide on preventing and retrying authentication: http://emberjs.com/guides/routing/preventing-and-retrying-transitions/
A simple way to do transitions based on whether or not a user is logged in:
App.ApplicationRoute = Ember.Route.extend({
willTransition: function () {
var session = this.controllerFor('session');
if (!session.get('authed')) {
this.transitionTo('login');
}
}
});
The example above assumes you have some kind of session controller or object managing the active sessions. This works because the ApplicationRoute is the very first route that is hit whenever you enter your application (from any URL).