Overview

Two tools cover leave guards: lockNavigation() for a silent hard block, and onNavigationRequest(fn) for confirm-to-leave. Prefer the request hook when you need a dialog.

With any onNavigationRequest subscriber, navigateTo never auto-commits. Every subscriber must call next() (AND). Prefer onNavigate for post-commit side effects.

lock vs request

API
Use when
Commit model
lockNavigation() Silent hard block navigateTo no-ops; beforeunload on tab close.
onNavigationRequest(fn) Confirm dialog Call next() to allow, omit to cancel.
onNavigate After commit Analytics / scroll — not a guard.

Unsaved text box — ConfirmDialog

Type in the text box to mark the form dirty. navigateTo then opens a mates-ui ConfirmDialog; Leave calls next(), Stay cancels.

import { html, atom, useUtils, renderApp } from 'mates';
import { ConfirmDialog, Button, registerLayoutElements } from 'mates-ui';

registerLayoutElements();

const App = () => {
const { onNavigationRequest, navigateTo, pathAtom } = useUtils();
const dirty = atom(false);
const confirmOpen = atom(false);
let pendingNext = null;
let leaveMessage = 'Leave this page? Unsaved changes will be lost.';

onNavigationRequest(({ to, from, next }) => {
if (!dirty()) {
next();
return;
}
leaveMessage = `Leave ${from} for ${to}? Unsaved changes will be lost.`;
pendingNext = next;
confirmOpen.set(true);
});

return () => html`
<x-col spacing="2">
<p style="margin:0;opacity:.7">path=${pathAtom()} dirty=${dirty()}</p>
<input
type="text"
@input=${(e) => dirty.set(!!e.target.value)}
placeholder="Type to mark unsaved changes…"
style="box-sizing:border-box;width:100%;padding:.5rem .75rem;border-radius:8px;border:1px solid var(--md-color-border);background:var(--md-surface-section);color:var(--md-color-text);font:inherit;"
/>
<x-row spacing="1" wrap>
${Button("navigateTo('/')", { variant: "outlined", on: { click: () => navigateTo('/') } })}
${Button("navigateTo('/about')", { variant: "outlined", on: { click: () => navigateTo('/about') } })}
${Button("Discard", { variant: "filled", on: { click: () => dirty.set(false) } })}
</x-row>

onNavigationRequest confirm (simulated)

Sandbox cannot show window.confirm reliably — toggle allow to simulate next() vs cancel.

import { html, atom, useUtils, renderApp } from 'mates';

const App = () => {
const { onNavigationRequest, navigateTo, pathAtom } = useUtils();
const allow = atom(true);
const log = atom([]);

onNavigationRequest(({ to, from, next }) => {
log.update((rows) => { rows.push(from + ' → ' + to); });
if (allow()) next();
// else cancel (no next)
});

return () => html`
<div class="m-col m-gap">
<p class="hint">path=${pathAtom()} allow=${allow()}</p>
<label class="m-flex m-items-center m-justify-center m-gap-sm">
<input type="checkbox" .checked=${allow()}
@change=${(e) => allow.set(e.target.checked)} />
Call next()
</label>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button @click=${() => navigateTo('/')}>/</button>
<button @click=${() => navigateTo('/about')}>/about</button>
</div>
<div class="card card-body">
${log().map((line) => html`<div class="mono">${line}</div>`)}
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));