Theme
Design your own
On this page (11)

§Load it

dist/index.js imports nothing and starts nothing when imported - no listener until start(). Load it as a module from jsDelivr, or install it for a bundler. It brings no renderer: the views are your markup, the DOM work goes through the components' df$.

<script type="module">
  import { createRouter } from 'https://cdn.jsdelivr.net/npm/defuss-dom-router@0.1.1/dist/index.js';
</script>
// bun add defuss-dom-router
import { createRouter } from 'defuss-dom-router';

§Views are sections, a route un-hides one

The scaffolds keep every view in the markup - a <section> each, all but the first hidden - so the page reads before any script runs and a view's components keep their state when it hides. Links are real anchors with data-router-link and a route in the fragment (href="#/messages/2"); attachLinks() handles their clicks and leaves modified clicks, downloads and other targets to the browser. The commit un-hides the view the route names and marks the current link. A :param names a record, a lone * catches everything else:

§A mailbox with routes

Routes: #/ (inbox), #/archive, #/messages/:id and * (not found - try message 9 under Archive). The line under the title shows what the route matched. In this preview the router cannot own the address (see Inside a docs preview below) - the same routes match the frame's hash.

what the mailbox uses
Call / markupWhat it does
createRouter({ mode: 'hash', routes, prepare })Validates and copies the configuration; routes match in order, the first one wins
{ id: 'message', path: '/messages/:id' }A named route with a parameter: to.params.id, decoded once (an encoded / stays data)
{ id: 'missing', path: '*' }Everything else - last, since the first match wins
prepare({ to, signal }) → { commit }Build the next view off the page (fetch with signal - a newer navigation aborts it), show it in commit
router.attachLinks(root)One delegated click listener for a[data-router-link] under root
await router.start()Takes the window and runs the first navigation - the address the page was opened with

§Hash or history

Hash mode keeps the route after the #: the server always serves the same file, so any static host works - this site is served by GitHub Pages, which has no fallback for deep paths, so every scaffold uses it. History mode (the default) gives clean paths like /messages/2; your server then answers every app path with the app's HTML, and basePath names the folder the app lives in. router.href({ id, params, query }) builds a link that is right in both modes.

const router = createRouter({
  basePath: '/app',
  routes: [{ id: 'message', path: '/messages/:id' }, { id: 'missing', path: '*' }],
  prepare: ({ to }) => ({ commit: () => show(to) }),
  afterCommit: ({ to }) => {
    document.title = df$('#mail-subject').text();
    df$('main h1').get(0)?.focus(); // title, focus and ARIA belong after the commit
  },
});
router.href({ id: 'message', params: { id: '2' } }); // '/app/messages/2'

§Results, not exceptions

navigate() never rejects - it resolves to exactly one result, so a script knows whether the screen changed:

navigation results
statusMeaning
committedHistory was written (or adopted) and the view committed
unchangedSame address and state as the current entry - nothing ran
blockedA guard or beforeLeave returned exactly false; history is untouched or was walked back
supersededA newer navigation took over before this one committed; its prepared view was disposed, never shown
rejectedInvalid input, or a router not started or destroyed
errorA guard, prepare(), the history write or commit() failed

§Guards and leaving

Guards run before pushState, so a veto leaves history as it was; a vetoed Back is walked forward again. A view's beforeLeave asks when its route changes - an Alert Dialog makes the question; only an exact false blocks.

const ask = () => new Promise((resolve) => {
  const dialog = df$('#leave-draft').get(0); // <dialog class="alert-dialog"> with Stay / Leave buttons
  dialog.addEventListener('close', () => resolve(dialog.returnValue === 'leave'), { once: true });
  dialog.showModal();
});
const router = createRouter({
  mode: 'hash',
  routes,
  prepare: ({ to }) => ({
    commit: () => show(to),
    beforeLeave: () => (draft.value.trim() ? ask() : true),
  }),
});
router.beforeEach(({ to }) => to.routeId !== 'admin' || signedIn); // false blocks

§Navigating from a script

A script that holds the router calls await router.navigate('#/messages/2'). Code that does not - the scaffolds' older wiring, a data grid's row handler - changes the fragment: location.hash = '/issues/ENG-142'. The router follows hashchange like a link, and so does the docs preview's fallback. Never call history.pushState yourself while a router owns the window: entries it did not write carry none of its metadata, and a blocked Back cannot be walked back across them.

§With the components

§Routing and translation

They do not depend on each other: a language switch does not navigate, a navigation does not re-render the texts. Because views are hidden rather than rendered, every translated region stays bound when it hides (Translation). Text a commit derives from translated markup - the Admin Dashboard's breadcrumb copies the current sidebar label - is read again on defuss-i18n:change.

§Inside a docs preview

The router routes HTTP(S) addresses of its own window. Every example on this site runs in a sandboxed about:srcdoc frame, which has no such address: start() resolves rejected with invalid-url. The frame's links still change its fragment, and the router's pure resolve() works without a window when a second router is given a baseUrl - so the examples match the same routes there. Open a scaffold or a Website template full screen (each has one) to see the router own a real address, with Back, Forward and deep links.

if ((await router.start()).status === 'rejected') {
  const preview = createRouter({ mode: 'hash', routes, baseUrl: 'https://preview.invalid/' });
  const follow = () => show(preview.resolve(location.hash.startsWith('#/') ? location.hash : '#/'));
  addEventListener('hashchange', follow);
  follow();
}

§Where it is used

the scaffolds' routes
ScaffoldRoutes
Admin Dashboard#/, #/orders, #/customers, #/settings
Messenger#/ (the list), #/chats/:id
Issue Tracker#/, #/my-issues, #/inbox, #/cycles, #/projects, #/issues/:id
Notes & Docs#/, #/pages/:slug - new pages too
Status Page#/, #/history, #/manage, #/incidents/:id
Desktop#/apps/:app, #/files/:name, #/browse/:site - what a launcher opens
Document Editor#/docs/:id
the Website templates' routes
TemplateRoutesWhat a route does
Landing Page#/features, #/pricing, #/faqScrolls to the section
News#/section/:topicShows the section's stories
Blog#/category/:category, #/archive/:year/:monthFilters the posts
Article#/articles/:slugprepare() builds the article from a data block
Comments#/comments/:id, ?sort=Highlights the comment, orders the thread
Company#/team, #/jobs/:department, #/pressThe site's pages, a department filter
Events & Promotions#/track/:track, #/sessions/:idFilters the schedule, opens a session
Media#/photos/:nOpens the slideshow on the photo
Shop#/cart, #/checkout, #/order/:idOne step of the purchase
Account & States#/profile, #/settings, #/onboarding, #/statesOne part of the account
Docs & Help#/docs/:pageLink, breadcrumb, title and pager follow
Search & Navigation#/?q=&category=&sort=&page=&tag=The search is the query string

Comments, ideas or improvements? Edit this page's source on GitHub