Routing
RouterView
RouterView, where route components are placed, nested and named outlets, and how a route component is built; with and without JSX.
RouterView — placing a route's output
The place where route components are shown on screen is RouterView. Put it inside a layout; without JSX it is built with motifComponent(RouterView). The component of a layout route puts a RouterView where its child routes should go. The layout in the example places a RouterView next to its own content (main.detail) and reads the route's meta data with useNavigation():
import { Component, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export default class NotesLayout extends Component {
notes = this.getService(NotesService)!;
nav = useNavigation();
get folder(): Folder {
return this.nav.meta.folder as Folder;
}
override view() {
return (
<div class="notes">
<section class="list">
<h2>{() => FOLDERS[this.folder].title}</h2>
<div class="rows">
{this.notes.list(this.folder).map((note) => (
<a key={note.id} rel="router" href={`/${this.folder}/${note.id}`}>{note.title}</a>
))}
</div>
</section>
<main class="detail">
<RouterView />
</main>
</div>
);
}
}
import { FromService, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export default function NotesLayout() {
const notes = FromService(NotesService)!;
const nav = useNavigation();
const folder = () => nav.meta.folder as Folder;
return (
<div class="notes">
<section class="list">
<h2>{() => FOLDERS[folder()].title}</h2>
<div class="rows">
{notes.list(folder()).map((note) => (
<a key={note.id} rel="router" href={`/${folder()}/${note.id}`}>{note.title}</a>
))}
</div>
</section>
<main class="detail">
<RouterView />
</main>
</div>
);
}
import { FromService, motifOptions, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
const NotesLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
get folder(): Folder {
return this.nav.meta.folder as Folder;
},
onConfigured() {
this.class.add("notes");
},
view() {
const notes = FromService(NotesService)!;
return (
<>
<section class="list">
<h2>{() => FOLDERS[this.folder].title}</h2>
<div class="rows">
{notes.list(this.folder).map((note) => (
<a key={note.id} rel="router" href={`/${this.folder}/${note.id}`}>{note.title}</a>
))}
</div>
</section>
<main class="detail">
<RouterView />
</main>
</>
);
},
});
export default NotesLayout;
import { Component, ComponentBase, motifComponent, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder, type Note } from "../services/notes";
export default class NotesLayout extends Component {
notes = this.getService(NotesService)!;
nav = useNavigation();
get folder(): Folder {
return this.nav.meta.folder as Folder;
}
override view() {
return motifComponent("div", (root: ComponentBase) => {
root.class.add("notes");
const list = motifComponent("section", (section: ComponentBase) => {
section.class.add("list");
const title = motifComponent("h2", (h2: ComponentBase) => h2.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => FOLDERS[this.folder].title))));
const rows = motifComponent("div", (div: ComponentBase) => {
div.class.add("rows");
div.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => this.notes.list(this.folder), (note: Note) => motifComponent("a", {
indexkey: () => note.id,
initializeComponent: (a: ComponentBase) => {
a.attr.add({ rel: "router", href: () => `/${this.folder}/${note.id}` });
a.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
section.controls.add(title, rows);
});
const detail = motifComponent("main", (main: ComponentBase) => {
main.class.add("detail");
main.controls.add(motifComponent(RouterView));
});
root.controls.add(list, detail);
});
}
}
import { ComponentBase, FromService, motifComponent, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder, type Note } from "../services/notes";
export default function NotesLayout() {
const notes = FromService(NotesService)!;
const nav = useNavigation();
const folder = () => nav.meta.folder as Folder;
return motifComponent("div", (root: ComponentBase) => {
root.class.add("notes");
const list = motifComponent("section", (section: ComponentBase) => {
section.class.add("list");
const title = motifComponent("h2", (h2: ComponentBase) => h2.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => FOLDERS[folder()].title))));
const rows = motifComponent("div", (div: ComponentBase) => {
div.class.add("rows");
div.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => notes.list(folder()), (note: Note) => motifComponent("a", {
indexkey: () => note.id,
initializeComponent: (a: ComponentBase) => {
a.attr.add({ rel: "router", href: () => `/${folder()}/${note.id}` });
a.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
section.controls.add(title, rows);
});
const detail = motifComponent("main", (main: ComponentBase) => {
main.class.add("detail");
main.controls.add(motifComponent(RouterView));
});
root.controls.add(list, detail);
});
}
import { ComponentBase, FromService, motifComponent, motifOptions, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder, type Note } from "../services/notes";
const NotesLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
get folder(): Folder {
return this.nav.meta.folder as Folder;
},
onConfigured() {
this.class.add("notes");
},
view() {
const notes = FromService(NotesService)!;
const list = motifComponent("section", (section: ComponentBase) => {
section.class.add("list");
const title = motifComponent("h2", (h2: ComponentBase) => h2.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => FOLDERS[this.folder].title))));
const rows = motifComponent("div", (div: ComponentBase) => {
div.class.add("rows");
div.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => notes.list(this.folder), (note: Note) => motifComponent("a", {
indexkey: () => note.id,
initializeComponent: (a: ComponentBase) => {
a.attr.add({ rel: "router", href: () => `/${this.folder}/${note.id}` });
a.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
section.controls.add(title, rows);
});
const detail = motifComponent("main", (main: ComponentBase) => {
main.class.add("detail");
main.controls.add(motifComponent(RouterView));
});
return [list, detail];
},
});
export default NotesLayout;
import { Component, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export default class NotesLayout extends Component {
notes = this.getService(NotesService);
nav = useNavigation();
get folder() {
return this.nav.meta.folder;
}
view() {
return (
<div class="notes">
<section class="list">
<h2>{() => FOLDERS[this.folder].title}</h2>
<div class="rows">
{this.notes.list(this.folder).map((note) => (
<a key={note.id} rel="router" href={`/${this.folder}/${note.id}`}>{note.title}</a>
))}
</div>
</section>
<main class="detail">
<RouterView />
</main>
</div>
);
}
}
import { FromService, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export default function NotesLayout() {
const notes = FromService(NotesService);
const nav = useNavigation();
const folder = () => nav.meta.folder;
return (
<div class="notes">
<section class="list">
<h2>{() => FOLDERS[folder()].title}</h2>
<div class="rows">
{notes.list(folder()).map((note) => (
<a key={note.id} rel="router" href={`/${folder()}/${note.id}`}>{note.title}</a>
))}
</div>
</section>
<main class="detail">
<RouterView />
</main>
</div>
);
}
import { FromService, motifOptions, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
const NotesLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
get folder() {
return this.nav.meta.folder;
},
onConfigured() {
this.class.add("notes");
},
view() {
const notes = FromService(NotesService);
return (
<>
<section class="list">
<h2>{() => FOLDERS[this.folder].title}</h2>
<div class="rows">
{notes.list(this.folder).map((note) => (
<a key={note.id} rel="router" href={`/${this.folder}/${note.id}`}>{note.title}</a>
))}
</div>
</section>
<main class="detail">
<RouterView />
</main>
</>
);
},
});
export default NotesLayout;
import { Component, motifComponent, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export default class NotesLayout extends Component {
notes = this.getService(NotesService);
nav = useNavigation();
get folder() {
return this.nav.meta.folder;
}
view() {
return motifComponent("div", (root) => {
root.class.add("notes");
const list = motifComponent("section", (section) => {
section.class.add("list");
const title = motifComponent("h2", (h2) => h2.controls.add(motifComponent("text", (node) => node.bindings.text(() => FOLDERS[this.folder].title))));
const rows = motifComponent("div", (div) => {
div.class.add("rows");
div.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => this.notes.list(this.folder), (note) => motifComponent("a", {
indexkey: () => note.id,
initializeComponent: (a) => {
a.attr.add({ rel: "router", href: () => `/${this.folder}/${note.id}` });
a.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
section.controls.add(title, rows);
});
const detail = motifComponent("main", (main) => {
main.class.add("detail");
main.controls.add(motifComponent(RouterView));
});
root.controls.add(list, detail);
});
}
}
import { FromService, motifComponent, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export default function NotesLayout() {
const notes = FromService(NotesService);
const nav = useNavigation();
const folder = () => nav.meta.folder;
return motifComponent("div", (root) => {
root.class.add("notes");
const list = motifComponent("section", (section) => {
section.class.add("list");
const title = motifComponent("h2", (h2) => h2.controls.add(motifComponent("text", (node) => node.bindings.text(() => FOLDERS[folder()].title))));
const rows = motifComponent("div", (div) => {
div.class.add("rows");
div.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => notes.list(folder()), (note) => motifComponent("a", {
indexkey: () => note.id,
initializeComponent: (a) => {
a.attr.add({ rel: "router", href: () => `/${folder()}/${note.id}` });
a.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
section.controls.add(title, rows);
});
const detail = motifComponent("main", (main) => {
main.class.add("detail");
main.controls.add(motifComponent(RouterView));
});
root.controls.add(list, detail);
});
}
import { FromService, motifComponent, motifOptions, RouterView, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
const NotesLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
get folder() {
return this.nav.meta.folder;
},
onConfigured() {
this.class.add("notes");
},
view() {
const notes = FromService(NotesService);
const list = motifComponent("section", (section) => {
section.class.add("list");
const title = motifComponent("h2", (h2) => h2.controls.add(motifComponent("text", (node) => node.bindings.text(() => FOLDERS[this.folder].title))));
const rows = motifComponent("div", (div) => {
div.class.add("rows");
div.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => notes.list(this.folder), (note) => motifComponent("a", {
indexkey: () => note.id,
initializeComponent: (a) => {
a.attr.add({ rel: "router", href: () => `/${this.folder}/${note.id}` });
a.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
section.controls.add(title, rows);
});
const detail = motifComponent("main", (main) => {
main.class.add("detail");
main.controls.add(motifComponent(RouterView));
});
return [list, detail];
},
});
export default NotesLayout;
If navigation stays on the same layout route, the layout stays in place and only the component in the RouterView changes; when moving to another route, the layout is rebuilt as well (see How an address gets on screen).
RouterView has no element of its own (it is a component without an element, a fragment): the route component sits directly where the RouterView is, inside main.detail in the example. Attributes written on the tag, such as class and style, do not show up in the DOM because there is no element to write them on; if you need a container around the output, put the RouterView inside an element (see Root element).
How a route component is built
The default export of the module loaded with control: () => import('./pages/SettingsPage') is used as the route component; with a module that has no default export, the page cannot be built, MJX304 is reported (with MJX127 as the cause) and the error route is shown. A class is built with new SomeClass(app) and a function with fn(app): the first argument is the running Application, and a route component takes no props from a tag. A function that returns an Options API object (or the object itself) becomes a route component too: the function is still called with fn(app), the returned object is turned into a component as in JSX, and ctor receives no props (undefined). The default loaded by Lazy can be one of these forms as well; Lazy builds it without arguments.
If the component is registered in DI (an @Injectable class, or a services.addTransient / addScoped / addSingleton registration), the instance is resolved from the provider, and the registration's lifetime decides the page's lifetime:
| Lifetime | Page |
|---|---|
transient (the default of @Injectable) |
A new page is built on every visit and disposed on leaving. |
scoped |
One page is built in each navigation's scope and disposed on leaving. With keepAlive: true, a cached page outlives its own navigation scope. |
singleton |
A single page for the whole application: like a route given a ready instance, it is not disposed on leaving but detached from the outlet (onDeactivated), and the same instance is attached again on return (onActivated). It is disposed with app.dispose(). |
Named RouterView
By default a route's component sits in the parent route's RouterView named default; for it to go into another outlet, give extend.targetOutlet on the route. A layout can carry several RouterViews; an outlet without a name is default. In the example the second outlet is named page:
<RouterView />
<RouterView name="page" />
const folders = motifComponent(RouterView);
const page = motifComponent(RouterView, { name: "page" });
<RouterView />
<RouterView name="page" />
const folders = motifComponent(RouterView);
const page = motifComponent(RouterView, { name: "page" });
const settings: RouteItem = { path: "/settings", control: () => import("./pages/SettingsPage"), extend: { targetOutlet: "page" } };
const settings = { path: "/settings", control: () => import("./pages/SettingsPage"), extend: { targetOutlet: "page" } };
Each route goes into a single outlet; only one route is shown at a time at the same level, so while /settings is open the default outlet stays empty, and when you return to a folder the page outlet is emptied.
If you do not give app.run a root component, MotifJS automatically places a RouterView with name: 'default' at the root, and the first link of the route table lands there.
If the layout's RouterView has not been built yet (for example because it is inside a section waiting with x-wait), the child route waits and is attached the moment the RouterView is built. If it is not built within 3000 ms in development mode, an MJX301 warning is written; the warning usually means the layout forgot to put a RouterView in.
A RouterView built after a navigation has finished fills up the same way: when the layout swaps its output with a Frame, or closes and opens it with {condition && <RouterView />}, the component of the route on screen goes into the new RouterView. No navigation happens for this; guards, onEntering and onEnter do not run, onShow does. A page disposed together with the old RouterView is built again, and if the old outlet has a leave transition, the new page does not wait for it. keepAlive, ready-instance, singleton and scoped pages are not disposed; the same instance is moved to the new RouterView (onDeactivated, then onActivated). If the RouterView is removed without a new one being put in its place, these pages wait, and no warning is written. If the page cannot be built, MJX304 is reported and the error route is shown.
The page that leaves
The enter and leave transitions of route pages play during navigation too: the page that leaves finishes its leave transition, and then the incoming page is placed and enters (see Styling and Animation).
The new page does not wait for the leaving page to be disposed. The router takes the leaving page off the screen, places the new page, and disposes the leaving page after the new page has come on screen:
- As soon as the page leaves, its bindings and the
context.on/context.onRouterChangedsubscriptions tied to its components stop; content it sent to a slot withTransportToleaves the slot. The element of a page with its own leave transition is removed when the transition finishes, and that of a page without one right away. onDisposing,onDisposedand cleanup work tied withmotif.setDisposable(such as callingdestroy()on library components) run after the new page has come on screen, and see the route that was navigated to. A long cleanup does not delay the arrival of the new page.- The leaving page uses the
scopedinstances of its own navigation until its disposal finishes; those instances are disposed after the page (see Lifetimes and Scopes). keepAlive, ready-instance andsingletonpages, and pages kept in the stack, are not disposed but detached from the outlet.- If a new navigation starts before one has finished, the page that the interrupted navigation built but could not show is disposed; when that route is visited later, the page is built again.
keepAlive, ready-instance andsingletonpages are kept. - The result of
app.navigateandonRouterChangedarrive before the leaving page is disposed.app.dispose()also disposes the pages waiting to be disposed.
Next step
To link pages to each other, go on to Links.