Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e8ca70e
scaffold micro Lit example
oddcelot May 5, 2026
362da0d
bump lit to ^3.3.2 in micro example
oddcelot May 5, 2026
42ecf30
add Lit header/footer components and main layout
oddcelot May 5, 2026
7c32ae0
add Lit pages for micro example
oddcelot May 5, 2026
af9e606
wire Lit example to Texivia via router and app-shell
oddcelot May 5, 2026
06ea2ec
pass page body to layout as template prop
oddcelot May 5, 2026
4c73adb
attach texivia listener before router.start in app-shell
oddcelot May 5, 2026
e46fda5
drop tsc step from Lit example build script
oddcelot May 5, 2026
fd0115d
add package-lock.json for Lit micro example
oddcelot May 5, 2026
400aa6b
add README for micro Lit example
oddcelot May 5, 2026
2ea9675
document Lit integration in README
oddcelot May 5, 2026
55efa00
add lit and web-components to npm keywords
oddcelot May 5, 2026
1c71b39
make main-layout a plain template function
oddcelot May 5, 2026
842965f
update example README for layout-as-function refactor
oddcelot May 5, 2026
d2c037c
emit dist/texivia.d.ts in the build
oddcelot May 5, 2026
00902bf
restore tsc step in Lit example build
oddcelot May 5, 2026
d7aa0c9
format Lit example with prettier
oddcelot May 5, 2026
bb16d97
clarify Light DOM rationale in Lit example app-shell
oddcelot May 7, 2026
62439e7
seed initial locale from navigator.language in Lit example
oddcelot May 7, 2026
108dd2b
document Light DOM requirement for Lit integration
oddcelot May 7, 2026
98e18e0
strip locale subtag in Lit example router redirect
oddcelot May 7, 2026
adeeb38
stop login form from triggering full page POST
oddcelot May 7, 2026
db2802f
sync Lit micro example README with current code
oddcelot May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ npm install texivia-router
| Svelte | – | – | ✓ | – | **✓** |
| React | ✓ | – | ✓ | ✓ | **✓** |
| Vue | – | ✓ | ✓ | – | **✓** |
| Lit | – | – | – | – | **✓** |
| Angular | – | – | ✓ | – | **✓** |
| Vanilla | – | – | ✓ | – | **✓** |

Expand Down Expand Up @@ -131,6 +132,102 @@ Texivia doesn't need a nested routing concept. Use your framework's composition

Layouts are components, not router config. This keeps the router simple and your layouts flexible.

## Lit 3

Type the router with a render-function `View` so each route's `view` is a `(params) => TemplateResult`. A single `<app-shell>` LitElement owns the navigation listener and re-renders by swapping the current view:

```typescript
// src/router.ts
import { Router } from 'texivia-router';
import type { TemplateResult } from 'lit';
import { renderLanding } from './pages/landing';
import { renderUserProfile } from './pages/user-profile';
import { renderNotFound } from './pages/not-found';

export type View = (params: Record<string, string>) => TemplateResult;

export const router = new Router<View>([
{ path: '/', view: renderLanding },
{ path: '/users/{id:\\d+}', view: renderUserProfile },
{ path: '*', view: renderNotFound },
]);
```

```typescript
// src/app-shell.ts
import { LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { router, type View } from './router';
import { renderLanding } from './pages/landing';

@customElement('app-shell')
export class AppShell extends LitElement {
// Light DOM keeps the clicked <a> as event.target so the router's
// document-level closest('a') resolves it; Shadow DOM would retarget
// target to the host. Also lets global CSS apply.
createRenderRoot() { return this; }

@state() private view: View = renderLanding;
@state() private params: Record<string, string> = {
locale: navigator.language.split('-')[0],
};

private onNavigate = (e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail?.view) this.view = detail.view;
this.params = detail?.params ?? {};
};

connectedCallback() {
super.connectedCallback();
// Attach listener BEFORE start() — routes without a handler dispatch
// synchronously, so a late listener misses the initial event on deep links.
document.addEventListener('texivia', this.onNavigate);
router.start();
}

disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('texivia', this.onNavigate);
router.stop();
}

render() {
return this.view(this.params);
}
}
```

Each page exports a LitElement and a render fn that the router map references:

