Query

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

App.tsx

import './App.css';

import { Component, get } from '@expressive/react';
import { Route, Router } from '@expressive/router';
import type { ReactNode } from 'react';

const BOOKS = [
  { title: 'Analysis I', year: 2016, tag: 'math' },
  { title: 'Concrete Mathematics', year: 1994, tag: 'math' },
  { title: 'Structure and Interpretation', year: 1985, tag: 'code' }
];

export default () => (
  <Router>
    <Route as={Frame}>
      <Route as={Catalog} />
    </Route>
  </Router>
);

const Frame = (props: { children?: ReactNode }) => (
  <div className="container">
    <h1>Query</h1>
    <p>
      <code>query</code> is not a string - it is a reactive map of the search
      params. Reading a key subscribes to that key alone; writing or deleting one
      navigates, pushing a history entry exactly as <code>goto</code> would.
    </p>
    {props.children}
    <small>
      The URL stays the only source of truth, so Back walks the sort and filter
      changes in order - no separate state to keep in step with it.
    </small>
  </div>
);

class Catalog extends Component {
  route = get(Route);
  router = get(Router);

  render() {
    const { route, router } = this;
    const { query } = route;
    const tag = query.get('tag');
    const sort = query.get('sort') ?? 'title';

    const books = BOOKS.filter((book) => !tag || book.tag === tag).sort((a, b) =>
      sort === 'year' ? a.year - b.year : a.title.localeCompare(b.title)
    );

    return (
      <section className="catalog">
        <div className="controls">
          <button onClick={() => query.set('sort', 'title')}>Sort title</button>
          <button onClick={() => query.set('sort', 'year')}>Sort year</button>
          <button onClick={() => query.set('tag', 'math')}>Only math</button>
          <button onClick={() => query.delete('tag')}>Clear tag</button>
        </div>

        <ul className="books">
          {books.map(({ title, year }) => (
            <li key={title}>
              {title} <small>{year}</small>
            </li>
          ))}
        </ul>

        <footer>
          <code>{router.url}</code>
          <button onClick={() => router.back()}>Back</button>
        </footer>
      </section>
    );
  }
}

App.css

.catalog {
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  width: 100%;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.controls button {
  font-size: var(--t-sm);
}

.books li {
  display: flex;
  justify-content: space-between;
  background: var(--bg);
  border: 1px solid var(--border);
  cursor: default;
}

.books li:hover {
  background: var(--bg);
}

.catalog footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}

.catalog footer code {
  overflow-x: auto;
  color: var(--accent);
}