Guards

Guards demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import State, { Provider } from '@expressive/react';
import { Link, Route, Router } from '@expressive/router';
import type { ReactNode } from 'react';

const DOCS = ['charter', 'ledger'];

class Session extends State {
  user: string | null = null;

  toggle() {
    this.user = this.user ? null : 'Ada';
  }
}

const session = new Session();
const router = new Router();

const vet = async () => {
  if (!session.user) return '/login';

  await new Promise((resolve) => setTimeout(resolve, 600));

  return DOCS.includes(router.path.split('/').pop()!) ? '' : null;
};

export default () => (
  <Provider for={{ session, router }}>
    <Route as={Frame}>
      <Route as={Lobby} />
      <Route to="login" as={Login} />
      <Route to="vault/:doc" as={Doc} redirect={vet} fallback={<p className="gate">checking…</p>} />
      <Route default as={NotFound} />
    </Route>
  </Provider>
);

const Frame = (props: { children?: ReactNode }) => (
  <div className="container">
    <h1>Guards</h1>
    <p>
      A <code>redirect</code> function is an entry guard, run when its route is
      matched, and one function covers every verdict: a path sends the visitor
      elsewhere, nothing at all lets the render through, and <code>null</code>{' '}
      cedes the path - the scope falls through to its <code>default</code> rather
      than admitting whether anything was there.
    </p>
    <nav className="nav">
      <Link to="/">Lobby</Link>
      <Link to="/vault/charter">Charter</Link>
      <Link to="/vault/secrets">Secrets</Link>
    </nav>
    <div className="view">{props.children}</div>
    <small>
      Signed out, both vault links land on the sign-in page. The verdict is cached
      for the space it was decided in, so return to the lobby between attempts -
      and since the check is async, the route’s <code>fallback</code> covers the
      wait.
    </small>
  </div>
);

const Lobby = () => {
  const { user, toggle } = Session.get();

  return (
    <p className="gate">
      {user ? `Signed in as ${user}` : 'Signed out'} -{' '}
      <button onClick={toggle}>{user ? 'Sign out' : 'Sign in'}</button>
    </p>
  );
};

const Login = () => (
  <p className="gate">
    The guard sent you here. Sign in from the lobby, then try the vault again.
  </p>
);

const Doc = () => {
  const { match } = Route.get();

  return <p className="doc">Reading {match?.doc}</p>;
};

const NotFound = () => <p className="gate">No such document in the vault.</p>;

App.css

.nav {
  display: flex;
  gap: var(--s3);
  width: 100%;
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--accent-soft);
}

.nav a {
  cursor: pointer;
}

.view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-height: 4rem;
}

.gate,
.doc {
  margin: 0;
  padding: var(--s3) var(--s4);
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
}

.doc {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}