```typescript
// src/pages/landing.ts
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('page-landing')
export class PageLanding extends LitElement {
createRenderRoot() { return this; }
@property() locale = 'en';

render() {
return html`<h1>Welcome</h1><p>Locale: ${this.locale}</p>`;
}
}

export const renderLanding = (p: Record<string, string>) =>
html`<page-landing .locale=${p.locale}></page-landing>`;
```

For programmatic navigation, import the router from anywhere:

```typescript
import { router } from './router';
router.navigate('/dashboard');
```

Plain `<a>` tags inside Lit templates are intercepted automatically — no `<Link>` wrapper needed, **provided the host renders in Light DOM** (`createRenderRoot() { return this; }`). Shadow DOM retargets click events to the host, so `<a>` inside a shadow root is invisible to the document-level click handler. See [`examples/lit/micro`](examples/lit/micro) for a complete example with layouts, parameterized routes, and a 404 fallback.

## Vue 3

```typescript
Expand Down
24 changes: 24 additions & 0 deletions examples/lit/micro/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
87 changes: 87 additions & 0 deletions examples/lit/micro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Micro — Lit + Texivia Router

Minimal Lit 3 example using `texivia-router` for client-side routing. Demonstrates locale-prefixed routes, parameterized paths, redirects, and a catch-all 404 page.

## Routes

| Path | View |
| ------------------------------ | ------------------------------------------------------- |
| `/` | Redirects to `/{locale}/` based on `navigator.language` |
| `/{locale}/` | Landing |
| `/{locale}/login` | Login |
| `/{locale}/users/{id}/profile` | UserProfile |
| `/{locale}/about` | About |
| `/{locale}/imprint` | Imprint |
| `/{locale}/contact` | Contact |
| `*` | NotFound |

## How It Works

`Router<View>` is parameterized over a render function — `View = (params) => TemplateResult`. Each page exports a LitElement plus a `renderXxx` function that the route map references:

```ts
// src/router.ts
export type View = (params: Record<string, string>) => TemplateResult;

export const router = new Router<View>([
{ path: '/', handler: () => `/${navigator.language.split('-')[0]}/` },
{ path: '/{locale}/', view: renderLanding },
{ path: '/{locale}/users/{id:\\d+}/profile', view: renderUserProfile },
// ...
{ path: '*', view: renderNotFound },
]);
```

`<app-shell>` is the root LitElement. It listens for the `texivia` event, swaps the current view, and re-renders:

```ts
// src/app-shell.ts
@customElement('app-shell')
export class AppShell extends LitElement {
createRenderRoot() {
return this;
} // light DOM

@state() private view: View = renderLanding;
@state() private params: Record<string, string> = {
locale: navigator.language.split('-')[0],
};

private onNavigate = (e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail?.view) this.view = detail.view;
this.params = detail?.params ?? {};
};

connectedCallback() {
super.connectedCallback();
document.addEventListener('texivia', this.onNavigate);
router.start();
}

disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('texivia', this.onNavigate);
router.stop();
}

render() {
return this.view(this.params);
}
}
```

## Lit-specific notes

**Light DOM (`createRenderRoot() { return this }`).** Composed click events bubble out of shadow DOM fine — but `event.target` is retargeted to the shadow host. Texivia's interceptor calls `event.target.closest('a')`, which then walks up from `<app-shell>` instead of from the actual anchor and finds nothing. Light DOM keeps the clicked `<a>` as the literal `event.target`, so `closest('a')` resolves it. Light DOM also lets global `app.css` apply without `static styles`. Shadow DOM is workable only if you fork the router to read `event.composedPath()`.

**Listener before `start()`.** Routes without a handler complete `_navigate` synchronously inside `router.start()` and dispatch the `texivia` event before control returns. Attach the listener first, then call `router.start()`, otherwise the initial event is missed on direct deep-link loads.

**Layouts as plain template functions.** `mainLayout(locale, body)` is a function returning a `TemplateResult`, not a custom element. Layouts have no state or behavior, so wrapping them in a `LitElement` only adds ceremony — and forcing children through a custom element means dealing with `<slot>` (which doesn't project in light DOM). A function is shorter, has no render root, and composes naturally inside `html\`...\``.

## Setup

```sh
npm install
npm run dev
```
12 changes: 12 additions & 0 deletions examples/lit/micro/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Micro Texivia Lit Example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Loading