1
votes

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:

  • If foo exists on the backend then no problem - the observables are set and the composition continues.
  • If 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 ...
    });
};
  • Can the activate (or canActivate) notify the router that the route is in fact invalid, just as though it never matched in the first place?
  • Can the activate (or canActivate) issue a rewrite (as opposed to a redirect) so that the router will display a different module without changing the URL?
  • Can I directly 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?

2

2 Answers

2
votes

I think you should be able to use the following from the activate or canActivate method.

router.navigate('not-found', {replace: true});

2
votes

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.

Working repo that demonstrates this solution