Routing

Links

rel="router" links, RouterLink, and route classes on any component; with and without JSX.

When an in-app link is clicked, the page does not reload; the router changes the address and builds the link of the route chain that changed. There are three ways to do this: writing rel="router" on a plain <a> tag, the RouterLink component that marks the active link with a class, and enableRouterClassing, which opens the same class logic to any component.

With rel="router"

Add rel="router" to a standard <a> tag so that MotifJS catches the click and navigates without a full page reload. href can be a fixed path or a getter:

<a rel="router" href={`/${this.folder}`} class="button button--sm ghost only-narrow">‹ Back</a>
<a rel="router" href="/notes" class="button">All notes</a>
<a rel="router" href={`/${folder()}`} class="button button--sm ghost only-narrow">‹ Back</a>
<a rel="router" href="/notes" class="button">All notes</a>
const back = motifComponent("a", (a: ComponentBase) => {
  a.attr.add({ rel: "router", href: () => `/${this.folder}` });
  a.class.add("button button--sm ghost only-narrow");
  a.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("‹ Back")));
});
const all = motifComponent("a", (a: ComponentBase) => {
  a.attr.add({ rel: "router", href: "/notes" });
  a.class.add("button");
  a.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("All notes")));
});
const back = motifComponent("a", (a: ComponentBase) => {
  a.attr.add({ rel: "router", href: () => `/${folder()}` });
  a.class.add("button button--sm ghost only-narrow");
  a.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("‹ Back")));
});
const all = motifComponent("a", (a: ComponentBase) => {
  a.attr.add({ rel: "router", href: "/notes" });
  a.class.add("button");
  a.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("All notes")));
});
<a rel="router" href={`/${this.folder}`} class="button button--sm ghost only-narrow">‹ Back</a>
<a rel="router" href="/notes" class="button">All notes</a>
<a rel="router" href={`/${folder()}`} class="button button--sm ghost only-narrow">‹ Back</a>
<a rel="router" href="/notes" class="button">All notes</a>
const back = motifComponent("a", (a) => {
  a.attr.add({ rel: "router", href: () => `/${this.folder}` });
  a.class.add("button button--sm ghost only-narrow");
  a.controls.add(motifComponent("text", (node) => node.setText("‹ Back")));
});
const all = motifComponent("a", (a) => {
  a.attr.add({ rel: "router", href: "/notes" });
  a.class.add("button");
  a.controls.add(motifComponent("text", (node) => node.setText("All notes")));
});
const back = motifComponent("a", (a) => {
  a.attr.add({ rel: "router", href: () => `/${folder()}` });
  a.class.add("button button--sm ghost only-narrow");
  a.controls.add(motifComponent("text", (node) => node.setText("‹ Back")));
});
const all = motifComponent("a", (a) => {
  a.attr.add({ rel: "router", href: "/notes" });
  a.class.add("button");
  a.controls.add(motifComponent("text", (node) => node.setText("All notes")));
});

In the function component, notes is the service obtained with FromService, and folder() is a local function that returns the folder.

rel can also be given together with other values (rel="router nofollow"); it is enough for the router token to be in the set. The data-router-link attribute does the same job. Clicks made with Ctrl/Meta/Shift/Alt, links whose target is anything other than _self, and in history mode addresses that go to another origin are not caught. In hash/file mode, href="#/archive" and href="/archive" go to the same place. The catcher listens in the page's document and in every document connected with app.connectDocument; a rel="router" link in content moved into a popup does not reload that window, it navigates the page's router.

A link component that manages the active/match classes automatically: as the current address matches to, it adds the activeClass / exactClass classes. Its children become the link's content; without JSX, the children are given with childs, and the text of a link that is only text with text:

<RouterLink to="/notes" activeClass="active">
  All notes <span class="badge">{() => this.notes.count("notes")}</span>
</RouterLink>
<RouterLink to="/settings" activeClass="active">Settings</RouterLink>
<RouterLink to="/notes" activeClass="active">
  All notes <span class="badge">{() => notes.count("notes")}</span>
