Routing
Stack Navigation
Mobile-style stack navigation; kept pages, the transition animation and swiping back.
Stack navigation — stack
By default, when you leave a page the old page is thrown away, and it is recreated when you come back. In mobile apps, though, the expected behaviour is a stack: going forward keeps the previous page with its state, its scroll position and its form, and going back brings back the same instance. stack turns this on:
app.useRouter({
routes,
mode: "history",
stack: {
retain: true, // keep previous pages hidden in the DOM (default false: in memory, detached from the DOM)
depth: 5, // how many previous pages to keep at most (default 5; the oldest page past the limit is dropped)
persist: false, // write the stack's addresses to sessionStorage (default false)
animation: "slide", // 'none' (default) | 'slide' | a custom function
duration: 300, // 'slide' duration (ms)
swipeBack: true, // go back by swiping right from the left edge of the screen (off by default)
},
});
app.useRouter({
routes,
mode: "history",
stack: {
retain: true, // keep previous pages hidden in the DOM (default false: in memory, detached from the DOM)
depth: 5, // how many previous pages to keep at most (default 5; the oldest page past the limit is dropped)
persist: false, // write the stack's addresses to sessionStorage (default false)
animation: "slide", // 'none' (default) | 'slide' | a custom function
duration: 300, // 'slide' duration (ms)
swipeBack: true, // go back by swiping right from the left edge of the screen (off by default)
},
});
stack: true turns it on with the default values. If the option is not given, the behaviour is as described above and nothing changes. In shell mode it has no effect, because there is no history.
The rules:
- A kept page is tied to the history entry, not to the route.
/notes/1→/notes/2→ back: the very instance of/notes/1comes back. (With the stack on, going forward to the same route with different parameters creates a new instance;onUpdateis still called.) - Going forward (push/forward) keeps the page being left; going back (back) throws it away. The browser's forward button recreates that page. A page replaced with
replaceis not kept. Going back and then somewhere new throws away the pages of the forward entries. - When a kept page is hidden,
onDeactivatedruns, and when it comes back,onActivatedand the route'sonShowhook run. The positions of scrollable items inside the page are kept. - With
retain: true, the hidden page stays in the DOM withdisplay: none,inertandaria-hidden; its styles are put back exactly as they were when it returns. If the page has no element (a fragment), it is kept by detaching it from the DOM. keepAliveroutes keep using their own cache.- When the application or the router is disposed, all kept pages are disposed.
app.router.stack gives the current list of the stack: [{ index, uri, current, retained }]. Use it for work such as showing the previous page's name on a back button. With persist: true this list comes back after a page reload too; on reload the previous pages are not recreated, they are created as you go back.
The transition animation. animation: 'slide' slides the new page in from the right when going forward, and slides the current page out to the right when going back; meanwhile the other page stays underneath. The animation works independently of the retain setting: with retain: false, the page being left stays in the DOM until the animation finishes and is detached after that. If the user has chosen "reduce motion" in the system, the animation is skipped. To write your own animation, give a function; the two pages arrive positioned on top of each other, and the styles are rolled back when the function finishes:
app.useRouter({
routes,
stack: {
retain: true,
animation: async ({ direction, entering, leaving }) => {
await Promise.all([
entering?.animate([{ opacity: 0 }, { opacity: 1 }], 200).finished,
leaving?.animate([{ opacity: 1 }, { opacity: 0 }], 200).finished,
]);
},
},
});
app.useRouter({
routes,
stack: {
retain: true,
animation: async ({ direction, entering, leaving }) => {
await Promise.all([
entering?.animate([{ opacity: 0 }, { opacity: 1 }], 200).finished,
leaving?.animate([{ opacity: 1 }, { opacity: 0 }], 200).finished,
]);
},
},
});
In a navigation where the stack animation runs, the pages' own transitions (motif.options.transition, see Styling and Animation) do not play; only the stack animation does the transition.
The pages' own transitions. If animation is not given, the stack uses the pages' own enter/leave transitions, and there is no need to define a stack animation separately. The order is the same as with the stack off: first the leave of the page being left finishes, then the incoming page enters.
- Going forward, the page being left plays its leave transition and is kept in the stack once it finishes (detached or hidden, depending on the
retainsetting). Going back, the page being left plays its leave transition and is thrown away. - The incoming page plays its enter transition; a kept page coming back from the stack also plays its enter transition again, whatever the value of
retain. - A page without a transition leaves or arrives instantly.
- For the duration of the transition, the page element's
data-nav-directionattribute carries the direction of the navigation (initial,push,back,forward,replace,traverse); it is removed when the transition finishes. With the same CSS you can write different animations for forward and back:
.page-enter-active { animation: slide-from-right .3s ease; }
.page-leave-active { animation: slide-to-left .3s ease forwards; }
[data-nav-direction="back"].page-enter-active { animation-name: slide-from-left; }
[data-nav-direction="back"].page-leave-active { animation-name: slide-to-right; }
Swiping back. With swipeBack: true, a horizontal touch starting from the left edge of the screen (24px by default) slides the page along with the finger. For the previous page to show underneath while dragging, retain: true is needed; with retain: false the previous page is not in the DOM, so the page background shows underneath, and going back still works on release. Passing 35% of the width (threshold) or a quick flick goes back; otherwise the page settles back into place. If a guard cancels going back, the page slides back and the address is kept. When going back by swiping, the drag is the animation itself; the pages' own transitions do not play. Settings: swipeBack: { edge: 24, threshold: 0.35 }. The touch listener becomes active only on a touch that starts from the edge; it does not affect the page's vertical scrolling or content that scrolls horizontally.
Next step
To learn how services are registered and injected, go on to Introduction to Dependency Injection.