Skip to content

Routed Pages ​

By default, every navigation creates the page component again. A page can instead declare two optional methods, and the router then works with it:

MethodCalledEffect
onRoute(route)Once the page is connected (after its onConnect), then at each URL change whose route is on the same pageThe router keeps the page instead of creating it again
canLeave(next)Before each URL changeReturning false (or a promise of false) keeps the person on the page, and puts the URL back

Both are typed by the RoutedPage interface.

Keeping a Page: onRoute ​

tsx
import { Jadis } from '@jadis/core';

import type { ActiveRoute, RoutedPage } from '@jadis/core';

export class UsersPage extends Jadis implements RoutedPage {
  static readonly selector = 'users-page';

  onRoute(route: ActiveRoute): void {
    // route.params.id is undefined on /users, set on /users/:id
    this.showUser(route.params.id);
  }
}
typescript
const routes = defineRoutes({
  user:  { path: '/users/:id', page: UsersPage },
  users: { path: '/users', page: UsersPage },
});

Going from /users to /users/42, then to /users/7, keeps the same <users-page>: its list, its scroll position and anything it holds stay. The router compares components, not route names: two routes on the same page (and the same root component) keep it.

When the page is kept, the router also updates the parameters' attributes: it sets the new ones and removes those the new route does not have. A page can therefore follow its parameters with useChange(…, { attribute }) instead of onRoute, but it must still declare onRoute to be kept.

onRoute is called after onConnect, so the page's listeners are wired when the first route arrives. If the route changes again before the page is connected, the page receives only the last one.

A page without onRoute behaves as before: it is created again at each navigation.

Holding the Person Back: canLeave ​

tsx
export class EditorPage extends Jadis implements RoutedPage {
  static readonly selector = 'editor-page';

  canLeave(): boolean | Promise<boolean> {
    return !this.isDirty() || askToLeave(); // a dialog of the app, resolving to true for "Leave"
  }
}

canLeave receives the route the person would go to, so it can let some changes through. It is asked:

  • before router.goto() changes the URL: when it refuses, nothing changes;
  • after the URL changed through a link, the address bar or the back or forward button (hashchange or popstate): when it refuses, the router takes the person back to the history entry of the page, so the history stays as it was: the next Back still goes where it went before.

It is not asked when the URL did not change.

Asking first: a promise ​

A page that asks the person in a dialog of its own answers with a Promise<boolean> (the type LeaveAnswer). While it is pending, the page stays. After router.goto(), the URL changes only once the answer is yes. After a link, the address bar or the back button, the URL is left where the person went: a yes then shows that route without adding to the history, and a no (or a promise that rejects) puts the previous URL back. A page with nothing to lose should answer true at once rather than a promise: the router then goes on without waiting.

A navigation that comes while the page asks wins over it: the answer of an earlier question is then ignored, so a late "Leave" never takes the person to a page they no longer wanted. Asking twice is the page's to avoid, by giving the same pending question to both (jlib's guardLeave() does).

For a reload or a closed tab, use the browser's beforeunload event: the browser asks, not the page.

The history entries ​

To find the page's entry again, the router numbers each history entry it shows, in history.state (under jadisRouterIndex), beside what the app keeps there. An entry it has not numbered is one a link or the address bar just added, after the page's. So an app that replaces the URL keeps the state: history.replaceState(history.state, '', url), not replaceState(null, …), which forgets the number. An app that follows its links with history.pushState then a popstate event needs nothing more.

The Active Route ​

onRoute, canLeave and router.currentRoute give an ActiveRoute:

typescript
interface ActiveRoute {
  name: string;                    // the route's key
  path: string;                    // the route's path pattern, such as '/users/:id'
  componentSelector: string;
  params: Record<string, string>;  // the parameters read from the URL
  query: URLSearchParams;          // the URL's query; in hash mode, the part of the hash after '?'
}

The query is not part of matching: /#/users?sort=name matches /users.

See Also ​