Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da24596
[IMP] scheduler: frame-budget between root fibers
ged-odoo Apr 23, 2026
4dd5a7b
[REF] scheduler: defer template rendering to rAF
ged-odoo Apr 25, 2026
8beb692
[IMP] component_node: sync-willStart fast path + count-aware nextTick
ged-odoo Apr 25, 2026
17c605b
[IMP] scheduler: drain reactive effects between render and commit
ged-odoo Apr 25, 2026
9b76e6e
[REF] scheduler: schedule work at microtask instead of rAF
ged-odoo Apr 25, 2026
a387f43
[IMP] scheduler: yield to the browser via MessageChannel on budget ex…
ged-odoo Apr 25, 2026
583f9ab
[REF] scheduler: replace render-time deferral with orphan-cancel
ged-odoo Apr 24, 2026
21b1f22
[FIX] build: dedup owl-core in the umbrella owl bundle
ged-odoo Apr 23, 2026
8a6afb0
[IMP] owl: inject compiler into TemplateSet via static fields
ged-odoo Apr 23, 2026
89e0382
[IMP] owl-core, owl-compiler: declare sideEffects for tree-shaking
ged-odoo Apr 23, 2026
469f4ea
[IMP] owl-runtime: move owl event handler into blockdom defaults
ged-odoo Apr 23, 2026
43e7dcb
[IMP] owl-runtime: drop shouldNormalizeDom from blockdom
ged-odoo Apr 23, 2026
75bc899
[IMP] owl-runtime: move devtools hook registration into App constructor
ged-odoo Apr 23, 2026
7bb559f
[IMP] owl-runtime: declare sideEffects false for tree-shaking
ged-odoo Apr 23, 2026
fcfc56c
[IMP] owl-core, blockdom: tighten any types on OwlError.cause and VNo…
ged-odoo Apr 23, 2026
a6f8e58
[IMP] owl-runtime: replace any with unknown in error_handling internals
ged-odoo Apr 23, 2026
d3cb251
[IMP] owl-runtime: replace any with unknown in props.ts internals
ged-odoo Apr 23, 2026
09b57c5
[IMP] owl-core: audit reactivity TODOs, add tests, drop stale ones
ged-odoo Apr 23, 2026
a53725d
[IMP] tests: drop vitest globals, import describe/test/expect/etc. ex…
ged-odoo Apr 23, 2026
3809a2a
[IMP] lint: broaden scope to all packages, fix surfaced issues
ged-odoo Apr 23, 2026
ea9d938
[IMP] owl-compiler: move jsdom to devDep + optional peerDep
ged-odoo Apr 23, 2026
47bf85c
[IMP] tooling: add husky + lint-staged pre-commit hook
ged-odoo Apr 23, 2026
361c829
[IMP] ci: npm cache + npm audit, drop vulnerable current-git-branch dep
ged-odoo Apr 23, 2026
b7b8c31
[ADD] create-owl: scaffolder for new Owl projects
ged-odoo Apr 23, 2026
0b8941f
[IMP] owl-core: deferred option on computed for lazy-propagating deri…
ged-odoo Apr 24, 2026
0fffa6e
[REM] owl-runtime: remove onWillUpdateProps lifecycle hook
ged-odoo May 6, 2026
4a4e83c
[IMP] owl-compiler, create-owl: switchable AOT/JIT template compilation
ged-odoo May 6, 2026
ba15cfb
[ADD] owl-router: signal-based, plugin-driven routing for Owl 4
ged-odoo May 7, 2026
e6187c8
[ADD] doc: owl-router documentation + multi-package switcher
ged-odoo May 7, 2026
f597875
[REM] owl-runtime: drop runtime delay for renders during render/commit
ged-odoo May 8, 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
6 changes: 4 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ jobs:
node-version: [20.x, 22.x]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm audit --audit-level=high
- run: npm run test
- run: npm run test:types
- run: npm run check-formatting
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run build:playground
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
41 changes: 29 additions & 12 deletions doc/.vitepress/config.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import { defineConfig } from "vitepress";
import { buildNavbar } from "./navbar.mjs";

// Re-enable this group at the top of each per-route sidebar when a second
// package lands (owl-router, owl-orm, …). For now it's noise — a single
// "Owl" entry linking to the same section the user is already in.
// const packageSwitcher = {
// text: "Packages",
// items: [
// { text: "Owl", link: "/v3/owl/" },
// { text: "Router", link: "/v3/owl-router/" },
// { text: "ORM", link: "/v3/owl-orm/" },
// ],
// };
// Prepended to every per-package sidebar so users can jump between packages
// from any doc page. Add new packages here as they land.
const packageSwitcher = {
text: "Packages",
items: [
{ text: "Owl", link: "/v3/owl/" },
{ text: "Router", link: "/v3/owl-router/" },
],
};

