Routing
The JOPS Router is a hash-based router: the part of the URL after # decides which screen is shown (/app/#users). There is one Router per app, declared in a layout, and you reach it from anywhere with Router.singleton().
Declaring routes
Section titled “Declaring routes”<header> <a href="#home">Home</a> <a href="#users">Users</a></header>
<Router jops animation="slide"> <Home jops path="/home" src="/src/Home.js"></Home> <Users jops path="/users" src="/src/Users.js"></Users></Router>Each view inside <Router> is a route, with its path. Link to it with href="#users": no leading slash in the hash, even though the path is /users.
A route can also be a plain view with a layout file: <View jops path="/about" layout="/src/layout/about.html"></View>. A route’s own layout may declare more views with jops tags; they are rendered with the route.
Two ways to navigate
Section titled “Two ways to navigate”JOPS has two navigation paradigms, both familiar from Android:
| Pattern | Mechanism | Instance | Android analogy |
|---|---|---|---|
href="#path" |
flat, hash change | created once, reused | tab navigation |
navigate(path, Class?) |
stack | always a new instance | startActivity() |
replace(path, Class?) |
resets the stack | existing or new | startActivity(FLAG_CLEAR_TOP) |
The views declared inside <Router> are flat routes. Each is created once at startup, kept alive and shown or hidden, like tabs. Switching between them is a plain link:
<a href="#users">Users</a><a href="#details?id=42">Order 42</a>Flat routes can be deep-linked, bookmarked and reloaded: the route in the URL hash is shown on page load.
navigate() creates a new instance of a view, pushes it on a stack on top of the current screen and animates it in. Pass the class whenever the path is not a declared route:
import { View, Router, layout } from "jops-core";import UserDetail from "./UserDetail.js";
class UserRow extends View { onOpenUser() { Router.singleton().navigate(`/user?id=${this.user.id}`, UserDetail); }}The view is destroyed when the user goes back (it gets onBackPressed() first). A link to a flat route closes the whole stack. Stacked views can’t be deep-linked: use flat routes for screens that need a URL.
back(), canGoBack() and replace()
Section titled “back(), canGoBack() and replace()”const router = Router.singleton();
router.back(); // pops a stacked view with animation, otherwise history.back()router.canGoBack(); // true if there is an earlier JOPS history entry to return to
// clear the stack and show a route in the current history entryrouter.replace("/home");// or show a view that is not a declared routerouter.replace("/welcome", Welcome);replace() uses the existing flat route instance when there is one, and otherwise creates a new view from the class you pass. It replaces the current history entry instead of adding one, which suits redirects such as “not signed in, go to /login”.
Query params
Section titled “Query params”Both kinds of navigation can carry query parameters in the hash, and getParams() returns them as an object of strings:
// a static link with params// <a href="#form?tab=profile">Profile</a>
// dynamic navigation with paramsRouter.singleton().navigate("/detail?id=42&mode=edit", DetailView);
// reading themconst { id, mode } = Router.singleton().getParams();Read params in onResume() of a flat route: it runs again when the same route is shown with new params (#details?id=1 → #details?id=2). A stacked view is always a new instance, so it can read them in onLayout().
Navigation and the lifecycle
Section titled “Navigation and the lifecycle”The Router calls onPause(), onResume() and onBackPressed() for you. This table shows exactly what runs on each kind of navigation:
| Navigation | Outgoing view | Incoming view |
|---|---|---|
Route link (href="#users"), Forward to a route |
onPause() |
onResume() |
| Back to the previous route | onBackPressed(), onPause() |
onResume() |
| Same route with new params | none | onResume() |
navigate() (stacked view) |
onPause() |
new instance: onLayout() → onResume() |
| Back from a stacked view | onBackPressed(), then destroyed |
onResume() |
| Route link while stacked views are open | stacked views destroyed (no onPause()) |
onResume() |
- Every route gets
onLayout()at startup, but a route is resumed only when it is shown. At startup only the selected route getsonResume(). onPause()always runs before the incoming view’sonResume().- Closing logic for a stacked view (saving a draft, for example) belongs in
onBackPressed(), the last call before it is destroyed.
The default route
Section titled “The default route”The first route declared in <Router> is the default. It is shown on startup when the URL has no known route hash, and whenever the hash becomes empty later (a href="#" link, or a link to the page itself).
Animation
Section titled “Animation”Set the transition with the animation attribute on <Router>:
| Value | Behaviour |
|---|---|
"none" (default) |
Instant switch |
"slide" |
Forward: the new view slides in from the right. Back: the leaving view slides out to the right. |
"fade" |
Crossfade between the two views |
Animations only run for stacked views: navigate() forward and back() while the stack is not empty. Switching between flat routes is always instant. A view whose layout has several top-level elements also switches instantly, except in screens mode.
Screens mode
Section titled “Screens mode”By default, routes are placed in the Router tag’s parent element and scroll with the page. For app-like layouts (a desktop app, a hybrid app, a fixed toolbar or sidebar), add the screens attribute:
<div class="shell"> <nav class="sidebar">…</nav> <Router jops screens animation="slide" class="content"> <Home jops path="/home" src="/src/Home.js"></Home> <Files jops path="/files" src="/src/Files.js"></Files> </Router></div>In screens mode:
- The
<Router>tag is replaced by<div class="jops-router">(plus the tag’s ownidandclass), styledposition: relative; overflow: hidden. Routes are placed inside it. - Each route, and each view opened with
navigate()orreplace(), is wrapped in a<div class="jops-screen">that fills the container, scrolls on its own and has an opaque background. - A new view covers the current one, which stays alive underneath. After the transition the covered screen is hidden and inert (not painted, not focusable), so it keeps its scroll position and comes back unchanged on Back. Inactive flat routes are hidden the same way.
slideandfademove only the upper screen, and also work for views with several top-level elements.
Styling screens
Section titled “Styling screens”The container needs a size from your CSS. Without a height it has none, and nothing is visible. In a flex column, let it fill the rest:
.jops-router { flex: 1; min-height: 0; --jops-screen-bg: var(--bg); /* screen background, default: Canvas */}
.jops-screen { padding: 0 24px 32px; /* padding inside every screen */}.jops-router and .jops-screen are hooks for your own CSS. The properties the Router sets inline (position, inset, overflow and background on screens; position and overflow on the container) take precedence over your stylesheet, so set the screen background with the --jops-screen-bg variable. Its default, Canvas, is the browser’s page colour in light and dark mode.