I have a Durandal application, and I use router.mapUnknownRoutes
to display a user-friendly error page if the URL does not correspond to a known route. This works fine -- if I go to, say /foo
, and that doesn't match a route, then the module specified by mapUnknownRoutes
is correctly displayed.
However I cannot find any way to display that same error page when I have a parameterised route, and the parameter does not match anything on the backend.
For example, say I have a route like people/:slug
where the corresponding module's activate
method looks like this:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
});
};
If I go to, say /people/foo
, then the result depends on whether dataService.load('foo')
returns data or an error:
foo
exists on the backend then no problem - the observables are set and the composition continues. foo
doesn't exist, then the error is thrown (because there is no catch
). This results in an unhandled error which causes the navigation to be cancelled and the router to stop working.I know that I can return false
from canActivate
and the navigation will be cancelled in a cleaner way without borking the router. However this isn't what I want; I want an invalid URL to tell the user that something went wrong.
I know that I can return { redirect: 'not-found' }
or something similar from canActivate
. However this is terrible because it breaks the back button -- after the redirect happens, if the user presses back they go back to /people/foo
which causes another error and therefore another redirect back to not-found
.
I've tried a few different approaches, mostly involving adding a catch
call to the promise definition:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
}).catch(function (err) {
// ... do something to indicate the error ...
});
};
activate
(or canActivate
) notify the router that the route is in fact invalid, just as though it never matched in the first place? activate
(or canActivate
) issue a rewrite
(as opposed to a redirect
) so that the router will display a different module without changing the URL?compose
some other module in place of the current module (and cancel the current module's composition)?I've also tried an empty catch
block, which swallows the error (and I can add a toast here to notify the user, which is better than nothing). However this causes a lot of binding errors because the observables expected by the view are never set. Potentially I can wrap the whole view in an if
binding to prevent the errors, but this results in a blank page rather than an error message; or I have to put the error message into every single view that might fail to retrieve its data. Either way this is view pollution and not DRY because I should write the "not found" error message only once.
I just want an invalid URL (specifically a URL that matches a route but contains an invalid parameter value) to display a page that says "page not found". Surely this is something that other people want as well? Is there any way to achieve this?
It turns out that Nathan's answer, while not quite right, has put me on the right track. What I have done seems a bit hacky but it does work.
There are two options that can be passed to router.navigate()
- replace
and trigger
. Passing replace
(which defaults to false
) toggles between the history plugin using pushState
and replaceState
(or simulating the same using hash change events). Passing trigger
(which defaults to true
) toggles between actually loading the view (and changing the URL) vs only changing the URL in the address bar. This looks like what I want, only the wrong way around - I want to load a different view without changing the URL.
(There is some information about this in the docs, but it is not very thorough: http://durandaljs.com/documentation/Using-The-Router.html)
My solution is to navigate to the not-found
module and activate it, then navigate back to the original URL without triggering activation.
So in my module that does the database lookup, in its activate
, if the record is not found I call:
router.navigate('not-found?realUrl=' + document.location.pathname + document.location.hash, { replace: true, trigger: true });
(I realise the trigger: true
is redundant but it makes it explicit).
Then in the not-found
module, it has an activate
that looks like:
if (params.realUrl) {
router.navigate(params.realUrl, { replace: true, trigger: false });
}
What the user sees is, it redirects to not-found?realUrl=people/joe
and then immediately the URL changes back to people/joe
while the not-found
module is still displayed. Because these are both replace
style navigations, if the user navigates back, they go to the previous entry, which is the page they came from before clicking the broken link (i.e. what the back button is supposed to do).
Like I said, this seems hacky and I don't like the URL flicker, but it seems like the best I can do, and most people won't notice the address bar.