Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bb2959d
Merge pull request #93 from bQuery/dev
JosunLP May 16, 2026
f329964
feat(forms): expand into batteries-included tier with validators, fie…
Copilot May 19, 2026
baa7ef9
feat(forms,component): expand into batteries-included tier (1.13.0)
Copilot May 19, 2026
ec8ae14
refactor: address review feedback (slots CSS-escape, bind hardening, …
Copilot May 19, 2026
f97b7e9
fix(forms,component): address PR review feedback
Copilot May 20, 2026
7d6787a
fix(forms,component): address PR review feedback on bind, ssr, events…
Copilot May 20, 2026
7a21ff5
fix: address PR #95 review thread issues
Copilot May 20, 2026
e1db0a4
fix(forms,component): address latest PR review comments
Copilot May 20, 2026
b1f858b
docs(forms): clarify arrayOf message override docs
Copilot May 20, 2026
8184491
fix(forms): address latest review-thread feedback
Copilot May 20, 2026
1d190a5
fix(forms,component): address latest review thread
Copilot May 20, 2026
2137f49
fix(component,tests): restore render-scope guard and CI typings
Copilot May 20, 2026
b9023a9
fix(forms,component): address latest review feedback
Copilot May 20, 2026
2b7abfc
fix(forms,component): address remaining review comments
Copilot May 20, 2026
6272f82
fix(forms): address remaining review comments
Copilot May 20, 2026
c1244dd
docs(forms): clarify useFieldArray disposer note
Copilot May 20, 2026
401c2ea
docs(forms): clarify field array cleanup note
Copilot May 20, 2026
931d6c1
docs(forms): note scope-aware field array cleanup
Copilot May 20, 2026
bcee1eb
docs(forms): clarify non scoped item cleanup
Copilot May 20, 2026
8b6052f
docs(forms): reword field array disposal docs
Copilot May 20, 2026
627019e
fix(component,forms): address render scope review feedback
Copilot May 20, 2026
e54bf03
fix(forms): align field array add signature
Copilot May 20, 2026
c97834a
docs(component): fix updated hook keyed list example
Copilot May 20, 2026
22c755b
fix(review): address latest component and forms comments
Copilot May 20, 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
2 changes: 1 addition & 1 deletion .clinerules
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You are working on bQuery.js (`@bquery/bquery`), a batteries-included TypeScript

bQuery.js is not a utility library; its broad module surface is the product. The reactive module now also covers HTTP clients, polling, pagination, WebSocket / SSE, REST helpers, request queues, and request deduplication.

Current release baseline: **1.12.0**.
Current release baseline: **1.13.0**.

Version `1.12.0` adds Store plugin teardown APIs (`unregisterPlugin()`, `clearPlugins()`), promotes `WebSocketSendData` to a public Reactive type export, and tightens `/full` bundle type-export drift checks through `bun run check:full-bundle`. Version `1.11.0` added the dedicated `@bquery/bquery/server` entry point plus a major runtime-agnostic `@bquery/bquery/ssr` expansion (`renderToStringAsync()`, `renderToStream()`, `renderToResponse()`, runtime adapters, snapshots, resumability), while preserving the `1.10.0` concurrency helpers and the `1.9.0` watch/view/media APIs as first-class public surface.

Expand Down
2 changes: 1 addition & 1 deletion .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ You are working on bQuery.js, a batteries-included TypeScript full-stack web fra

> Derived snapshot for Cursor. If this file conflicts with `package.json`, `src/*/index.ts`, or `AGENT.md`, trust those files in that order and then realign this one.

Current release baseline: **1.12.0**.
Current release baseline: **1.13.0**.

bQuery.js is not a utility library; treat its broad module surface as the product, and keep guidance aligned with that scope.

Expand Down
10 changes: 9 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Work autonomously inside the scope of the current request, but do not invent a h

bQuery.js is a batteries-included framework, not a utility library. It is modular, tree-shakeable, has zero runtime dependencies, and currently ships **23 public entry points**.

Current release baseline: **1.12.0**.
Current release baseline: **1.13.0**.

Start here before making assumptions:

Expand All @@ -30,6 +30,14 @@ Prefer pointing back to those files instead of duplicating large architecture se

For repo guidance refreshes, keep the role split clear: `AGENT.md` is the deep reference, `llms.txt` is the compact mirror, this file stays behavioral/meta-oriented, and `.cursorrules` / `.clinerules` are derivative tool snapshots.

## Version 1.13.0 highlights

- `@bquery/bquery/forms` is now a true batteries-included tier. New validators (`integer`, `numeric`, `between`, `length`, `oneOf`, `notOneOf`, `arrayOf`, `requiredIf`, `requiredUnless`, `dateAfter`, `dateBefore`, `validDate`, `fileSize`, `fileType`) and combinators (`compose`, `all`, `not`, `withMessage`) are tree-shakeable; field/form state gains `isValidating`, `isFocused`, `dirtySince`, `disabled`, `setValue`/`setError`/`clearError`, `submitCount`, `submitError`, `isPristine`, `touchAll`/`untouchAll`, `resetField`, `resetErrors`, `getDirtyValues`, `subscribe`, `validationStrategy`, and `mode`; dynamic field arrays land via `createFieldArray`; declarative schema-style configuration via `schema()`; two-way DOM bindings via `bindField` / `bindForm`; scope-aware composables `useForm` / `useField` / `useFieldArray`; SSR helpers `serializeFormState` / `readSerializedFormState` / `hydrateForm`.
- `@bquery/bquery/component` adds slot helpers (`useSlot`, `hasSlot`, `slotText`), refs (`useRef`), async data (`useAsync`, `whenIdle`), DI (`provide` / `inject` / `formContextKey`), `beforeUnmount` and `errorBoundary` lifecycle hooks, instance-level `setProp` / `getProp` for non-string props, sanitizer-safe delegated event helpers (`on`, `onClick`, `onInput`, `onChange`, `onSubmit`, `bindDelegatedEvents`), a `css` tagged template literal with adoptable stylesheet support, and `keyedList` / `reconcileKeyed` for keyed list rendering.
- The `1.12.0` store/reactive surface (`unregisterPlugin`, `clearPlugins`, `WebSocketSendData`) and the `1.11.0` SSR/server runtime (`createServer`, `renderToStringAsync`, `renderToStream`, `renderToResponse`, runtime-agnostic WebSocket sessions) remain first-class public surface.
- The `1.10.0` concurrency additions plus the `1.9.0` watcher/view/media APIs remain first-class public surface and should still appear in documentation refreshes.
- Publish and local validation target Node.js `>=24.0.0` and Bun `>=1.3.13`; when release metadata or repo guidance changes, `bun run check:ai-guidance` is part of the expected validation.

## Version 1.12.0 highlights

- `@bquery/bquery/store` now includes `unregisterPlugin()` and `clearPlugins()` for plugin teardown, test isolation, and runtime plugin reloads.
Expand Down
9 changes: 8 additions & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name | bQuery.js |
| Package | `@bquery/bquery` |
| Version | 1.12.0 |
| Version | 1.13.0 |
| License | MIT |
| Language | TypeScript (strict) |
| Runtime | Browser (ESM, UMD, IIFE), plus Node.js, Bun, and Deno for SSR/server workflows |
Expand Down Expand Up @@ -40,6 +40,13 @@ bun run dev # VitePress docs server

Project-specific starter prompts live in [`.github/prompts/`](.github/prompts/) for common workflows such as starting a task, fixing a bug, extending a public API, adding a module, working on SSR/server features, and refreshing AI guidance.

## Version 1.13.0 Highlights

- `@bquery/bquery/forms` graduates into a batteries-included tier: new validators (`integer`, `numeric`, `between`, `length`, `oneOf`, `notOneOf`, `arrayOf`, `requiredIf`, `requiredUnless`, `dateAfter`, `dateBefore`, `validDate`, `fileSize`, `fileType`), validator combinators (`compose`, `all`, `not`, `withMessage`), enriched field/form state (`isValidating`, `isFocused`, `dirtySince`, `disabled`, `setValue`/`setError`/`clearError`, `submitCount`, `submitError`, `isPristine`, `touchAll`/`untouchAll`, `resetField`, `resetErrors`, `getDirtyValues`, `subscribe`, `validationStrategy`, `mode: 'all' | 'first'`), dynamic field arrays via `createFieldArray`, fluent `schema()` declaration, two-way DOM bindings (`bindField`, `bindForm`), scope-aware composables (`useForm`, `useField`, `useFieldArray`), and SSR helpers (`serializeFormState`, `readSerializedFormState`, `hydrateForm`).
- `@bquery/bquery/component` gains slot, ref, async, lifecycle, DI, and styling primitives: `useSlot` / `hasSlot` / `slotText`, `useRef`, `useAsync`, `whenIdle`, `provide` / `inject` / `formContextKey`, additive `beforeUnmount` and `errorBoundary` hooks, instance-level `setProp` / `getProp` for non-string props, delegated event helpers (`on`, `onClick`, `onInput`, `onChange`, `onSubmit`, `bindDelegatedEvents`), a `css` tagged template with adoptable stylesheet support, and `keyedList` / `reconcileKeyed` for keyed list rendering.
- All earlier baselines (`1.12.0` store plugin teardown, `1.11.0` runtime-agnostic SSR/server, `1.10.0` concurrency, `1.9.0` watch/view/media APIs) remain first-class public surface.
- Local validation and publish checks target Node.js `>=24.0.0` and Bun `>=1.3.13`; whenever release metadata or AI guidance changes, `bun run check:ai-guidance` should pass before you stop.

## Version 1.12.0 Highlights

- `@bquery/bquery/store` now exposes `unregisterPlugin()` and `clearPlugins()` so plugin registries can be torn down for test isolation and runtime reloads without affecting stores that already received extensions.
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to Semantic Versioning.
- [Unreleased](#unreleased)
- [Added (Unreleased)](#added-unreleased)
- [Fixed (Unreleased)](#fixed-unreleased)
- [\[1.13.0\] - 2026-05-19](#1130---2026-05-19)
- [Added (1.13.0)](#added-1130)
- [Changed (1.13.0)](#changed-1130)
- [\[1.12.0\] - 2026-05-16](#1120---2026-05-16)
- [Added (1.12.0)](#added-1120)
- [Changed (1.12.0)](#changed-1120)
Expand Down Expand Up @@ -83,6 +86,33 @@ and this project adheres to Semantic Versioning.

### Fixed (Unreleased)

## [1.13.0] - 2026-05-19

### Added (1.13.0)

- **Forms / Validators**: Added a batteries-included set of tree-shakeable validators to `@bquery/bquery/forms` — `integer`, `numeric`, `between`, `length`, `oneOf`, `notOneOf`, `arrayOf`, `requiredIf`, `requiredUnless`, `dateAfter`, `dateBefore`, `validDate`, `fileSize`, `fileType` — plus combinators `compose`, `all`, `not`, and `withMessage`. (`validDate` is exported under that name to avoid collision with the existing `isDate` type guard in `@bquery/bquery/core`.)
- **Forms / Field state**: Lifted `isValidating`, `isFocused`, and `dirtySince` signals onto every `FormField`. Added per-field helpers `focus()`, `blur()`, `setValue(value, { touch, validate, silent })`, `setError(message)`, `clearError()`, a `disabled` signal that excludes the field from validation, and per-field `validateOn` / `debounceMs` parity with `useFormField`. `FieldConfig` now accepts `parse` and `format` for programmatic inbound/outbound value normalization.
- **Forms / Form state**: Added `submitCount`, `lastSubmittedAt`, `submitError`, aggregated `isValidating` and `isPristine`, and helpers `touchAll()`, `untouchAll()`, `resetField(name)`, `resetErrors()`, `getDirtyValues()`, and `subscribe(listener)`. `FormConfig` now accepts `onSubmitError`, `onSubmitSuccess`, `validationStrategy`, and `mode: 'all' | 'first'`.
- **Forms / Field arrays**: Added `createFieldArray({ initial, factory, validators })` with `add`, `remove`, `move`, `insert`, `clear`, `items`, and `length` for dynamic repeating field groups.
- **Forms / Schema**: Added a fluent `schema({ name: field<string>().required().minLength(2), … })` helper that composes existing validator factories into a `FieldConfig` map.
- **Forms / DOM bindings**: Added `bindField(field, element, options?)` and `bindForm(form, formElement, options?)` to bridge `Form` and `FormField` instances to standard inputs, selects, textareas, checkboxes, radios, file inputs, and `[contenteditable]` elements; both return cleanup functions. `bindForm` auto-discovers `[name]` inputs, marks `aria-invalid`, and supports a configurable error slot mapper.
- **Forms / Composables**: Added scope-aware `useForm`, `useField`, and `useFieldArray` wrappers that auto-dispose with the owning component.
- **Forms / SSR**: Added `serializeFormState(id, form.snapshot())`, `readSerializedFormState(id)`, and `hydrateForm(form, id)` helpers (built on `src/ssr/escape.ts`) so server-rendered form state can resume on the client.
- **Component / Refs**: Added `useRef<T>()` that auto-clears on disconnect.
- **Component / Slots**: Added `useSlot(host, name?)` (reactive `Signal<Element[]>`), `hasSlot(host, name?)`, and `slotText(host, name?)`.
- **Component / Events**: Added sanitizer-safe delegated event helpers `on(event, handler)`, `onClick`, `onInput`, `onChange`, `onSubmit`, and `bindDelegatedEvents(host)`. Handlers are stored in a module-level map keyed by opaque IDs; templates only carry `data-bq-on-<event>="<id>"` attributes.
- **Component / DI**: Added `provide(host, key, value)`, `inject(host, key, fallback?)`, `injectionKey<T>(description)`, and the `formContextKey` for letting inputs auto-bind to an enclosing `<bq-form>` without globals.
- **Component / Lifecycle**: Added `beforeUnmount` and `errorBoundary(error, info)` hooks on `ComponentDefinition`, plus a scope-tracked `whenIdle(fn)` helper.
- **Component / Async**: Added `useAsync(fn)` returning `{ data, error, loading, refresh }` signals with `AbortController`-aware cancellation.
- **Component / Props**: Added imperative `setProp(name, value)` and `getProp(name)` methods on every component instance for non-string objects (arrays, callbacks) that bypass attribute serialization.
- **Component / Styles**: Added a `css` tagged template literal that produces a `ComponentStyles` payload. When Constructable Stylesheets are available the styles are shared via `document.adoptedStyleSheets`; otherwise the existing `<style>` element pathway is used. Interpolated values are CSS-escaped.
- **Component / Lists**: Added `keyedList(items, keyFn, renderItem)` and `reconcileKeyed(container)` for keyed list rendering inside shadow DOM.

### Changed (1.13.0)

- **Full bundle**: `src/full.ts` re-exports every new public forms and component runtime/type surface; `bun run check:full-bundle` continues to enforce drift detection.
- **AI guidance**: AGENT.md, llms.txt, copilot-instructions, Cursor / Cline rules, and CHANGELOG were refreshed for the 1.13.0 baseline. `bun run check:ai-guidance` passes.

## [1.12.0] - 2026-05-16

### Added (1.12.0)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

---

> **New in 1.12.0:** Store plugins now support teardown via `unregisterPlugin()` and `clearPlugins()`, Reactive WebSocket helpers expose the public `WebSocketSendData` payload type, and the `/full` bundle validation now catches runtime and type export drift before release. The runtime-agnostic SSR/server surface from 1.11.x remains first-class.
> **New in 1.13.0:** `@bquery/bquery/forms` graduates into a true batteries-included tier with many new validators (`integer`, `between`, `oneOf`, `arrayOf`, `requiredIf`, `dateAfter`, `fileSize`, …), combinators (`compose`, `all`, `not`, `withMessage`), dynamic field arrays (`createFieldArray`), a fluent `schema()` builder, two-way DOM bindings (`bindForm`, `bindField`), scope-aware composables (`useForm`, `useField`, `useFieldArray`), and SSR resumability. `@bquery/bquery/component` adds slot/ref helpers (`useSlot`, `useRef`), DI (`provide`/`inject`), `beforeUnmount` and `errorBoundary` hooks, `useAsync`/`whenIdle`, sanitizer-safe delegated events (`on`/`onClick`/…), a `css` tagged template with adoptable stylesheets, and keyed list rendering. All existing APIs from 1.12.x and earlier remain unchanged.

## Highlights

Expand Down
12 changes: 6 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

134 changes: 134 additions & 0 deletions docs/guide/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,137 @@ export const ButtonStory = {
```

`storyHtml()` sanitizes interpolated markup and understands Storybook-style boolean attribute shorthand such as `?disabled=${true}`.

## What's new in 1.13.0 — Component framework parity

`@bquery/bquery/component` gained a set of additive primitives that bring it to
parity with mainstream component frameworks while staying zero-dependency and
sanitizer-safe. None of the existing APIs changed.

### Refs and slots

```ts
import { component, html, useEffect, useRef, useSlot } from '@bquery/bquery/component';

component('focus-input', {
connected() {
const input = useRef<HTMLInputElement>();
const items = useSlot(this, 'item'); // Signal<Element[]>
useEffect(() => console.log('Items:', items.value.length));
queueMicrotask(() => input.bind(this.shadowRoot!.querySelector('input')!));
},
render: () => html`<input /><slot name="item"></slot>`,
});
```

### Delegated event handlers (no inline `on*` attributes, no `eval`)

```ts
import { onClick, bindDelegatedEvents } from '@bquery/bquery/component';

component('counter', {
connected() { bindDelegatedEvents(this); },
state: { n: 0 },
render({ state }) {
return html`<button ${onClick(() => state.n += 1)}>+ ${state.n}</button>`;
},
});
```

Handlers are stored in a module-level Map keyed by opaque IDs, so the sanitizer never
sees inline JavaScript.

### Dependency injection

```ts
import { provide, inject, injectionKey } from '@bquery/bquery/component';

const ThemeKey = injectionKey<{ dark: boolean }>('theme');

component('app-shell', {
connected() { provide(this, ThemeKey, { dark: true }); },
render: () => html`<slot></slot>`,
});

component('themed-button', {
connected() {
const theme = inject(this, ThemeKey, { dark: false });
console.log(theme!.dark);
},
render: () => html`<button><slot></slot></button>`,
});
```

Uses a typed `CustomEvent` over the composed path — no globals.

### Lifecycle additions

- `beforeUnmount()` — symmetric to `beforeMount`, runs before disconnect cleanup.
- `errorBoundary(error, info)` — returning a string renders that markup as a
fallback when `render()` throws.
- `whenIdle(fn)` — scope-tracked `requestIdleCallback` with `setTimeout` fallback.

### Async data

```ts
import { useAsync } from '@bquery/bquery/component';

connected() {
const { data, error, loading, refresh } = useAsync(async (signal) =>
(await fetch('/api/me', { signal })).json()
);
}
```

Aborts via `AbortController` when the component disconnects or `refresh()` is
re-invoked.

### Imperative props (non-string objects)

```ts
host.setProp('items', ['a', 'b']);
const current = host.getProp<string[]>('items');
```

Triggers a re-render and bypasses attribute serialization for arrays, objects,
and callbacks.

### `css` tagged template + adoptable stylesheets

```ts
import { component, css, html } from '@bquery/bquery/component';

component('themed-card', {
styles: css`
:host { display: block; padding: 1rem; }
h2 { color: ${'#0066cc'}; }
`,
render: () => html`<h2><slot></slot></h2>`,
});
```

When Constructable Stylesheets are available, the underlying CSS is shared
across instances via `document.adoptedStyleSheets`. Otherwise the existing
`<style>`-tag pathway is used. Interpolations are CSS-escaped.

### Keyed lists

```ts
import { component, html, keyedList, reconcileKeyed } from '@bquery/bquery/component';

component('todo-list', {
state: { items: [] as Array<{ id: string; text: string }> },
render({ state }) {
return html`<ul>${keyedList(state.items, (it) => it.id, (it) =>
`<li>${it.text}</li>`
)}</ul>`;
},
updated() {
const items = this.getState<Array<{ id: string }>>('items');
reconcileKeyed(
this.shadowRoot!.querySelector('ul')!,
items.map((item) => item.id)
);
},
});
```
Loading
Loading