Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ and this project adheres to Semantic Versioning.

## [Unreleased]

### Added (Unreleased)

- **`@bquery/bquery/view`** — declarative enter/leave/move transitions ([#137](https://github.com/bQuery/bQuery/issues/137)). New companion attributes `bq-transition`, `bq-in`, `bq-out`, `bq-transition-duration`, `bq-transition-easing` drive enter/leave animations on `bq-if` / `bq-show`, and `bq-animate="flip"` drives FLIP move animations when `bq-for` items reorder. The layer delegates to the existing `motion` engine (Web Animations + FLIP), skips the initial paint, defers removal until the leave finishes, is race-safe on rapid toggles, and honours `prefers-reduced-motion`.
- **`@bquery/bquery/view/compiler`** — optional, build-tool-agnostic compiler ([#138](https://github.com/bQuery/bQuery/issues/138)). `compileViews()`, `compileToModule()`, `compileExpression()`, `emitModule()`, and the dependency-free CLI (`runCompileCli` / `compileFiles`, `bquery-view-compile`) pre-parse `bq-*` expressions into optimized, `with`-free update functions. New runtime hooks `registerCompiledExpressions()` / `clearCompiledExpressions()` (exported from `@bquery/bquery/view`) let the runtime use the precompiled functions, skipping the `new Function()` evaluator (and its `'unsafe-eval'` requirement). The runtime evaluator stays the default; un-compilable expressions transparently fall back to it, so both paths are behaviourally identical.

### Changed (Unreleased)

- **`@bquery/bquery/view`** — `view` is now **targeting Stable in 1.15.0** ([#136](https://github.com/bQuery/bQuery/issues/136)). The directive set and expression grammar are frozen for one minor cycle, and a per-directive SSR support matrix is published in the [View guide](https://bquery.js.org/guide/view).

### Fixed (Unreleased)

- **`@bquery/bquery/view`** — `bq-for` duplicate-key handling is resolved ([#136](https://github.com/bQuery/bQuery/issues/136)): colliding keys now fall back to a deterministic, referentially-stable composite key so duplicate rows reuse their DOM across re-renders, and the duplicate-key warning is dev-only and emitted once per offending key instead of on every reactive update.
- **`@bquery/bquery/view`** — object-expression shorthand is resolved ([#136](https://github.com/bQuery/bQuery/issues/136)): `bq-class="{ active }"` (and `bq-style` / `bq-aria` object syntax) now behaves like JS object shorthand (`{ active: active }`) instead of silently dropping the property.

## [1.14.2] - 2026-06-26

### Fixed (1.14.2)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ import { storyHtml, when } from '@bquery/bquery/storybook';
| **Platform** | Stable | Storage, cache, cookies, page metadata, announcers, and shared runtime config |
| **Router** | Stable | SPA routing, constrained params, redirects, guards, `useRoute()`, and `<bq-link>` |
| **Store** | Stable | Signal-based state management, persistence, migrations, action hooks, and plugin lifecycle helpers |
| **View** | Beta | Declarative DOM bindings with `bq-*` directives for content, classes, forms, errors, ARIA, and plugins |
| **View** | Beta | Declarative DOM bindings with `bq-*` directives, declarative enter/leave/move transitions, and an optional expression precompiler (targeting Stable in 1.15.0) |
| **Forms** | Beta | Reactive form state with sync/async validation and submit handling |
| **i18n** | Beta | Reactive locales, interpolation, pluralization, lazy loading, and Intl formatting |
| **A11y** | Beta | Focus traps, live-region announcements, roving tabindex, skip links, and audits |
Expand Down
14 changes: 14 additions & 0 deletions bin/bquery-view-compile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node
/**
* CLI launcher for the optional bQuery view compiler.
*
* Thin wrapper around `runCompileCli` from the built `view/compiler` entry.
* Runs against the published `dist/` build; for local development run the
* compiler programmatically from `src/view/compiler`.
*
* Usage: bquery-view-compile [options] <file...>
*/
import { runCompileCli } from '../dist/view-compiler.es.mjs';

const exitCode = await runCompileCli(process.argv.slice(2));
process.exit(exitCode);
132 changes: 129 additions & 3 deletions docs/guide/view.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: View
---

::: tip What's new in 1.14.0
View gained `parseDirective` / `ParsedDirective`, new directives `bq-once`, `bq-init`, `bq-pre`, `bq-cloak`, `bq-html-safe`, `bq-memo`, and the full `bq-on` modifier system in 1.14.0. See the [1.14.0 release notes](/release-notes/1.14#additive-module-expansions).
::: tip What's new in 1.15.0
View is **targeting Stable in 1.15.0**: the directive contract is frozen, the documented `bq-for` duplicate-key and object-expression edge cases are resolved, declarative [enter/leave/move transitions](#transitions) bind the `motion` engine to `bq-if`/`bq-show`/`bq-for`, and an [optional compiler](#optional-compiler-build-step) precompiles `bq-*` expressions. See [Stability](#stability-targeting-stable-in-1150).
:::

The view module provides declarative DOM bindings similar to Vue/Svelte templates, but without requiring a compiler. Bindings are evaluated at runtime using bQuery's reactive system. Internally, the view module is now split into focused submodules while the public API remains unchanged.
Expand All @@ -13,6 +13,39 @@ import { mount } from '@bquery/bquery/view';
import { signal, computed } from '@bquery/bquery/reactive';
```

## Stability: targeting Stable in 1.15.0

`view` is the declarative rendering layer and has been **Beta**. Its directive contract grew materially in 1.14.0 (`bq-once`, `bq-init`, `bq-pre`, `bq-cloak`, `bq-html-safe`, `bq-memo`, and the full `bq-on` modifier system). The work to graduate it is tracked in [#136](https://github.com/bQuery/bQuery/issues/136): freeze the directive set and grammar for one minor cycle, resolve the documented parser edge cases, and publish a versioned directive reference with per-directive SSR support. Promotion to **Stable** then follows one full minor cycle with the surface frozen.

### Exit criteria

- [x] **Directive set + expression grammar frozen for one minor** — see [Frozen directive reference](#frozen-directive-reference-1150) below; no additive directives land during the freeze.
- [x] **`bq-for` duplicate-key edge case resolved** ([#136](https://github.com/bQuery/bQuery/issues/136)) — a deterministic, referentially-stable composite key replaces the colliding key (so duplicate rows reuse their DOM), and the warning is dev-only and emitted once per offending key instead of on every re-render.
- [x] **Object-expression parsing edge case resolved** — shorthand properties (`bq-class="{ active }"`) now behave like JS object shorthand (`{ active: active }`) instead of being silently dropped.
- [x] **Per-directive SSR support documented** ([#128](https://github.com/bQuery/bQuery/issues/128)) — see the [matrix](#per-directive-ssr-support) below.
- [ ] **Public surface frozen for one minor** (no breaking directive/grammar changes) — demonstrated across the 1.15 cycle.

### Frozen directive reference (1.15.0)

The frozen directive set: `bq-text`, `bq-html`, `bq-html-safe`, `bq-if`, `bq-show`, `bq-for`, `bq-class`, `bq-style`, `bq-bind:*`, `bq-model`, `bq-on` (+ modifiers), `bq-once`, `bq-init`, `bq-pre`, `bq-cloak`, `bq-memo`, `bq-error`, `bq-aria`, `bq-ref`. The declarative transition companions (`bq-transition`, `bq-in`, `bq-out`, `bq-transition-duration`, `bq-transition-easing`, `bq-animate`) and the `bq-key` / `:key` companion are part of the frozen surface. The expression grammar is standard JavaScript expressions evaluated against the binding context.

### Per-directive SSR support

The SSR renderer evaluates a subset of directives into hydration-ready markup; the rest attach on the client during hydration. Pass `{ directives: 'full' }` to `renderToString()` to server-render the interactive directives, and `onUnsupportedDirective` to enforce the boundary (see the [SSR guide](./ssr)).

| Directive | SSR (`static`) | SSR (`full`) | Client (hydrate) |
| ------------------------------------------------------ | -------------- | ------------ | ---------------- |
| `bq-text` / `bq-html` | yes | yes | yes |
| `bq-if` / `bq-show` / `bq-for` | yes | yes | yes |
| `bq-class` / `bq-style` / `bq-bind:*` | yes | yes | yes |
| `bq-model` | no | yes (value) | yes |
| `bq-on:*` | no | marker only | yes (attaches) |
| `bq-html-safe` / `bq-once` / `bq-init` / `bq-memo` / `bq-ref` | no | no | yes (client-only)|
| `bq-error` / `bq-aria` | no | no | yes |
| Transitions (`bq-transition`/`bq-in`/…) | no | no | yes (client-only)|

Transitions are inherently client-only (they animate live DOM); on the server the companion attributes are inert and stripped/ignored.

## Basic Usage

```html
Expand Down Expand Up @@ -331,6 +364,96 @@ mount('#app', { inputEl });
inputEl.value?.focus();
```

## Transitions

Declarative enter/leave/move transitions bind the [`motion`](./motion) engine to the structural directives — no lifecycle glue required. They are a thin layer over `motion` (Web Animations for enter/leave, FLIP for moves) and automatically respect the user's reduced-motion preference.

```html
<!-- enter/leave on conditional render -->
<div bq-if="open" bq-transition="fade" bq-transition-duration="200">…</div>

<!-- separate in / out -->
<li bq-if="visible" bq-in="slide-up" bq-out="fade">…</li>

<!-- FLIP move on list reorder -->
<ul>
<li bq-for="item in items" bq-key="item.id" bq-animate="flip">…</li>
</ul>
```

### Companion attributes

| Attribute | Applies to | Purpose |
| ------------------------- | ------------------------- | ------------------------------------------------------------------- |
| `bq-transition` | `bq-if` / `bq-show` / `bq-for` | Named preset used for **both** enter and leave. |
| `bq-in` | `bq-if` / `bq-show` / `bq-for` | Enter-only preset (overrides `bq-transition` for enter). |
| `bq-out` | `bq-if` / `bq-show` / `bq-for` | Leave-only preset (overrides `bq-transition` for leave). |
| `bq-transition-duration` | same | Duration in milliseconds (default `200`; FLIP default `300`). |
| `bq-transition-easing` | same | CSS easing (default `ease`; FLIP default `ease-out`). |
| `bq-animate="flip"` | `bq-for` | FLIP move animation when list items reorder. |

Built-in presets: `fade`, `scale`, `slide`, `slide-up`, `slide-down`, `slide-left`, `slide-right`. (`slide` is an alias for `slide-up`; an unknown name falls back to `fade`.)

### Behaviour

- **No animation on first paint.** Initial render is not animated — only subsequent inserts/removals/reorders are (parity with Vue/Svelte, which require an explicit appear transition).
- **Leave defers removal.** `bq-if` keeps the element mounted until its leave animation finishes, then swaps in the placeholder; `bq-for` removes the row only after its leave resolves.
- **Race-safe.** Re-showing an element while it is leaving cancels the pending removal and animates it back in.
- **Reduced motion.** When `prefers-reduced-motion` is set (or `setReducedMotion(true)`), animations are skipped and state changes commit immediately.

```ts
import { mount, signal } from '@bquery/bquery/view';

const items = signal([{ id: 1, label: 'One' }]);
mount('#list', {
items,
add: () => (items.value = [...items.value, { id: items.value.length + 1, label: 'New' }]),
});
```

```html
<ul id="list">
<li bq-for="item in items" bq-key="item.id" bq-animate="flip" bq-in="slide-up" bq-out="fade" bq-text="item.label"></li>
</ul>
```

## Optional compiler (build step)

`view` evaluates directive expressions at runtime via `new Function()` — exactly right for the zero-build CDN story, but it requires `'unsafe-eval'` in your CSP and pays a parse cost on the hot path. The **opt-in** compiler at `@bquery/bquery/view/compiler` pre-parses `bq-*` expressions at build time and emits optimized, `with`-free update functions. The runtime evaluator stays the default; compiled and runtime paths are behaviourally identical, and any expression the compiler can't statically handle transparently falls back to runtime.

```ts
import { compileToModule } from '@bquery/bquery/view/compiler';

const { code, stats } = compileToModule(templateHtml);
// Write `code` to disk and import it once before mounting. The emitted module
// calls registerCompiledExpressions(...) — no `new Function()`, CSP-safe.
console.log(`${stats.compiled}/${stats.total} expressions compiled`);
```

The emitted module registers the precompiled functions via `registerCompiledExpressions()` (from `@bquery/bquery/view`); the runtime then uses them automatically. You can also register a map by hand and clear it with `clearCompiledExpressions()`:

```ts
import { registerCompiledExpressions, clearCompiledExpressions } from '@bquery/bquery/view';

registerCompiledExpressions({ 'count + 1': ($ctx) => $ctx.count + 1 });
// …later, to restore the pure runtime path (e.g. in tests):
clearCompiledExpressions();
```

### CLI

A dependency-free CLI compiles template files to sibling modules (pass explicit paths; let your shell expand globs):

```bash
bquery-view-compile --out-dir src/views/.compiled src/views/*.html
```

Or drive it from a build script with `compileFiles()` / `runCompileCli()`. The compiler is intentionally **not** a build tool of its own — `compileViews()` and `compileToModule()` are small transforms usable from any bundler (Vite/esbuild/Rollup).

### What compiles

The transform is conservative: identifiers, member access, calls, arithmetic/logical/comparison operators, ternaries, array and object literals (including shorthand), and string literals compile. It bails — and the expression falls back to runtime — on assignments, arrow/`function` bodies, `new`, spread, regex and template literals. Those skipped expressions are listed in `stats.skipped` with a reason, so nothing is silently dropped.

## Mounting

### mount()
Expand Down Expand Up @@ -629,7 +752,7 @@ The current approach matches industry standards (Vue, Alpine, Angular) while kee

<!-- uniform-template-footer -->

## Directive reference (1.14.0)
## Directive reference (1.15.0, frozen)

| Directive | Purpose |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -647,6 +770,8 @@ The current approach matches industry standards (Vue, Alpine, Angular) while kee
| `bq-cloak` | Hide until the view is mounted to prevent FOUC. |
| `bq-memo` | Memoize a subtree by a reactive key. |
| `bq-error` | Per-subtree error boundary for binding failures. |
| `bq-transition` / `bq-in` / `bq-out` | Declarative enter/leave [transitions](#transitions) on `bq-if` / `bq-show` / `bq-for` (companion attributes). |
| `bq-animate="flip"` | FLIP [move transition](#transitions) when `bq-for` items reorder. |

## Pitfalls and gotchas

Expand Down Expand Up @@ -676,4 +801,5 @@ The current approach matches industry standards (Vue, Alpine, Angular) while kee

## Version history

- **1.15.0** — **targeting Stable**: directive set + grammar frozen; `bq-for` duplicate-key and object-expression (`{ active }` shorthand) edge cases resolved; declarative enter/leave/move transitions (`bq-transition`, `bq-in`, `bq-out`, `bq-transition-duration`, `bq-transition-easing`, `bq-animate="flip"`); optional `@bquery/bquery/view/compiler` build step with `registerCompiledExpressions` / `clearCompiledExpressions` runtime hooks.
- **1.14.0** — `parseDirective`, `ParsedDirective`, new directives `bq-once`, `bq-init`, `bq-pre`, `bq-cloak`, `bq-html-safe`, `bq-memo`, full `bq-on` modifier system.
2 changes: 2 additions & 0 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Stable modules will not introduce breaking changes between minor releases. Beta

`concurrency` is also **targeting Stable in 1.15.0**: its adoption-blocking prerequisite is resolved — CSP-safe module workers (`defineWorker` / `exposeTask`) remove the mandatory `'unsafe-eval'` — and the public surface is now frozen for one minor cycle. It also gains client UI-scheduling primitives (`suspense`, `startTransition`, `deferred`). See the [Concurrency Stability section](/guide/concurrency) for the exit-criteria checklist, frozen surface, and per-environment support matrix.

`view` is also **targeting Stable in 1.15.0**: the directive set and expression grammar are frozen, the documented `bq-for` duplicate-key and object-expression edge cases are resolved, and a per-directive SSR support matrix is published. It also gains declarative enter/leave/move transitions (binding the `motion` engine to `bq-if`/`bq-show`/`bq-for`) and an optional `@bquery/bquery/view/compiler` build step that precompiles `bq-*` expressions without `'unsafe-eval'`. See the [View Stability section](/guide/view) for the exit-criteria checklist, frozen directive reference, and per-directive SSR matrix.

## When to use bQuery

bQuery is a good fit when you want:
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
"import": "./dist/view.es.mjs",
"types": "./dist/view/index.d.ts"
},
"./view/compiler": {
"import": "./dist/view-compiler.es.mjs",
"types": "./dist/view/compiler/index.d.ts"
},
"./storybook": {
"import": "./dist/storybook.es.mjs",
"types": "./dist/storybook/index.d.ts"
Expand Down Expand Up @@ -107,9 +111,13 @@
"files": [
"dist",
"src",
"bin",
"README.md",
"LICENSE.md"
],
"bin": {
"bquery-view-compile": "bin/bquery-view-compile.mjs"
},
"sideEffects": false,
"scripts": {
"dev": "vitepress dev docs",
Expand Down
9 changes: 8 additions & 1 deletion src/full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,14 @@ export type {
// ============================================================================
// View Module: Declarative DOM bindings without compiler
// ============================================================================
export { clearExpressionCache, createTemplate, mount, parseDirective } from './view/index';
export {
clearCompiledExpressions,
clearExpressionCache,
createTemplate,
mount,
parseDirective,
registerCompiledExpressions,
} from './view/index';
export type { BindingContext, MountOptions, ParsedDirective, View } from './view/index';

// ============================================================================
Expand Down
Loading