const owlRouterSidebar = [
packageSwitcher,
{ text: "Introduction", link: "/v3/owl-router/" },
{ text: "Installation", link: "/v3/owl-router/installation" },
{
text: "Reference",
items: [
{ text: "Overview", link: "/v3/owl-router/reference/overview" },
{ text: "Router", link: "/v3/owl-router/reference/router" },
{ text: "Codec", link: "/v3/owl-router/reference/codec" },
{ text: "Matcher", link: "/v3/owl-router/reference/matcher" },
{ text: "Plugin", link: "/v3/owl-router/reference/plugin" },
{ text: "Components & hooks", link: "/v3/owl-router/reference/components" },
{ text: "History adapters", link: "/v3/owl-router/reference/history" },
],
},
];

const owlSidebar = [
packageSwitcher,
{ text: "Introduction", link: "/v3/owl/" },
{ text: "Overview", link: "/v3/owl/reference/overview" },
{ text: "Installation", link: "/v3/owl/installation" },
Expand Down Expand Up @@ -107,7 +124,7 @@ export default defineConfig({

sidebar: {
"/v3/owl/": owlSidebar,
// Future per-package sidebars go here, each starting with packageSwitcher.
"/v3/owl-router/": owlRouterSidebar,
},

search: {
Expand Down
41 changes: 25 additions & 16 deletions doc/v3/index.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
---
layout: page
title: Redirecting…
head:
- - script
- {}
- "window.location.replace('./owl/');"
- - meta
- http-equiv: refresh
content: "0; url=./owl/"
---

Redirecting to [@odoo/owl docs](./owl/index.md)…

> Once more packages land in the v3 ecosystem (owl-router, owl-orm, …), this
> page can be turned back into a tile landing listing them.
# Owl v3 documentation

The Owl v3 ecosystem ships as a small family of packages, each focused on a
single responsibility. Pick the one you need:

## Packages

### [@odoo/owl](./owl/index.md) — the core framework

Components, signals, computed values, plugins, lifecycle hooks. Everything
you need to build an Owl app. Start here if you're new to Owl 3.

- [Introduction](./owl/index.md)
- [Installation](./owl/installation.md)
- [Reference / Overview](./owl/reference/overview.md)

### [@odoo/owl-router](./owl-router/index.md) — routing

Signal-based routing: pluggable URL codec, plugin-based DI, pattern routes,
`<Link>` and `<RouteSwitch>` components. Optional — install only if your app
needs URL-driven navigation.

- [Introduction](./owl-router/index.md)
- [Installation](./owl-router/installation.md)
- [Reference / Overview](./owl-router/reference/overview.md)
93 changes: 93 additions & 0 deletions doc/v3/owl-router/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Owl Router

`@odoo/owl-router` is a small, generic routing layer for Owl 4. It is built
around three ideas:

- **Signals own the state.** The current state and URL are reactive values you
read with `router.state()` and `router.url()`. Components, computed values,
and effects subscribe in the standard way — there is no separate event bus.
- **Plugins own the lifetime.** A `RouterPlugin` is registered when you mount
the app; the router is created on `setup` and disposed when the plugin
manager is destroyed. Tests get a fresh router per test for free.
- **A codec owns the URL shape.** The router does not assume anything about
state shape. A `RouterCodec<TState>` you supply turns state into a URL and
back. Built-in codecs cover pattern routes (`/users/{id:int}`); custom
codecs handle anything more elaborate.

## Quick example

```js
import { mount, Component, xml } from "@odoo/owl";
import { RouterPlugin, Link, RouteSwitch, createMatcher, useRouter } from "@odoo/owl-router";

const codec = createMatcher({
home: "/",
user: "/users/{id:int}",
about: "/about",
});

class Home extends Component {
static template = xml`<h1>Home</h1>`;
}
class User extends Component {
static template = xml`<h1>User <t t-out="this.router.state().params.id"/></h1>`;
router = useRouter();
}
class NotFound extends Component {
static template = xml`<h1>Not found</h1>`;
}

class App extends Component {
static components = { Link, RouteSwitch, Home, User, NotFound };
static template = xml`
<nav>
<Link href="'/'">Home</Link>
<Link href="'/users/42'">User 42</Link>
<Link href="'/about'">About</Link>
</nav>
<RouteSwitch select="(s) => s.name">
<t t-set-slot="home"><Home/></t>
<t t-set-slot="user"><User/></t>
<t t-set-slot="default"><NotFound/></t>
</RouteSwitch>`;
}

await mount(App, document.body, {
plugins: [RouterPlugin],
config: { codec },
});
```

That's the full surface for a typical SPA. Drop the `RouteSwitch` if you want
to drive rendering imperatively from `router.state()`; drop `Link` if you
prefer raw `<a>` tags plus [`useLinkInterceptor`](reference/components.md#uselinkinterceptor).

## When to use it

Owl Router is generic on purpose. It does not encode any application
conventions — no breadcrumb stack, no `/odoo` prefix, no notion of "actions".
Everything you can express via a codec lives in your codec. This makes the
package suitable for:

- **Single-page apps** with stable URL shapes (pattern routes via
[`createMatcher`](reference/matcher.md)).
- **Apps with bespoke URL grammars** (write a [custom codec](reference/codec.md)
and plug it in).
- **Tests and SSR**, by swapping the [`HistoryAdapter`](reference/history.md)
for an in-memory implementation.

If you need something on top of these primitives — breadcrumb-aware action
stacks, locked URL keys, route guards — see the per-page reference for
patterns that compose what's already there.

## Read on

- [Installation](./installation.md) — install and wire the plugin.
- [Reference / Overview](./reference/overview.md) — full list of exports.
- [Router](./reference/router.md) — the main class, navigation methods.
- [Codec](./reference/codec.md) — how state ↔ URL works, middlewares.
- [Plugin](./reference/plugin.md) — registering with an app, `useRouter`.
- [Matcher](./reference/matcher.md) — pattern routes.
- [Components](./reference/components.md) — `Link`, `RouteSwitch`,
`useLinkInterceptor`.
- [History adapters](./reference/history.md) — browser, memory, custom.
92 changes: 92 additions & 0 deletions doc/v3/owl-router/installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Installation

`@odoo/owl-router` is published alongside `@odoo/owl`. Install the two together
in any Owl 4 project:

```bash
npm install @odoo/owl @odoo/owl-router
```

The router builds on Owl's plugin system, so it is registered like any other
plugin — pass it to `mount()` (or `providePlugins()` for component-level
scope) along with the codec describing your URL shape.

## Minimal setup

```js
import { mount, Component, xml } from "@odoo/owl";
import { RouterPlugin, createMatcher, useRouter } from "@odoo/owl-router";

const codec = createMatcher({
home: "/",
user: "/users/{id:int}",
});

class App extends Component {
static template = xml`
<div>
<p>Current route: <t t-out="this.router.state().name"/></p>
<button t-on-click="() => this.router.push({ name: 'user', params: { id: 42 } })">
Go to user 42
</button>
</div>`;

router = useRouter();
}

await mount(App, document.body, {
plugins: [RouterPlugin],
config: { codec },
});
```

The configuration that `RouterPlugin` understands:

| Key | Type | Description |
| --------- | ---------------- | -------------------------------------------------------------- |
| `codec` | `RouterCodec` | **Required.** State ↔ URL codec. |
| `history` | `HistoryAdapter` | Optional. Defaults to a `BrowserHistoryAdapter`. |
| `reload` | `() => void` | Optional. Called when navigation specifies `{ reload: true }`. |

## Tests

For tests (or SSR), pass a [`MemoryHistoryAdapter`](./reference/history.md#memoryhistoryadapter)
so the router never touches `window.history`:

```js
import { MemoryHistoryAdapter, RouterPlugin } from "@odoo/owl-router";

const history = new MemoryHistoryAdapter({ initialUrl: "/users/42" });

await mount(App, fixture, {
plugins: [RouterPlugin],
config: { codec, history },
});
```

Each test gets its own router because each `mount()` creates its own plugin
manager — there is no module-level singleton to reset between runs.

## Component-scoped router

If only a subtree of your app needs routing, register the plugin via
`providePlugins()` instead of at app level:

```js
import { providePlugins } from "@odoo/owl";
import { RouterPlugin, createMatcher } from "@odoo/owl-router";

class Wizard extends Component {
static template = xml`<WizardSteps/>`;
static components = { WizardSteps };

setup() {
providePlugins([RouterPlugin], {
codec: createMatcher({ step1: "/wizard/1", step2: "/wizard/2" }),
});
}
}
```

The router is destroyed (and stops listening to `popstate`) when `Wizard`
unmounts.
Loading