</RouterLink>
<RouterLink to="/settings" activeClass="active">Settings</RouterLink>
const all = motifComponent(RouterLink, {
  to: "/notes",
  activeClass: "active",
  childs: [
    motifComponent("text", (node: ComponentBase) => node.setText("All notes ")),
    motifComponent("span", (span: ComponentBase) => {
      span.class.add("badge");
      span.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => this.notes.count("notes"))));
    }),
  ],
});
const settings = motifComponent(RouterLink, { to: "/settings", activeClass: "active", text: "Settings" });
const all = motifComponent(RouterLink, {
  to: "/notes",
  activeClass: "active",
  childs: [
    motifComponent("text", (node: ComponentBase) => node.setText("All notes ")),
    motifComponent("span", (span: ComponentBase) => {
      span.class.add("badge");
      span.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => notes.count("notes"))));
    }),
  ],
});
const settings = motifComponent(RouterLink, { to: "/settings", activeClass: "active", text: "Settings" });
<RouterLink to="/notes" activeClass="active">
  All notes <span class="badge">{() => this.notes.count("notes")}</span>
</RouterLink>
<RouterLink to="/settings" activeClass="active">Settings</RouterLink>
<RouterLink to="/notes" activeClass="active">
  All notes <span class="badge">{() => notes.count("notes")}</span>
</RouterLink>
<RouterLink to="/settings" activeClass="active">Settings</RouterLink>
const all = motifComponent(RouterLink, {
  to: "/notes",
  activeClass: "active",
  childs: [
    motifComponent("text", (node) => node.setText("All notes ")),
    motifComponent("span", (span) => {
      span.class.add("badge");
      span.controls.add(motifComponent("text", (node) => node.bindings.text(() => this.notes.count("notes"))));
    }),
  ],
});
const settings = motifComponent(RouterLink, { to: "/settings", activeClass: "active", text: "Settings" });
const all = motifComponent(RouterLink, {
  to: "/notes",
  activeClass: "active",
  childs: [
    motifComponent("text", (node) => node.setText("All notes ")),
    motifComponent("span", (span) => {
      span.class.add("badge");
      span.controls.add(motifComponent("text", (node) => node.bindings.text(() => notes.count("notes"))));
    }),
  ],
});
const settings = motifComponent(RouterLink, { to: "/settings", activeClass: "active", text: "Settings" });

The link is an <a> element; for another tag, give el. Props:

Prop Effect
to The target path. It is also written as href; in hash/file mode a # is put in front (#/archive), so a link opened in a new tab or copied goes to the right address. href is written according to the router's mode when the link is set up.
showHref If false, no href is written; a click still navigates.
target Written as an attribute. With any value other than _self, the click is left to the browser and the router does not navigate.
text The link's text if it has no children; if it has children, the children are used.
bypass If true, the click does not go to the router; the link opens in the browser like a normal link.

A click made with Ctrl/Cmd/Shift/Alt is left to the browser. On other clicks, RouterLink calls router.navigate(to) and applies the classes based on the match with the current URL:

Class / callback When
activeClass, onActive / offActive When the current path starts with to (by segment; an exact match included). While at /notes/5, to="/notes" is active.
exactClass, onExact / offExact Only when the current path matches to exactly (the query string and # are ignored).

On an exact match both classes are applied together. to="/" is active only at the root path; it does not count as a prefix of every path.

Route classes on any component — enableRouterClassing

The class logic of RouterLink is open to every component: when a RouterClassingSettings is written to motif.options.enableRouterClassing, the component's element gets classes as the current address matches path. It is used for elements that are not <a>, such as a menu item, a tab or a sidebar heading. NavItem is an <li> and takes its text as a child:

import { Component } from "@motifx/core";

export class NavItem extends Component<HTMLLIElement, { path: string }> {
  override onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: this.props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  }

  override view() {
    return <span>{this.childs}</span>;
  }
}
import type { ComponentBase } from "@motifx/core";

export function NavItem(props: { path: string; childs?: ComponentBase[] }) {
  return (
    <li onConfigured={(item) => {
      item.motif.options.enableRouterClassing = {
        to: "all",
        path: props.path,
        activeClass: "is-active",
        exactClass: "is-exact",
      };
    }}>
      <span>{props.childs}</span>
    </li>
  );
}
import { motifOptions } from "@motifx/core";

export const NavItem = (props: { path: string }) => motifOptions({
  el: "li",

  onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  },

  view() {
    return <span>{this.childs}</span>;
  },
});
import { Component, ComponentBase, motifComponent } from "@motifx/core";

export class NavItem extends Component<HTMLLIElement, { path: string }> {
  static override elementTag = "li";

  override onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: this.props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  }

  override view() {
    return motifComponent("span", (span: ComponentBase) => span.controls.add(...(this.childs ?? [])));
  }
}
import { ComponentBase, motifComponent } from "@motifx/core";

export function NavItem(props: { path: string; childs?: ComponentBase[] }) {
  return motifComponent("li", {
    onConfigured: (item: ComponentBase) => {
      item.motif.options.enableRouterClassing = {
        to: "all",
        path: props.path,
        activeClass: "is-active",
        exactClass: "is-exact",
      };
    },
    initializeComponent: (item: ComponentBase) => item.controls.add(motifComponent("span", (span: ComponentBase) => span.controls.add(...(props.childs ?? [])))),
  });
}
import { ComponentBase, motifComponent, motifOptions } from "@motifx/core";

export const NavItem = (props: { path: string }) => motifOptions({
  el: "li",

  onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  },

  view() {
    return motifComponent("span", (span: ComponentBase) => span.controls.add(...(this.childs ?? [])));
  },
});
import { Component } from "@motifx/core";

export class NavItem extends Component {
  static elementTag = "li";

  onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: this.props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  }

  view() {
    return <span>{this.childs}</span>;
  }
}
export function NavItem(props) {
  return (
    <li onConfigured={(item) => {
      item.motif.options.enableRouterClassing = {
        to: "all",
        path: props.path,
        activeClass: "is-active",
        exactClass: "is-exact",
      };
    }}>
      <span>{props.childs}</span>
    </li>
  );
}
import { motifOptions } from "@motifx/core";

export const NavItem = (props) => motifOptions({
  el: "li",

  onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  },

  view() {
    return <span>{this.childs}</span>;
  },
});
import { Component, motifComponent } from "@motifx/core";

export class NavItem extends Component {
  static elementTag = "li";

  onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: this.props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  }

  view() {
    return motifComponent("span", (span) => span.controls.add(...(this.childs ?? [])));
  }
}
import { motifComponent } from "@motifx/core";

export function NavItem(props) {
  return motifComponent("li", {
    onConfigured: (item) => {
      item.motif.options.enableRouterClassing = {
        to: "all",
        path: props.path,
        activeClass: "is-active",
        exactClass: "is-exact",
      };
    },
    initializeComponent: (item) => item.controls.add(motifComponent("span", (span) => span.controls.add(...(props.childs ?? [])))),
  });
}
import { motifComponent, motifOptions } from "@motifx/core";

export const NavItem = (props) => motifOptions({
  el: "li",

  onConfigured() {
    this.motif.options.enableRouterClassing = {
      to: "all",
      path: props.path,
      activeClass: "is-active",
      exactClass: "is-exact",
    };
  },

  view() {
    return motifComponent("span", (span) => span.controls.add(...(this.childs ?? [])));
  },
});

Using it: at the /notes address the first item gets is-active is-exact, and at /notes/5 only is-active:

<ul class="menu">
  <NavItem path="/notes">All notes</NavItem>
  <NavItem path="/archive">Archive</NavItem>
</ul>
const menu = motifComponent("ul", (ul: ComponentBase) => {
  ul.class.add("menu");
  ul.controls.add(
    motifComponent(NavItem, { path: "/notes", childs: [motifComponent("text", (node: ComponentBase) => node.setText("All notes"))] }),
    motifComponent(NavItem, { path: "/archive", childs: [motifComponent("text", (node: ComponentBase) => node.setText("Archive"))] }),
  );
});
<ul class="menu">
  <NavItem path="/notes">All notes</NavItem>
  <NavItem path="/archive">Archive</NavItem>
</ul>
const menu = motifComponent("ul", (ul) => {
  ul.class.add("menu");
  ul.controls.add(
    motifComponent(NavItem, { path: "/notes", childs: [motifComponent("text", (node) => node.setText("All notes"))] }),
    motifComponent(NavItem, { path: "/archive", childs: [motifComponent("text", (node) => node.setText("Archive"))] }),
  );
});
Field Description
path The path to compare.
to Which matches are handled: 'all' (both), 'active', 'exact', 'none'.
activeClass / exactClass Class names separated by spaces; they are toggled with classList.toggle according to the match state.
onActive / offActive / onExact / offExact Callbacks called on every evaluation according to the match state.

The matching rules are the same as for RouterLink (active = a segment prefix or exact; exact = exact; the query and # are ignored). The setting is evaluated once the moment it is written, and then again on every navigation (onRouterChanged); the subscription goes away together with the component. An evaluation error is reported as MJX105. If the router is not set up (no useRouter), it does nothing. The component must have an element; a component without an element (a fragment) has no element to write classes on (see Root element).

Next step

To navigate from code and read the route state, go on to Navigation and Route State.