@vielzeug/codex 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/.cache.json +31 -30
- package/data/llms-full.txt +730 -199
- package/data/llms.txt +5 -1
- package/data/vielzeug-data.json +4514 -4395
- package/package.json +1 -1
package/data/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vielzeug — Full Documentation
|
|
2
2
|
|
|
3
|
-
> Complete documentation for all
|
|
3
|
+
> Complete documentation for all 31 Vielzeug packages. Version: 1.0.3
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1211,6 +1211,569 @@ const price = currency({ amount: 123456n, currency: 'USD' }); // $1,234.56
|
|
|
1211
1211
|
- Type checking utilities (id: `typed-is`)
|
|
1212
1212
|
|
|
1213
1213
|
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
## @vielzeug/assay
|
|
1217
|
+
|
|
1218
|
+
**Category:** testing
|
|
1219
|
+
**Keywords:** testing, dom, events, queries, waitfor, custom-elements, jsdom
|
|
1220
|
+
**Key exports:** within, query, queryAll, queryByTestId, queryAllByTestId, queryByText, queryAllByText, queryInShadow, queryAllInShadow, queryPart, getSlotted, fire (+7 more)
|
|
1221
|
+
**Related:** ore, refine
|
|
1222
|
+
|
|
1223
|
+
### Overview
|
|
1224
|
+
|
|
1225
|
+
## Why Assay?
|
|
1226
|
+
|
|
1227
|
+
Testing DOM-level code (custom elements, vanilla event handlers, framework-rendered output) usually means hand-rolling `dispatchEvent` boilerplate and ad-hoc polling loops, or pulling in a full testing-library dependency tied to one framework's rendering model. Assay extracts just the generic, framework-agnostic pieces — scoped queries, event dispatch, async waiting — as a standalone, zero-dependency package.
|
|
1228
|
+
|
|
1229
|
+
```ts
|
|
1230
|
+
// Before — hand-rolled event dispatch and polling in every test file
|
|
1231
|
+
const btn = panel.querySelector('button.submit')!;
|
|
1232
|
+
|
|
1233
|
+
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
1234
|
+
|
|
1235
|
+
const deadline = Date.now() + 1000;
|
|
1236
|
+
while (Date.now() setTimeout(r, 50));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// After
|
|
1240
|
+
import { fire, waitFor, within } from '@vielzeug/assay';
|
|
1241
|
+
|
|
1242
|
+
const { query, queryByText } = within(panel);
|
|
1243
|
+
|
|
1244
|
+
fire.click(query('button.submit')!);
|
|
1245
|
+
await waitFor(() => queryByText('Saved') !== null);
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
| Feature | Assay | @testing-library/dom |
|
|
1249
|
+
| ----------------------- | ------------------------------------------------ | ---------------------------------------- |
|
|
1250
|
+
| Bundle size | | ~15 kB |
|
|
1251
|
+
| Framework-agnostic | | |
|
|
1252
|
+
| Scoped queries | `within()` | |
|
|
1253
|
+
| Low-level event dispatch| `fire.*` | Partial (`fireEvent`) |
|
|
1254
|
+
| Deterministic `waitFor` | | |
|
|
1255
|
+
| Zero dependencies | | |
|
|
1256
|
+
|
|
1257
|
+
**Use Assay when** you're testing custom elements or vanilla DOM code and want scoped queries, synchronous event dispatch, and async waiting without adopting a framework-specific testing library.
|
|
1258
|
+
|
|
1259
|
+
**Consider alternatives when** you're already standardized on `@testing-library/dom` (or a framework-specific wrapper around it) and don't need to avoid that dependency.
|
|
1260
|
+
|
|
1261
|
+
## Installation
|
|
1262
|
+
|
|
1263
|
+
Assay is a testing dependency — install it alongside your test runner.
|
|
1264
|
+
|
|
1265
|
+
```sh [pnpm]
|
|
1266
|
+
pnpm add -D @vielzeug/assay
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
```sh [npm]
|
|
1270
|
+
npm install -D @vielzeug/assay
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
```sh [yarn]
|
|
1274
|
+
yarn add -D @vielzeug/assay
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
## Quick Start
|
|
1278
|
+
|
|
1279
|
+
```ts
|
|
1280
|
+
import { fire, waitFor, within } from '@vielzeug/assay';
|
|
1281
|
+
|
|
1282
|
+
const panel = document.querySelector('.panel')!;
|
|
1283
|
+
const { query, queryByText } = within(panel);
|
|
1284
|
+
|
|
1285
|
+
fire.click(query('button.submit')!);
|
|
1286
|
+
|
|
1287
|
+
await waitFor(() => queryByText('Saved') !== null);
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
## Features
|
|
1291
|
+
|
|
1292
|
+
- `within(element)` — scoped `query`/`queryAll`/`queryByText`/`queryAllByText`/`queryByTestId`/`queryAllByTestId` for any element or shadow root, each also available as a free function (`query(root, selector)`, etc.) for when you already have a root.
|
|
1293
|
+
- `queryInShadow`/`queryAllInShadow`/`queryPart` — shadow-DOM-aware queries that return `null`/`[]` instead of throwing when the host has no shadow root.
|
|
1294
|
+
- `getSlotted(host, slotName?)` — light-DOM children assigned to a named slot, or every slotted child.
|
|
1295
|
+
- `fire.*` — low-level synchronous DOM event dispatchers (`click`, `input`, `keyDown`, `pointerDown`/`pointerMove`/`pointerCancel`, `custom`, and more), each with sensible `bubbles`/`cancelable` defaults and a consistent `boolean` return value — no exceptions.
|
|
1296
|
+
- `waitFor(fn, options?)` — polls until a callback returns truthy or a bare `expect()` call doesn't throw; always rejects with `AssayTimeoutError` on timeout, with the original failure preserved as `.cause`.
|
|
1297
|
+
- `waitForEvent(element, name, timeout?)` — resolves with the next matching event, or rejects with `AssayTimeoutError`.
|
|
1298
|
+
- `nextTick()`/`wait(ms?)` — microtask and macrotask timing helpers for reactive updates and debounced code.
|
|
1299
|
+
- `AssayError` / `AssayTimeoutError` — a single error hierarchy for every timeout this package raises.
|
|
1300
|
+
- No DOM-framework coupling — works with vanilla custom elements, `@vielzeug/ore` components, or any other DOM output.
|
|
1301
|
+
|
|
1302
|
+
## Documentation
|
|
1303
|
+
|
|
1304
|
+
- [Usage Guide](./usage.md)
|
|
1305
|
+
- [API Reference](./api.md)
|
|
1306
|
+
|
|
1307
|
+
## See Also
|
|
1308
|
+
|
|
1309
|
+
- [Ore](/ore/) — web-component authoring library whose `@vielzeug/ore/testing` sub-path re-exports Assay's query/event/wait primitives
|
|
1310
|
+
- [Refine](/refine/) — accessible component library built on Ore, testable with the same Assay primitives
|
|
1311
|
+
|
|
1312
|
+
### API Reference
|
|
1313
|
+
|
|
1314
|
+
## API Overview
|
|
1315
|
+
|
|
1316
|
+
| Symbol | Purpose | Execution mode | Common gotcha |
|
|
1317
|
+
| -------------------- | ----------------------------------------------------- | -------------- | -------------- |
|
|
1318
|
+
| `within(element)` | Scoped query helpers for one element/subtree | Sync | Returns a fresh `QueryScope` per call — cheap, but don't cache it across DOM mutations you care about |
|
|
1319
|
+
| `query()` / `queryAll()` | Free-function equivalents of `within(root).query`/`.queryAll` | Sync | Use these when you already have a root and don't need the rest of `QueryScope` |
|
|
1320
|
+
| `queryByTestId()` / `queryAllByTestId()` | Match a `data-testid` attribute | Sync | Free-function equivalents of `within(root).queryByTestId`/`.queryAllByTestId` |
|
|
1321
|
+
| `queryByText()` | First element matching trimmed text content | Sync | Matches exact trimmed text, not substrings |
|
|
1322
|
+
| `queryAllByText()` | Every element matching trimmed text content | Sync | Same exact-match caveat as `queryByText()` |
|
|
1323
|
+
| `queryInShadow()` / `queryAllInShadow()` | Query inside a host's shadow root | Sync | Returns `null`/`[]` (not a throw) when the host has no shadow root |
|
|
1324
|
+
| `queryPart()` | Query a shadow-DOM element by its `part` attribute | Sync | Shorthand for `queryInShadow(host, '[part="x"]')` |
|
|
1325
|
+
| `getSlotted()` | Light-DOM children assigned to a slot | Sync | Only direct children (`:scope >`) — doesn't recurse into further-nested slots |
|
|
1326
|
+
| `fire.*` | Dispatch a DOM event synchronously | Sync | Doesn't wait for anything — pair with `waitFor()`/`await` for async reactions |
|
|
1327
|
+
| `createPointerEvent()` | Build a `PointerEvent`, falling back to `MouseEvent` | Sync | Only needed if you're constructing an event by hand instead of using `fire.pointer*` |
|
|
1328
|
+
| `waitFor()` | Poll until a callback returns truthy or resolves | Async | Always rejects with `AssayTimeoutError` on timeout — the original failure is `.cause`, never the thrown error's own type |
|
|
1329
|
+
| `waitForEvent()` | Resolve on the next matching event | Async | Rejects with `AssayTimeoutError`, not a plain `Error`, on timeout |
|
|
1330
|
+
| `nextTick()` | Resolve after one microtask tick | Async | Doesn't wait for `setTimeout`-scheduled work — use `wait()` for that |
|
|
1331
|
+
| `wait()` | Resolve after a fixed millisecond delay | Async | A fixed delay, not a condition — prefer `waitFor()` when you can express a condition instead |
|
|
1332
|
+
|
|
1333
|
+
## Package Entry Point
|
|
1334
|
+
|
|
1335
|
+
| Import | Purpose |
|
|
1336
|
+
| -------------------- | ------------------------------------------------------------------- |
|
|
1337
|
+
| `@vielzeug/assay` | The entire public API — queries, event dispatch, async waiting |
|
|
1338
|
+
|
|
1339
|
+
## Query Helpers
|
|
1340
|
+
|
|
1341
|
+
### `within(element)`
|
|
1342
|
+
|
|
1343
|
+
Creates query helpers scoped to a single element or shadow root.
|
|
1344
|
+
|
|
1345
|
+
**Parameters**
|
|
1346
|
+
|
|
1347
|
+
| Name | Type | Description |
|
|
1348
|
+
| --------- | ---------- | ------------------------------------- |
|
|
1349
|
+
| `element` | `Element` | The root to scope every query to |
|
|
1350
|
+
|
|
1351
|
+
**Returns:** `QueryScope`
|
|
1352
|
+
|
|
1353
|
+
**Example**
|
|
1354
|
+
|
|
1355
|
+
```ts
|
|
1356
|
+
import { within } from '@vielzeug/assay';
|
|
1357
|
+
|
|
1358
|
+
const { query, queryAll, queryByTestId, queryAllByTestId, queryByText, queryAllByText } = within(panel);
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
`QueryScope` methods:
|
|
1362
|
+
|
|
1363
|
+
| Method | Returns |
|
|
1364
|
+
| ---------------------------------- | ----------------- |
|
|
1365
|
+
| `query(selector)` | `Element \| null` |
|
|
1366
|
+
| `queryAll(selector)` | `Element[]` |
|
|
1367
|
+
| `queryByText(text, selector?)` | `Element \| null` |
|
|
1368
|
+
| `queryAllByText(text, selector?)` | `Element[]` |
|
|
1369
|
+
| `queryByTestId(testId)` | `Element \| null` |
|
|
1370
|
+
| `queryAllByTestId(testId)` | `Element[]` |
|
|
1371
|
+
|
|
1372
|
+
`selector` defaults to `'*'` for the text-matching methods.
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
### `query(root, selector)` / `queryAll(root, selector)`
|
|
1377
|
+
|
|
1378
|
+
Free-function equivalents of `within(root).query`/`.queryAll` — a thin wrapper over `root.querySelector(All)`, exported directly so every `QueryScope` method has both a scoped and unscoped form.
|
|
1379
|
+
|
|
1380
|
+
**Returns:** `Element | null` / `Element[]`
|
|
1381
|
+
|
|
1382
|
+
---
|
|
1383
|
+
|
|
1384
|
+
### `queryByTestId(root, testId)` / `queryAllByTestId(root, testId)`
|
|
1385
|
+
|
|
1386
|
+
Free-function equivalents of `within(root).queryByTestId`/`.queryAllByTestId` — matches a `data-testid` attribute.
|
|
1387
|
+
|
|
1388
|
+
**Returns:** `Element | null` / `Element[]`
|
|
1389
|
+
|
|
1390
|
+
---
|
|
1391
|
+
|
|
1392
|
+
### `queryByText(root, text, selector)`
|
|
1393
|
+
|
|
1394
|
+
The unscoped function `within()` is built on — useful when you already have a root and don't need the rest of `QueryScope`.
|
|
1395
|
+
|
|
1396
|
+
**Parameters**
|
|
1397
|
+
|
|
1398
|
+
| Name | Type | Description |
|
|
1399
|
+
| ---------- | ---------------------- | ---------------------------------------------- |
|
|
1400
|
+
| `root` | `Element \| ShadowRoot`| Subtree to search |
|
|
1401
|
+
| `text` | `string` | Exact text to match against trimmed `textContent` |
|
|
1402
|
+
| `selector` | `string` | CSS selector narrowing candidate elements |
|
|
1403
|
+
|
|
1404
|
+
**Returns:** `Element | null` — the first matching element, or `null`.
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
|
|
1408
|
+
### `queryAllByText(root, text, selector)`
|
|
1409
|
+
|
|
1410
|
+
Same matching rules as `queryByText`, returning every match.
|
|
1411
|
+
|
|
1412
|
+
**Returns:** `Element[]`
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
### `queryInShadow(host, selector)` / `queryAllInShadow(host, selector)`
|
|
1417
|
+
|
|
1418
|
+
Query inside `host.shadowRoot`. Returns `null` (or `[]` for the `All` variant) instead of throwing when `host` has no shadow root — safe to call without an `if (host.shadowRoot)` guard.
|
|
1419
|
+
|
|
1420
|
+
**Returns:** `Element | null` / `Element[]`
|
|
1421
|
+
|
|
1422
|
+
---
|
|
1423
|
+
|
|
1424
|
+
### `queryPart(host, part)`
|
|
1425
|
+
|
|
1426
|
+
Shorthand for `queryInShadow(host, '[part="' + part + '"]')`.
|
|
1427
|
+
|
|
1428
|
+
**Returns:** `Element | null`
|
|
1429
|
+
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
### `getSlotted(host, slotName?)`
|
|
1433
|
+
|
|
1434
|
+
Returns the light-DOM children assigned to a named slot, or every slotted child (`:not([slot])`) when `slotName` is omitted.
|
|
1435
|
+
|
|
1436
|
+
**Returns:** `Element[]`
|
|
1437
|
+
|
|
1438
|
+
## Event Dispatch
|
|
1439
|
+
|
|
1440
|
+
### `fire`
|
|
1441
|
+
|
|
1442
|
+
An object of synchronous event dispatchers. Every method calls `element.dispatchEvent(...)` with an appropriately-typed `Event` subclass and sensible defaults, and returns `dispatchEvent`'s own boolean result.
|
|
1443
|
+
|
|
1444
|
+
```ts
|
|
1445
|
+
import { fire } from '@vielzeug/assay';
|
|
1446
|
+
|
|
1447
|
+
fire.click(el, opts?: PointerEventInit);
|
|
1448
|
+
fire.blur(el, opts?: FocusEventInit);
|
|
1449
|
+
fire.change(el, opts?: EventInit);
|
|
1450
|
+
fire.custom(el, name, opts?: CustomEventInit);
|
|
1451
|
+
fire.event(el, event: Event);
|
|
1452
|
+
fire.focus(el, name?, opts?: FocusEventInit);
|
|
1453
|
+
fire.input(el, opts?: EventInit);
|
|
1454
|
+
fire.keyboard(el, type, opts?: KeyboardEventInit);
|
|
1455
|
+
fire.keyDown(el, opts?: KeyboardEventInit);
|
|
1456
|
+
fire.keyUp(el, opts?: KeyboardEventInit);
|
|
1457
|
+
fire.mouse(el, type, opts?: MouseEventInit);
|
|
1458
|
+
fire.pointerCancel(el, opts?: PointerEventInit);
|
|
1459
|
+
fire.pointerDown(el, opts?: PointerEventInit);
|
|
1460
|
+
fire.pointerEnter(el, opts?: PointerEventInit);
|
|
1461
|
+
fire.pointerLeave(el, opts?: PointerEventInit);
|
|
1462
|
+
fire.pointerMove(el, opts?: PointerEventInit);
|
|
1463
|
+
fire.pointerUp(el, opts?: PointerEventInit);
|
|
1464
|
+
fire.submit(el, opts?: EventInit);
|
|
1465
|
+
fire.touch(el, type, opts?: EventInit);
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
`fire.touch` falls back to `CustomEvent` in environments without a `TouchEvent` constructor. `fire.pointer*` methods route through `createPointerEvent()`, so they fall back to `MouseEvent` the same way.
|
|
1469
|
+
|
|
1470
|
+
---
|
|
1471
|
+
|
|
1472
|
+
### `createPointerEvent(type, init?)`
|
|
1473
|
+
|
|
1474
|
+
Builds a `PointerEvent`, or a `MouseEvent` when `PointerEvent` isn't available in the current environment.
|
|
1475
|
+
|
|
1476
|
+
**Returns:** `Event`
|
|
1477
|
+
|
|
1478
|
+
## Async Waiting
|
|
1479
|
+
|
|
1480
|
+
### `waitFor(fn, options?)`
|
|
1481
|
+
|
|
1482
|
+
Polls `fn` until it returns truthy, returns `undefined` (a bare `expect()` call that didn't throw), or the timeout elapses.
|
|
1483
|
+
|
|
1484
|
+
**Parameters**
|
|
1485
|
+
|
|
1486
|
+
| Name | Type | Default | Description |
|
|
1487
|
+
| ------------------ | ------------------- | ------- | ---------------------------------------------- |
|
|
1488
|
+
| `fn` | `() => unknown` | — | Condition to poll |
|
|
1489
|
+
| `options.timeout` | `number` | `1000` | Maximum wait time in ms |
|
|
1490
|
+
| `options.interval` | `number` | `50` | Delay between polling attempts in ms |
|
|
1491
|
+
| `options.message` | `string` | — | Prefixed in front of the default timing summary in the timeout error — never replaces it |
|
|
1492
|
+
|
|
1493
|
+
**Returns:** `Promise`
|
|
1494
|
+
|
|
1495
|
+
**Throws:** `AssayTimeoutError` on timeout, unconditionally — regardless of whether the last polling attempt returned a falsy value or threw. The original failure (a thrown error, or its message) is preserved as `.cause` and folded into the timeout message; `fn`'s thrown error itself is never mutated.
|
|
1496
|
+
|
|
1497
|
+
**Example**
|
|
1498
|
+
|
|
1499
|
+
```ts
|
|
1500
|
+
await waitFor(() => queryByText('Saved') !== null);
|
|
1501
|
+
await waitFor(() => expect(spy).toHaveBeenCalled(), { timeout: 2000 });
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
---
|
|
1505
|
+
|
|
1506
|
+
### `waitForEvent(element, name, timeout?)`
|
|
1507
|
+
|
|
1508
|
+
Resolves with the next event of the given name.
|
|
1509
|
+
|
|
1510
|
+
**Parameters**
|
|
1511
|
+
|
|
1512
|
+
| Name | Type | Default | Description |
|
|
1513
|
+
| --------- | --------- | ------- | --------------------------------- |
|
|
1514
|
+
| `element` | `Element` | — | Element to listen on |
|
|
1515
|
+
| `name` | `string` | — | Event name |
|
|
1516
|
+
| `timeout` | `number` | `1000` | Maximum wait time in ms |
|
|
1517
|
+
|
|
1518
|
+
**Returns:** `Promise` — the event instance, typed as `T extends Event` (defaults to `Event`).
|
|
1519
|
+
|
|
1520
|
+
---
|
|
1521
|
+
|
|
1522
|
+
### `nextTick()`
|
|
1523
|
+
|
|
1524
|
+
Resolves after one microtask tick (`queueMicrotask`) — for waiting on reactivity (signal effects, promise-chain continuations) without moving into the macrotask queue.
|
|
1525
|
+
|
|
1526
|
+
**Returns:** `Promise`
|
|
1527
|
+
|
|
1528
|
+
---
|
|
1529
|
+
|
|
1530
|
+
### `wait(ms?)`
|
|
1531
|
+
|
|
1532
|
+
Resolves after `ms` milliseconds (default `0`).
|
|
1533
|
+
|
|
1534
|
+
**Returns:** `Promise`
|
|
1535
|
+
|
|
1536
|
+
## Types
|
|
1537
|
+
|
|
1538
|
+
```ts
|
|
1539
|
+
interface QueryScope {
|
|
1540
|
+
query(selector: string): E | null;
|
|
1541
|
+
queryAll(selector: string): E[];
|
|
1542
|
+
queryByText(text: string, selector?: string): E | null;
|
|
1543
|
+
queryAllByText(text: string, selector?: string): E[];
|
|
1544
|
+
queryByTestId(testId: string): E | null;
|
|
1545
|
+
queryAllByTestId(testId: string): E[];
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
interface WaitOptions {
|
|
1549
|
+
timeout?: number;
|
|
1550
|
+
interval?: number;
|
|
1551
|
+
message?: string;
|
|
1552
|
+
}
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
## Errors
|
|
1556
|
+
|
|
1557
|
+
| Error | Thrown by | Notable properties |
|
|
1558
|
+
| -------------------- | ------------------------------ | -------------------- |
|
|
1559
|
+
| `AssayError` | Base class for every Assay error — use `instanceof AssayError` to catch any of them | `AssayError.is(err)` static type guard |
|
|
1560
|
+
| `AssayTimeoutError` | `waitFor()`, `waitForEvent()` when the timeout elapses | Extends `AssayError` |
|
|
1561
|
+
|
|
1562
|
+
### Usage Guide
|
|
1563
|
+
|
|
1564
|
+
## Basic Usage
|
|
1565
|
+
|
|
1566
|
+
`within(element)` scopes queries to a subtree — useful for slotted content, shadow roots, or any container you don't want to re-select from `document` on every assertion.
|
|
1567
|
+
|
|
1568
|
+
```ts
|
|
1569
|
+
import { within } from '@vielzeug/assay';
|
|
1570
|
+
|
|
1571
|
+
const panel = document.querySelector('.panel')!;
|
|
1572
|
+
const { query, queryAll, queryByText } = within(panel);
|
|
1573
|
+
|
|
1574
|
+
query('.title'); // Element | null, scoped to panel
|
|
1575
|
+
queryAll('.item'); // Element[], scoped to panel
|
|
1576
|
+
queryByText('Save'); // matches trimmed textContent
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
`queryByTestId`/`queryAllByTestId` match a `data-testid` attribute — useful when text content or CSS structure is likely to change but a stable test hook is worth keeping:
|
|
1580
|
+
|
|
1581
|
+
```ts
|
|
1582
|
+
const { queryByTestId } = within(panel);
|
|
1583
|
+
|
|
1584
|
+
queryByTestId('save-button')?.click();
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
Every `QueryScope` method is also available as a free function taking the root as its first argument — reach for these when you already have a root in scope and don't need the rest of `within()`'s object:
|
|
1588
|
+
|
|
1589
|
+
```ts
|
|
1590
|
+
import { query, queryAll, queryByTestId, queryAllByTestId } from '@vielzeug/assay';
|
|
1591
|
+
|
|
1592
|
+
query(panel, '.title');
|
|
1593
|
+
queryAll(panel, '.item');
|
|
1594
|
+
queryByTestId(panel, 'save-button');
|
|
1595
|
+
queryAllByTestId(panel, 'item-row');
|
|
1596
|
+
```
|
|
1597
|
+
|
|
1598
|
+
For a custom element's shadow DOM, use `queryInShadow`/`queryAllInShadow`/`queryPart` instead — they return `null`/`[]` rather than throwing when the host has no shadow root (e.g. a `shadow: false` component), so you don't need an `if (host.shadowRoot)` guard at every call site:
|
|
1599
|
+
|
|
1600
|
+
```ts
|
|
1601
|
+
import { queryAllInShadow, queryInShadow, queryPart } from '@vielzeug/assay';
|
|
1602
|
+
|
|
1603
|
+
queryInShadow(customEl, '.internal-label');
|
|
1604
|
+
queryAllInShadow(customEl, '.option');
|
|
1605
|
+
queryPart(customEl, 'trigger'); // shorthand for queryInShadow(host, '[part="trigger"]')
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
`getSlotted()` reads the *light*-DOM children a host projects into a named slot (or every slotted child with no argument) — the complement to the shadow-DOM helpers above:
|
|
1609
|
+
|
|
1610
|
+
```ts
|
|
1611
|
+
import { getSlotted } from '@vielzeug/assay';
|
|
1612
|
+
|
|
1613
|
+
const slides = getSlotted(carousel); // default slot
|
|
1614
|
+
const actions = getSlotted(dialog, 'footer'); // named slot
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
## Firing Events
|
|
1618
|
+
|
|
1619
|
+
`fire.*` dispatches real DOM events synchronously — no framework-specific event simulation layer, just `dispatchEvent` with sensible defaults (`bubbles: true`, `cancelable: true` where that matches the real event's behavior). Every method returns the same `boolean` `dispatchEvent` itself returns (`false` when a listener called `preventDefault()`), with no exceptions — useful for asserting a handler actually intercepted the event:
|
|
1620
|
+
|
|
1621
|
+
```ts
|
|
1622
|
+
import { fire } from '@vielzeug/assay';
|
|
1623
|
+
|
|
1624
|
+
fire.click(button);
|
|
1625
|
+
fire.input(textInput); // fires a plain 'input' Event
|
|
1626
|
+
fire.keyDown(textInput, { key: 'Enter' });
|
|
1627
|
+
fire.custom(el, 'value-change', { detail: { value: 42 } });
|
|
1628
|
+
|
|
1629
|
+
const notPrevented = fire.click(link); // false if a listener called preventDefault()
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
Pointer events fall back to `MouseEvent` in environments without a `PointerEvent` constructor (some older jsdom versions) — Assay logs a one-time `console.warn` the first time this happens, so an environment gap doesn't show up as a confusing, unrelated test failure instead:
|
|
1633
|
+
|
|
1634
|
+
```ts
|
|
1635
|
+
fire.pointerDown(slider);
|
|
1636
|
+
fire.pointerMove(slider, { clientX: 120 });
|
|
1637
|
+
fire.pointerUp(slider);
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
Dispatch a pre-built event instance directly with `fire.event` when you need full control over the event's properties:
|
|
1641
|
+
|
|
1642
|
+
```ts
|
|
1643
|
+
fire.event(el, new CustomEvent('ready', { bubbles: true, detail: { ok: true } }));
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1646
|
+
## Waiting for Async Conditions
|
|
1647
|
+
|
|
1648
|
+
`waitFor()` polls a callback until it returns truthy (or doesn't throw, for a bare `expect()` call) — use it for anything that updates asynchronously (a reactive framework's next render, a debounced input handler, a `fetch` completing).
|
|
1649
|
+
|
|
1650
|
+
```ts
|
|
1651
|
+
import { waitFor } from '@vielzeug/assay';
|
|
1652
|
+
|
|
1653
|
+
await waitFor(() => panel.querySelector('.status')?.textContent === 'Ready');
|
|
1654
|
+
|
|
1655
|
+
// Works with `expect()` assertions too — a thrown assertion error means "not yet",
|
|
1656
|
+
// and its message is folded into the AssayTimeoutError thrown if the timeout is reached
|
|
1657
|
+
// (see below — the assertion's own error type never escapes waitFor() directly).
|
|
1658
|
+
await waitFor(() => expect(callback).toHaveBeenCalled());
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
Tune the polling interval and timeout per call — the defaults (1000ms timeout, 50ms interval) suit most reactive UI updates:
|
|
1662
|
+
|
|
1663
|
+
```ts
|
|
1664
|
+
await waitFor(() => queue.isEmpty(), { interval: 10, timeout: 5000 });
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
`waitForEvent()` resolves with the next matching event instead of polling — prefer it when you're waiting on something that's guaranteed to fire an event rather than settle into an observable DOM state:
|
|
1668
|
+
|
|
1669
|
+
```ts
|
|
1670
|
+
import { fire, waitForEvent } from '@vielzeug/assay';
|
|
1671
|
+
|
|
1672
|
+
const promise = waitForEvent>(el, 'item-added');
|
|
1673
|
+
|
|
1674
|
+
fire.click(addButton);
|
|
1675
|
+
|
|
1676
|
+
const event = await promise;
|
|
1677
|
+
console.log(event.detail.id);
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
Both `waitFor()` and `waitForEvent()` reject with `AssayTimeoutError` on timeout — unconditionally, even if the last attempt inside `waitFor()` threw a different error type (an `expect()` assertion, say). Catch `AssayTimeoutError` specifically when you want to distinguish "the condition never became true" from other failures; the original failure is preserved on `.cause` if you need it:
|
|
1681
|
+
|
|
1682
|
+
```ts
|
|
1683
|
+
import { AssayTimeoutError, waitForEvent } from '@vielzeug/assay';
|
|
1684
|
+
|
|
1685
|
+
try {
|
|
1686
|
+
await waitForEvent(el, 'never-fires', 100);
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
if (err instanceof AssayTimeoutError) {
|
|
1689
|
+
// expected — assert on the timeout itself, or inspect err.cause for the original failure
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
`nextTick()` and `wait()` cover the two other common timing needs: waiting for reactivity to settle, and waiting for a real macrotask delay.
|
|
1695
|
+
|
|
1696
|
+
```ts
|
|
1697
|
+
import { nextTick, wait } from '@vielzeug/assay';
|
|
1698
|
+
|
|
1699
|
+
signal.value = 'updated';
|
|
1700
|
+
await nextTick(); // let a microtask-scheduled effect run
|
|
1701
|
+
|
|
1702
|
+
await wait(300); // wait out a debounce timer — prefer nextTick()/waitFor() where possible
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
## Testing Custom Elements
|
|
1706
|
+
|
|
1707
|
+
Assay has no opinion about how your DOM was produced — it works the same way against a vanilla custom element, a framework-rendered component, or plain `document.createElement` output.
|
|
1708
|
+
|
|
1709
|
+
```ts
|
|
1710
|
+
import { fire, waitFor, within } from '@vielzeug/assay';
|
|
1711
|
+
|
|
1712
|
+
customElements.define(
|
|
1713
|
+
'my-counter',
|
|
1714
|
+
class extends HTMLElement {
|
|
1715
|
+
connectedCallback() {
|
|
1716
|
+
this.innerHTML = `+10`;
|
|
1717
|
+
this.querySelector('button')!.addEventListener('click', () => {
|
|
1718
|
+
const span = this.querySelector('.count')!;
|
|
1719
|
+
|
|
1720
|
+
span.textContent = String(Number(span.textContent) + 1);
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
);
|
|
1725
|
+
|
|
1726
|
+
const el = document.createElement('my-counter');
|
|
1727
|
+
|
|
1728
|
+
document.body.appendChild(el);
|
|
1729
|
+
|
|
1730
|
+
const { query } = within(el);
|
|
1731
|
+
|
|
1732
|
+
fire.click(query('button')!);
|
|
1733
|
+
|
|
1734
|
+
await waitFor(() => query('.count')?.textContent === '1');
|
|
1735
|
+
|
|
1736
|
+
el.remove();
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
## Working with Other Vielzeug Libraries
|
|
1740
|
+
|
|
1741
|
+
`@vielzeug/ore`'s `./testing` sub-path re-exports Assay's `within`, `fire`, `createPointerEvent`, `waitFor`, and `waitForEvent` directly — if you're already testing Ore components, you don't need a separate Assay import:
|
|
1742
|
+
|
|
1743
|
+
```ts
|
|
1744
|
+
import { fire, mount, waitFor } from '@vielzeug/ore/testing';
|
|
1745
|
+
|
|
1746
|
+
const { query } = await mount(() => html` {}}>Click`);
|
|
1747
|
+
|
|
1748
|
+
fire.click(query('button')!);
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
`@vielzeug/refine`'s `./testing` sub-path re-exports Assay's `queryInShadow`, `queryAllInShadow`, `queryPart`, `getSlotted`, `nextTick`, and `wait` the same way, alongside refine-specific helpers (ARIA attribute assertions, form-associated helpers) that stay in refine because they're specific to testing ore/refine component *contracts*, not generic DOM interaction:
|
|
1752
|
+
|
|
1753
|
+
```ts
|
|
1754
|
+
import { getAriaState, getSlotted, nextTick } from '@vielzeug/refine/testing';
|
|
1755
|
+
```
|
|
1756
|
+
|
|
1757
|
+
Import from `@vielzeug/assay` directly when testing DOM code that has nothing to do with Ore or Refine — a vanilla custom element, a framework-rendered component, or plain event-handler logic.
|
|
1758
|
+
|
|
1759
|
+
## Best Practices
|
|
1760
|
+
|
|
1761
|
+
- Prefer `within(element)` over repeated `element.querySelector(...)` calls — it reads better at call sites with multiple assertions against the same subtree.
|
|
1762
|
+
- Use `queryByTestId` for elements whose text or structure is expected to change; use `queryByText` when the visible text itself is what you're asserting on.
|
|
1763
|
+
- Reach for `waitForEvent()` over `waitFor()` when the thing you're waiting on is guaranteed to dispatch an event — it resolves on the first matching event instead of polling.
|
|
1764
|
+
- Catch `AssayTimeoutError` specifically (not a bare `Error`) when a test needs to branch on "the condition never happened" versus any other failure.
|
|
1765
|
+
- Keep `fire.*` calls synchronous where the real user interaction would be — `fire.click()` doesn't await anything; `await` the assertion that follows it instead.
|
|
1766
|
+
- Remove elements you create directly with `document.createElement` (`el.remove()`) at the end of each test — Assay itself has no auto-cleanup registry, unlike `@vielzeug/ore/testing`'s `mount()`/`cleanup()`.
|
|
1767
|
+
- Prefer `queryInShadow`/`queryAllInShadow` over `host.shadowRoot!.querySelector(...)` — the non-null assertion breaks the moment a component is tested with `shadow: false`, while `queryInShadow` degrades to `null` instead of throwing.
|
|
1768
|
+
|
|
1769
|
+
### Examples
|
|
1770
|
+
|
|
1771
|
+
## Examples
|
|
1772
|
+
|
|
1773
|
+
- [Custom Element Interaction](./examples/custom-element-interaction.md)
|
|
1774
|
+
- [Waiting for Async Updates](./examples/waiting-for-async-updates.md)
|
|
1775
|
+
|
|
1776
|
+
|
|
1214
1777
|
---
|
|
1215
1778
|
|
|
1216
1779
|
## @vielzeug/clockwork
|
|
@@ -12988,17 +13551,15 @@ if (!submission.ok && submission.type === 'validation') {
|
|
|
12988
13551
|
|
|
12989
13552
|
- Typed field paths with compile-time value inference
|
|
12990
13553
|
- Explicit validation API: `validate()`, `validate(name)`, and `validate(fields[])`
|
|
12991
|
-
- Streaming validation with `validateStream()` — yields each field result as it resolves, read-only
|
|
12992
13554
|
- Per-connection validation triggers via `connect()` with `ValidationModes` presets
|
|
12993
13555
|
- `connect()` bindings own independent debounce timers; call `binding.dispose()` on unmount
|
|
12994
13556
|
- `submit(handler)` — returns `{ ok: true, value }` or `{ ok: false, errors }`
|
|
12995
13557
|
- Schema integration: pass any `safeParse`-compatible schema directly to `validator`
|
|
12996
|
-
- `scope(prefix)` — memoized scoped sub-forms that share parent state with relative field paths
|
|
12997
|
-
- `
|
|
12998
|
-
- `snapshot()` / `restore()` — capture and replay complete form state
|
|
13558
|
+
- `scope(prefix)` — memoized scoped sub-forms that share parent state with relative field paths; `subscribe()` on a scoped form filters to just that prefix
|
|
13559
|
+
- `history.snapshot()` / `history.restore()` — capture and replay complete form state
|
|
12999
13560
|
- `form.fields.remove(name)` — clean conditional field lifecycle
|
|
13000
13561
|
- Full array helpers: `append`, `prepend`, `insert`, `remove`, `move`, `swap`, `replace`
|
|
13001
|
-
- Explicit synchronous subscriptions: `subscribe
|
|
13562
|
+
- Explicit synchronous subscriptions: `subscribe` (form or scoped-form state) and `subscribeField` (one field)
|
|
13002
13563
|
- Stable frozen snapshots for `form.state` and `form.field(name)` (external-store friendly)
|
|
13003
13564
|
- Explicit touched and error controls: `touch`, `untouch`, `touchAll`, `untouchAll`, `setError`, `resetErrors`
|
|
13004
13565
|
- Mutation batching with `batch(fn)` and dynamic field validators via `fields.setValidator`
|
|
@@ -13030,13 +13591,12 @@ if (!submission.ok && submission.type === 'validation') {
|
|
|
13030
13591
|
| `form.get()` / `form.set()` | Read/write field values by dot-path | Sync | `set()` after `dispose()` throws |
|
|
13031
13592
|
| `form.field()` / `form.state` | Read field and form snapshots | Sync | Returns a stable frozen snapshot; re-read on each subscriber call |
|
|
13032
13593
|
| `form.validate()` | Run validation — all fields, a subset, or a single field | Async | Each call re-runs validators from scratch |
|
|
13033
|
-
| `form.validateStream()` | Streaming validation — yields each field result as it resolves | Async (iterator) | Read-only — does not write errors to form state |
|
|
13034
13594
|
| `form.submit()` | Deterministic submit flow returning a `SubmitResult` | Async | Rejects if called while already submitting — guard with `form.isSubmitting` |
|
|
13035
13595
|
| `form.connect()` | Live field binding with DOM event handlers and live getters | Sync | Do not destructure — live getters lose context; call `dispose()` on unmount |
|
|
13036
13596
|
| `form.scope()` | Memoized scoped sub-form with relative field paths | Sync | Returns the same object for repeated calls with the same prefix; `state` is scoped — flags reflect only prefix fields |
|
|
13037
13597
|
| `form.array()` | Array mutation helpers | Sync | Returns a cached helper — call once and reuse |
|
|
13038
|
-
| `form.subscribe()` / `form.subscribeField()`
|
|
13039
|
-
| `form.snapshot()` / `form.restore()`
|
|
13598
|
+
| `form.subscribe()` / `form.subscribeField()` | Synchronous form and field subscriptions | Sync | On a scoped form, `subscribe()` is already prefix-filtered — callbacks receive frozen snapshots |
|
|
13599
|
+
| `form.history.snapshot()` / `form.history.restore()` | Capture and replay complete form state | Sync | Useful for undo/redo and draft saving |
|
|
13040
13600
|
| `form.batch()` | Group mutations into one notification | Sync | Nested `batch()` calls are safe — only the outermost flush notifies |
|
|
13041
13601
|
| `form.touch()` / `form.touchAll()` | Mark fields touched | Sync | `touchAll()` marks every key currently in the store |
|
|
13042
13602
|
| `form.setError()` / `form.clearError()` / `form.resetErrors()` | Manual error management | Sync | `setError()` bypasses validators; cleared on next `validate()` run for that field |
|
|
@@ -13349,11 +13909,6 @@ subscribeField>(
|
|
|
13349
13909
|
options?: SubscribeOptions,
|
|
13350
13910
|
): Unsubscribe
|
|
13351
13911
|
|
|
13352
|
-
subscribeScoped(
|
|
13353
|
-
listener: (state: FormState) => void,
|
|
13354
|
-
options?: SubscribeOptions,
|
|
13355
|
-
): Unsubscribe
|
|
13356
|
-
|
|
13357
13912
|
type SubscribeOptions = { sync?: boolean };
|
|
13358
13913
|
type Unsubscribe = () => void;
|
|
13359
13914
|
```
|
|
@@ -13362,17 +13917,17 @@ Pass `{ sync: true }` to also receive the current snapshot immediately upon subs
|
|
|
13362
13917
|
|
|
13363
13918
|
Subscriptions fire synchronously whenever the form mutates. Because state snapshots are stable (frozen, reference-equal between mutations), these integrate directly with React `useSyncExternalStore`, Vue `shallowRef`, and the Svelte store protocol.
|
|
13364
13919
|
|
|
13365
|
-
###
|
|
13920
|
+
### subscribe on a scoped form
|
|
13366
13921
|
|
|
13367
|
-
`
|
|
13922
|
+
`subscribe` behaves differently depending on which form object it's called on:
|
|
13368
13923
|
|
|
13369
13924
|
- **On a scoped form** — filters `errors`, `touchedFields`, and `validatingFields` to paths within the scope's prefix (remapped to relative paths). The listener is **only called when the scoped projection changes** — mutations outside the scope are suppressed. `isDirty`, `isValid`, `isTouched`, and `isValidating` reflect **only the scoped fields**. `isSubmitting`, `isLoading`, and `submitCount` reflect the full form.
|
|
13370
|
-
- **On a root form** —
|
|
13925
|
+
- **On a root form** — no filtering is applied; every mutation notifies the listener.
|
|
13371
13926
|
|
|
13372
13927
|
```ts
|
|
13373
13928
|
const address = form.scope('address');
|
|
13374
13929
|
|
|
13375
|
-
address.
|
|
13930
|
+
address.subscribe((state) => {
|
|
13376
13931
|
// state.errors uses relative keys: { city: '...' } not { 'address.city': '...' }
|
|
13377
13932
|
// only fires when an address.* field changes
|
|
13378
13933
|
console.log(state.errors, state.touchedFields);
|
|
@@ -13399,11 +13954,13 @@ type ArrayField = {
|
|
|
13399
13954
|
|
|
13400
13955
|
`append()` and `prepend()` initialize the field as a one-item array when its current value is `undefined` or `null`. If the field already holds a non-array, non-nullish value (e.g. written by `set()`), both are a no-op — they never overwrite an existing scalar with an array. `insert()`, `remove()`, `move()`, `swap()`, and `replace()` are all no-ops when the field's current value is not an array.
|
|
13401
13956
|
|
|
13402
|
-
## Snapshot / Restore
|
|
13957
|
+
## History (Snapshot / Restore)
|
|
13403
13958
|
|
|
13404
13959
|
```ts
|
|
13405
|
-
|
|
13406
|
-
|
|
13960
|
+
history: {
|
|
13961
|
+
snapshot(): FormSnapshot;
|
|
13962
|
+
restore(snap: FormSnapshot): void;
|
|
13963
|
+
}
|
|
13407
13964
|
|
|
13408
13965
|
type FormSnapshot = {
|
|
13409
13966
|
readonly baseline: Partial, unknown>>;
|
|
@@ -13415,44 +13972,17 @@ type FormSnapshot = {
|
|
|
13415
13972
|
};
|
|
13416
13973
|
```
|
|
13417
13974
|
|
|
13418
|
-
- `snapshot()` — captures the complete form state (values, baseline, errors, touched, dirty, submitCount) into a plain object.
|
|
13419
|
-
- `restore(snap)` — replaces all state with the snapshot. Aborts any in-flight validation.
|
|
13975
|
+
- `history.snapshot()` — captures the complete form state (values, baseline, errors, touched, dirty, submitCount) into a plain object.
|
|
13976
|
+
- `history.restore(snap)` — replaces all state with the snapshot. Aborts any in-flight validation.
|
|
13420
13977
|
|
|
13421
|
-
Useful for undo/redo, draft saving, and "discard changes" flows:
|
|
13978
|
+
Namespaced off the main `Form` surface since it's a distinctly less common operation than reading/writing values — grouped the same way `fields` already groups dynamic-field-lifecycle operations. Useful for undo/redo, draft saving, and "discard changes" flows:
|
|
13422
13979
|
|
|
13423
13980
|
```ts
|
|
13424
|
-
const draft = form.snapshot();
|
|
13981
|
+
const draft = form.history.snapshot();
|
|
13425
13982
|
|
|
13426
13983
|
form.set('email', 'changed@example.com');
|
|
13427
13984
|
|
|
13428
|
-
form.restore(draft); // reverts all changes
|
|
13429
|
-
```
|
|
13430
|
-
|
|
13431
|
-
## validateStream()
|
|
13432
|
-
|
|
13433
|
-
```ts
|
|
13434
|
-
validateStream(signal?: AbortSignal): AsyncIterableIterator
|
|
13435
|
-
```
|
|
13436
|
-
|
|
13437
|
-
Runs all field validators in parallel and yields each result as soon as its validator resolves. If a form-level validator is configured, all keys it returns are yielded last — including `field: '_form'` and any field-specific keys returned by the form validator.
|
|
13438
|
-
|
|
13439
|
-
**Read-only**: `validateStream()` does not write to `fieldErrors` or trigger subscriber notifications. Use `validate()` when you want errors applied to form state.
|
|
13440
|
-
|
|
13441
|
-
```ts
|
|
13442
|
-
for await (const { field, error } of form.validateStream()) {
|
|
13443
|
-
if (error) showInlineError(field, error);
|
|
13444
|
-
}
|
|
13445
|
-
// After the loop: form.state.errors is unchanged
|
|
13446
|
-
```
|
|
13447
|
-
|
|
13448
|
-
Pass an `AbortSignal` to cancel the stream:
|
|
13449
|
-
|
|
13450
|
-
```ts
|
|
13451
|
-
const ctrl = new AbortController();
|
|
13452
|
-
for await (const result of form.validateStream(ctrl.signal)) {
|
|
13453
|
-
processResult(result);
|
|
13454
|
-
}
|
|
13455
|
-
ctrl.abort(); // cancels any remaining in-flight validators
|
|
13985
|
+
form.history.restore(draft); // reverts all changes
|
|
13456
13986
|
```
|
|
13457
13987
|
|
|
13458
13988
|
## Baseline and Value Management
|
|
@@ -13648,12 +14178,15 @@ type ForgeDevtoolsOptions
|
|
|
13648
14178
|
|
|
13649
14179
|
// Utility types
|
|
13650
14180
|
type DeepPartial
|
|
13651
|
-
type FlatKeyOf
|
|
14181
|
+
type FlatKeyOf // capped at MAX_TYPED_PATH_DEPTH (5) — see below
|
|
13652
14182
|
type TypeAtPath
|
|
13653
14183
|
type ErrorKeyOf
|
|
13654
14184
|
type ScopedValues
|
|
14185
|
+
const MAX_TYPED_PATH_DEPTH // = 5 — not user-configurable, see rationale below
|
|
13655
14186
|
```
|
|
13656
14187
|
|
|
14188
|
+
`FlatKeyOf` falls back to plain `string` for paths deeper than `MAX_TYPED_PATH_DEPTH` — nothing throws, you just lose autocomplete/type-checking on that specific path (a dev-only console warning fires once per deep key so this is discoverable, not silent). This is a fixed constant, not a per-form generic parameter: it exists purely to protect TypeScript compile time, which doesn't get safer by letting one form's type param regress it for everyone importing that form's module.
|
|
14189
|
+
|
|
13657
14190
|
## Errors
|
|
13658
14191
|
|
|
13659
14192
|
Forge exports a small typed error hierarchy, all extending a common `ForgeError` base:
|
|
@@ -13905,7 +14438,7 @@ await address.submit((vals) => vals); // validates and submits only address.* fi
|
|
|
13905
14438
|
|
|
13906
14439
|
1. Call `const address = form.scope('address')` once per UI/module boundary and pass that around.
|
|
13907
14440
|
2. Use `address.validate()` / `address.submit()` instead of manually feeding `state.touchedFields` into `validate(fields[])`.
|
|
13908
|
-
3.
|
|
14441
|
+
3. Use `address.subscribe(...)` for section UIs — on a scoped form `subscribe` is already prefix-filtered, so sibling/root mutations do not trigger redraws.
|
|
13909
14442
|
|
|
13910
14443
|
**Key characteristics:**
|
|
13911
14444
|
|
|
@@ -13914,12 +14447,12 @@ await address.submit((vals) => vals); // validates and submits only address.* fi
|
|
|
13914
14447
|
|
|
13915
14448
|
### Scoped Subscriptions
|
|
13916
14449
|
|
|
13917
|
-
`
|
|
14450
|
+
On a scoped form, `subscribe()` delivers form state filtered to the scope's prefix. `errors`, `touchedFields`, and `validatingFields` use relative keys. `isDirty`, `isValid`, `isTouched`, and `isValidating` reflect **only the scoped fields**. The listener **only fires when the scoped projection changes** — mutations outside the prefix are suppressed. (On a root form, `subscribe()` behaves identically to today — no filtering applies.)
|
|
13918
14451
|
|
|
13919
14452
|
```ts
|
|
13920
14453
|
const address = form.scope('address');
|
|
13921
14454
|
|
|
13922
|
-
address.
|
|
14455
|
+
address.subscribe((state) => {
|
|
13923
14456
|
// state.errors → { city: 'Required' } (not 'address.city')
|
|
13924
14457
|
// state.isDirty → true only when an address.* field is dirty
|
|
13925
14458
|
// does not fire when form.set('name', 'Alice') is called
|
|
@@ -13951,29 +14484,18 @@ Snapshot semantics:
|
|
|
13951
14484
|
- Reference identity is preserved until a relevant mutation occurs.
|
|
13952
14485
|
- These are directly compatible with external-store patterns such as React `useSyncExternalStore`, Vue `shallowRef`, and the Svelte store protocol.
|
|
13953
14486
|
|
|
13954
|
-
##
|
|
14487
|
+
## History (Snapshot / Restore)
|
|
13955
14488
|
|
|
13956
|
-
|
|
14489
|
+
Capture and replay complete form state for undo/redo or "discard changes" flows, via the `history` namespace:
|
|
13957
14490
|
|
|
13958
14491
|
```ts
|
|
13959
|
-
|
|
13960
|
-
if (error) showInlineError(field, error);
|
|
13961
|
-
}
|
|
13962
|
-
// form.state.errors is unchanged after the loop
|
|
13963
|
-
```
|
|
13964
|
-
|
|
13965
|
-
## Snapshots and Restore
|
|
13966
|
-
|
|
13967
|
-
Capture and replay complete form state for undo/redo or "discard changes" flows:
|
|
13968
|
-
|
|
13969
|
-
```ts
|
|
13970
|
-
const draft = form.snapshot();
|
|
14492
|
+
const draft = form.history.snapshot();
|
|
13971
14493
|
|
|
13972
14494
|
// ... user edits ...
|
|
13973
14495
|
form.set('email', 'different@example.com');
|
|
13974
14496
|
|
|
13975
14497
|
// Revert all changes, including errors, touched, dirty, and submitCount
|
|
13976
|
-
form.restore(draft);
|
|
14498
|
+
form.history.restore(draft);
|
|
13977
14499
|
```
|
|
13978
14500
|
|
|
13979
14501
|
## Arrays
|
|
@@ -14219,7 +14741,6 @@ const formWithSchema = createForm({
|
|
|
14219
14741
|
- Field & Form Validation (id: `form-validation`)
|
|
14220
14742
|
- Schema Integration - safeParse Auto-detection (id: `schema-integration`)
|
|
14221
14743
|
- Scoped Sub-Forms (scope) (id: `scoped-sub-forms`)
|
|
14222
|
-
- Streaming Validation (validateStream) (id: `validate-stream`)
|
|
14223
14744
|
|
|
14224
14745
|
|
|
14225
14746
|
---
|
|
@@ -17706,7 +18227,7 @@ effect(() => {
|
|
|
17706
18227
|
|
|
17707
18228
|
**Category:** i18n
|
|
17708
18229
|
**Keywords:** internationalization, translations, pluralization, locale, i18n, l10n, async-loading
|
|
17709
|
-
**Key exports:** createI18n, createFormatter,
|
|
18230
|
+
**Key exports:** createI18n, createFormatter, validateCatalog, LinguaError, LinguaDisposedError, LinguaInvalidCountError, LinguaCountInVarsError, LinguaMissingLocaleError, LinguaInvalidLocaleError, LinguaNamespaceMissingError, LinguaRestoreError
|
|
17710
18231
|
**Related:** ripple, wayfinder, courier
|
|
17711
18232
|
|
|
17712
18233
|
### Overview
|
|
@@ -17821,7 +18342,7 @@ i18n.getSupportedLocales();
|
|
|
17821
18342
|
- Namespace lazy loading: `registerNamespace(ns, factory)` + `loadNamespace(ns, locale?)` — or use `extend(ns, factory, locale?)` as a convenience that does both; deduplicates per `ns + locale`; use for per-route or per-feature keys
|
|
17822
18343
|
- Scoped translation helpers: `scope(prefix)` returns a `{ fmt, t, tp, has }` helper bound to a key prefix
|
|
17823
18344
|
- Unified key existence check: `has(key)` returns `true` for leaf keys, branch keys, and pipe-plural base keys in the active fallback chain
|
|
17824
|
-
- Loaded-locale predicate: `isLoaded(locale)` returns `true` when a catalog is fully resolved — safe for `
|
|
18345
|
+
- Loaded-locale predicate: `isLoaded(locale)` returns `true` when a catalog is fully resolved — safe for `getState()` guards
|
|
17825
18346
|
- Registered-locale predicate: `isRegistered(locale)` distinguishes "never configured" from "async loader not yet called"
|
|
17826
18347
|
- Instance disposal: `dispose()` clears all subscribers and catalog state — prevents memory leaks in route-scoped SPA instances
|
|
17827
18348
|
- Typed error handling: every thrown/rejected error is `instanceof LinguaError`, with named subclasses (`LinguaDisposedError`, `LinguaMissingLocaleError`, `LinguaNamespaceMissingError`, …) for specific `instanceof` branching
|
|
@@ -17830,6 +18351,7 @@ i18n.getSupportedLocales();
|
|
|
17830
18351
|
- Deterministic fallback chain using active locale plus configured fallback locales
|
|
17831
18352
|
- Separate missing handlers: `onMissingKey(key, locale)` and `onMissingVar(varName, key, locale)`
|
|
17832
18353
|
- Formatting via `createFormatter(source)` — exported from the main entry alongside `createI18n`
|
|
18354
|
+
- Automatic dev-mode plural-form validation: every catalog registered or loaded is checked against CLDR rules for its locale, logged via `console.warn` — zero setup, and never bundled into production
|
|
17833
18355
|
|
|
17834
18356
|
## Documentation
|
|
17835
18357
|
|
|
@@ -17869,13 +18391,11 @@ i18n.getSupportedLocales();
|
|
|
17869
18391
|
| `i18n.loadNamespace()` | Load a registered namespace for a locale | Async | Deduplicates concurrent and repeated calls; throws `LinguaNamespaceMissingError` if namespace not registered |
|
|
17870
18392
|
| `i18n.isNamespaceLoaded()` | Check if a namespace is loaded for the active (or given) locale | Sync | Returns `false` if not registered or not yet loaded for this locale |
|
|
17871
18393
|
| `i18n.isNamespaceRegistered()` | Check if a namespace factory has been registered | Sync | `true` after `registerNamespace()` or `extend()`; `false` before |
|
|
17872
|
-
| `i18n.getState()` | Extract a serializable snapshot of loaded catalogs + active locale | Sync |
|
|
17873
|
-
| `i18n.restoreState()` | Hydrate instance from serialized state | Sync |
|
|
17874
|
-
| `serializeI18n()` | Serialise loaded catalogs for SSR hydration | Sync | Loader-only locales are omitted — check `isLoaded()` before calling |
|
|
17875
|
-
| `hydrateI18n()` | Hydrate a client instance from server-serialised state | Sync | Throws `LinguaRestoreError` if `state.locale` has no catalog |
|
|
18394
|
+
| `i18n.getState()` | Extract a serializable snapshot of loaded catalogs + active locale | Sync | Loader-only locales are omitted — check `isLoaded()` before calling |
|
|
18395
|
+
| `i18n.restoreState()` | Hydrate instance from serialized state | Sync | Throws `LinguaRestoreError` if `state.locale` has no catalog |
|
|
17876
18396
|
| Error classes | Named error subclasses (`LinguaDisposedError`, `LinguaMissingLocaleError`, …) | — | All runtime errors are `instanceof LinguaError`; use `instanceof` for specific handling |
|
|
17877
18397
|
| `createFormatter()` | Create a standalone Intl formatter | Sync | Available from the main entry or `@vielzeug/lingua/format` — pass a getter `() => i18n.locale` to follow locale changes |
|
|
17878
|
-
| `validateCatalog()` | Check a catalog for missing CLDR plural forms and missing `{count}` interpolations | Sync | Import from `@vielzeug/lingua/validate` —
|
|
18398
|
+
| `validateCatalog()` | Check a catalog for missing CLDR plural forms and missing `{count}` interpolations | Sync | Import from `@vielzeug/lingua/validate` for CI enforcement — `createI18n()` already runs the same check automatically in dev builds |
|
|
17879
18399
|
|
|
17880
18400
|
## Package Entry Points
|
|
17881
18401
|
|
|
@@ -18073,7 +18593,7 @@ Returns `true` if a namespace factory has been registered under `ns` via `regist
|
|
|
18073
18593
|
getState(): I18nState
|
|
18074
18594
|
```
|
|
18075
18595
|
|
|
18076
|
-
Extracts a serializable snapshot of all **fully loaded** catalogs and the active locale.
|
|
18596
|
+
Extracts a serializable snapshot of all **fully loaded** catalogs and the active locale.
|
|
18077
18597
|
|
|
18078
18598
|
**Warning:** Only fully resolved catalogs are included. Loader-only locales not yet preloaded are omitted. Use `i18n.isLoaded(locale)` to verify before calling.
|
|
18079
18599
|
|
|
@@ -18088,13 +18608,15 @@ const state = i18n.getState();
|
|
|
18088
18608
|
restoreState(state: I18nState): void
|
|
18089
18609
|
```
|
|
18090
18610
|
|
|
18091
|
-
Hydrates this instance from an `I18nState` produced by `getState()
|
|
18611
|
+
Hydrates this instance from an `I18nState` produced by `getState()`.
|
|
18092
18612
|
|
|
18093
18613
|
- Replaces all catalogs with those from `state`.
|
|
18094
18614
|
- Sets the active locale to `state.locale`.
|
|
18095
18615
|
- Clears all namespace loaded-markers so that `extend()` / `loadNamespace()` can re-apply namespaces.
|
|
18096
18616
|
- Notifies subscribers.
|
|
18097
18617
|
|
|
18618
|
+
Unlike `register()` and construction, this does **not** run the automatic dev-mode plural-form check (see [`validateCatalog()`](#validatecatalog)) — `state` is assumed to already have been registered, and therefore already checked, once on whatever system produced it.
|
|
18619
|
+
|
|
18098
18620
|
Throws `LinguaRestoreError` if `state.locale` has no catalog in `state.catalogs`.
|
|
18099
18621
|
Throws `LinguaDisposedError` if called on a disposed instance.
|
|
18100
18622
|
|
|
@@ -18218,13 +18740,13 @@ isLoaded(locale: Locale): boolean
|
|
|
18218
18740
|
|
|
18219
18741
|
Returns `true` if the catalog for `locale` is fully resolved (i.e. not a pending async loader). Returns `false` for unregistered locales, pending loaders, and invalid locale tags — never throws.
|
|
18220
18742
|
|
|
18221
|
-
Primary use case: guarding `
|
|
18743
|
+
Primary use case: guarding `getState()` in SSR to avoid silently omitting locales that were registered as async loaders but never preloaded.
|
|
18222
18744
|
|
|
18223
18745
|
```ts
|
|
18224
18746
|
// SSR guard — ensure all locales are loaded before serialising
|
|
18225
18747
|
const locales = i18n.getSupportedLocales();
|
|
18226
18748
|
await Promise.all(locales.filter((l) => !i18n.isLoaded(l)).map((l) => i18n.preload(l)));
|
|
18227
|
-
const state =
|
|
18749
|
+
const state = i18n.getState(); // now includes all locales
|
|
18228
18750
|
```
|
|
18229
18751
|
|
|
18230
18752
|
### `isRegistered()`
|
|
@@ -18246,7 +18768,7 @@ Use `isRegistered` + `isLoaded` together to distinguish the three states:
|
|
|
18246
18768
|
```ts
|
|
18247
18769
|
if (!i18n.isRegistered('fr')) throw new Error('fr locale not configured');
|
|
18248
18770
|
if (!i18n.isLoaded('fr')) await i18n.preload('fr');
|
|
18249
|
-
const state =
|
|
18771
|
+
const state = i18n.getState(); // 'fr' guaranteed to be present
|
|
18250
18772
|
```
|
|
18251
18773
|
|
|
18252
18774
|
### `disposalSignal`
|
|
@@ -18330,6 +18852,8 @@ Checks a flat or nested message catalog against CLDR plural rules for `locale`.
|
|
|
18330
18852
|
|
|
18331
18853
|
Returns an empty array when there are no issues.
|
|
18332
18854
|
|
|
18855
|
+
**Automatic dev-mode checks:** `createI18n()` already calls this internally, in dev builds only, every time a catalog becomes fully available — at construction (`createI18n({ catalogs })`), via `register()`, or once an async loader resolves — logging any warning through `console.warn`. Call `validateCatalog()` directly only when you want CI to fail the build on a warning rather than just log it; the automatic check already covers everyday authoring feedback with zero setup. The automatic check loads `validate.ts`'s logic as a separate, lazily-fetched chunk — it's never part of your production bundle either way.
|
|
18856
|
+
|
|
18333
18857
|
**Note:** A branch is treated as a plural branch when any of its child keys is a CLDR form (`zero`, `one`, `two`, `few`, `many`, `other`). A mixed-use branch (e.g. `{ count: 'x', one: 'y' }`) will also be flagged and may produce spurious warnings for non-CLDR sibling keys.
|
|
18334
18858
|
|
|
18335
18859
|
`validateCatalog` also checks for a common authoring error: a form template for `other`, `two`, `few`, or `many` that does not contain `{count}`. Since `tp()` injects `count` automatically, omitting it from a non-singleton form is almost always a mistake. These warnings use `form: ':missing-count'` (e.g. `'other:missing-count'`). The `zero` and `one` forms are exempt — intentionally omitting `{count}` is normal there (e.g. `'No messages'`, `'One message'`).
|
|
@@ -18449,7 +18973,7 @@ type I18nState = {
|
|
|
18449
18973
|
};
|
|
18450
18974
|
```
|
|
18451
18975
|
|
|
18452
|
-
Produced by `getState()`
|
|
18976
|
+
Produced by `getState()` and consumed by `restoreState()`. Catalogs are stored as flat dot-notation maps.
|
|
18453
18977
|
|
|
18454
18978
|
### `NamespaceFactory`
|
|
18455
18979
|
|
|
@@ -18608,50 +19132,25 @@ type ListFormatOptions = {
|
|
|
18608
19132
|
};
|
|
18609
19133
|
```
|
|
18610
19134
|
|
|
18611
|
-
##
|
|
19135
|
+
## SSR: `getState()` / `restoreState()`
|
|
18612
19136
|
|
|
18613
|
-
|
|
18614
|
-
import { serializeI18n } from '@vielzeug/lingua';
|
|
18615
|
-
|
|
18616
|
-
serializeI18n(i18n: I18n): I18nState
|
|
18617
|
-
```
|
|
18618
|
-
|
|
18619
|
-
Serialises the current loaded catalogs and active locale into an `I18nState` object. Use this on the server before embedding state in the HTML response. Loader-only locales that have not been preloaded are silently omitted — call `isLoaded()` to verify all locales are resolved before calling `serializeI18n()`.
|
|
19137
|
+
No standalone functions — call these directly on an instance (see [`getState()`](#getstate) / [`restoreState()`](#restorestate) above).
|
|
18620
19138
|
|
|
18621
19139
|
```ts
|
|
18622
19140
|
// Server
|
|
18623
19141
|
const i18n = createI18n({ catalogs: { de: deMessages, en: enMessages }, locale: 'de' });
|
|
18624
|
-
const state =
|
|
19142
|
+
const state = i18n.getState();
|
|
18625
19143
|
// Embed in the HTML response:
|
|
18626
19144
|
// window.__I18N__ = ${JSON.stringify(state)}
|
|
18627
19145
|
```
|
|
18628
19146
|
|
|
18629
|
-
## hydrateI18n
|
|
18630
|
-
|
|
18631
|
-
```ts
|
|
18632
|
-
import { hydrateI18n } from '@vielzeug/lingua';
|
|
18633
|
-
|
|
18634
|
-
hydrateI18n(i18n: I18n, state: I18nState): void
|
|
18635
|
-
```
|
|
18636
|
-
|
|
18637
|
-
Hydrates a client-side instance from server-serialised state. Replaces all catalogs and switches the active locale. Notifies subscribers once after hydration.
|
|
18638
|
-
|
|
18639
|
-
Throws `LinguaRestoreError` if `state.locale` has no corresponding entry in `state.catalogs`.
|
|
18640
|
-
|
|
18641
19147
|
```ts
|
|
18642
19148
|
// Client
|
|
18643
|
-
const
|
|
18644
|
-
|
|
19149
|
+
const client = createI18n();
|
|
19150
|
+
client.restoreState(window.__I18N__);
|
|
18645
19151
|
// Catalogs from state are immediately available; no network request needed.
|
|
18646
19152
|
```
|
|
18647
19153
|
|
|
18648
|
-
**Parameters:**
|
|
18649
|
-
|
|
18650
|
-
| Parameter | Type | Description |
|
|
18651
|
-
| --------- | ----------- | ------------------------------------------ |
|
|
18652
|
-
| `i18n` | `I18n` | The instance to hydrate. |
|
|
18653
|
-
| `state` | `I18nState` | State object produced by `serializeI18n()` |
|
|
18654
|
-
|
|
18655
19154
|
## Error Classes
|
|
18656
19155
|
|
|
18657
19156
|
All errors thrown by the `@vielzeug/lingua` runtime extend `LinguaError`. Use `instanceof LinguaError` to catch any lingua error, or `instanceof` the specific subclass for precise handling.
|
|
@@ -18679,7 +19178,7 @@ try {
|
|
|
18679
19178
|
| `LinguaMissingLocaleError` | `preload()` / `setLocale()` — locale has no registered source |
|
|
18680
19179
|
| `LinguaInvalidLocaleError` | Any API receiving an invalid BCP 47 tag |
|
|
18681
19180
|
| `LinguaNamespaceMissingError` | Namespace requested but not loaded for the current locale |
|
|
18682
|
-
| `LinguaRestoreError` | `
|
|
19181
|
+
| `LinguaRestoreError` | `restoreState()` — `state.locale` absent from `state.catalogs` |
|
|
18683
19182
|
|
|
18684
19183
|
### Usage Guide
|
|
18685
19184
|
|
|
@@ -18843,7 +19342,9 @@ Without `onMissingKey`, missing keys return the key string. Without `onMissingVa
|
|
|
18843
19342
|
|
|
18844
19343
|
## Validating Catalogs
|
|
18845
19344
|
|
|
18846
|
-
|
|
19345
|
+
`createI18n()` already runs this check automatically in dev builds — every time a catalog becomes available (construction, `register()`, or an async loader resolving), it's checked against CLDR plural rules and any issue is logged via `console.warn`. This costs nothing in production: the check runs behind the same dev-only gate as the rest of `@vielzeug/lingua`'s dev warnings, and the validation logic itself only loads as a separate on-demand chunk, never bundled into your app.
|
|
19346
|
+
|
|
19347
|
+
For CI enforcement (failing a build rather than just warning), call `validateCatalog()` directly. Import it from the dedicated `@vielzeug/lingua/validate` entry — never from the main entry or it will end up in your production bundle.
|
|
18847
19348
|
|
|
18848
19349
|
```ts
|
|
18849
19350
|
import { validateCatalog } from '@vielzeug/lingua/validate';
|
|
@@ -18882,7 +19383,7 @@ Key characteristics:
|
|
|
18882
19383
|
|
|
18883
19384
|
## SSR Hydration
|
|
18884
19385
|
|
|
18885
|
-
|
|
19386
|
+
Use the instance methods `getState()` on the server and `restoreState()` on the client:
|
|
18886
19387
|
|
|
18887
19388
|
```ts
|
|
18888
19389
|
import { createI18n } from '@vielzeug/lingua';
|
|
@@ -18899,7 +19400,7 @@ i18n.restoreState(window.__I18N__);
|
|
|
18899
19400
|
// Catalogs from state are immediately available; no network request needed.
|
|
18900
19401
|
```
|
|
18901
19402
|
|
|
18902
|
-
`restoreState()` replaces all catalogs, switches the active locale, clears namespace loaded-markers, and notifies subscribers once.
|
|
19403
|
+
`restoreState()` replaces all catalogs, switches the active locale, clears namespace loaded-markers, and notifies subscribers once.
|
|
18903
19404
|
|
|
18904
19405
|
**Warning:** `getState()` silently omits locales registered as async loaders but not yet preloaded. Use `isLoaded()` to guard:
|
|
18905
19406
|
|
|
@@ -19044,12 +19545,12 @@ router.subscribe(() => {
|
|
|
19044
19545
|
- Keep translation keys flat or one level deep — deeply nested keys are harder to refactor.
|
|
19045
19546
|
- Set `fallback` to a locale with 100% coverage so missing keys degrade gracefully.
|
|
19046
19547
|
- Use `extend(ns, factory, locale?)` or `registerNamespace()` + `loadNamespace()` for per-route or per-feature key sets.
|
|
19047
|
-
- Use `isLoaded(locale)` before `getState()`
|
|
19548
|
+
- Use `isLoaded(locale)` before `getState()` in SSR to avoid silently omitting async-loader locales.
|
|
19048
19549
|
- Use `isRegistered(locale)` to check if a locale is configured; use `isLoaded(locale)` to check if it is ready.
|
|
19049
19550
|
- Call `dispose()` on route-level or request-scoped `fork()` instances when they are no longer needed.
|
|
19050
19551
|
- Use `{ signal }` in `subscribe()` for lifecycle-safe subscriptions; use the returned `Unsubscribe` otherwise.
|
|
19051
19552
|
- Use `onMissingKey` and `onMissingVar` in development to surface authoring errors early; omit them in production.
|
|
19052
|
-
-
|
|
19553
|
+
- `createI18n()` already validates plural forms automatically in dev builds — only import `validateCatalog` from `@vielzeug/lingua/validate` directly if you want CI to fail the build on a warning.
|
|
19053
19554
|
- Share one `i18n` instance per app entry point; avoid creating separate instances per component.
|
|
19054
19555
|
|
|
19055
19556
|
### Examples
|
|
@@ -19083,7 +19584,7 @@ router.subscribe(() => {
|
|
|
19083
19584
|
- Pluralization Rules (id: `pluralization`)
|
|
19084
19585
|
- Preload Pattern (id: `preload-pattern`)
|
|
19085
19586
|
- scope(), has(), isLoaded(), extend() (id: `scope-bind`)
|
|
19086
|
-
-
|
|
19587
|
+
- getState() / restoreState() — SSR hydration (id: `ssr-hydration`)
|
|
19087
19588
|
- createFormatter() — standalone (no createI18n) (id: `standalone-formatter`)
|
|
19088
19589
|
- Variable Interpolation (id: `variable-interpolation`)
|
|
19089
19590
|
|
|
@@ -20913,7 +21414,7 @@ define('x-tooltip', {
|
|
|
20913
21414
|
|
|
20914
21415
|
**Category:** ui-primitives
|
|
20915
21416
|
**Keywords:** web-components, custom-elements, reactive, templates, signals, lifecycle
|
|
20916
|
-
**Key exports:** define, prop, html, css, ref, createContext, inject, injectStrict, provide, onMounted, onCleanup, useEmit (+
|
|
21417
|
+
**Key exports:** define, prop, html, css, ref, createContext, inject, injectStrict, provide, onMounted, onCleanup, useEmit (+15 more)
|
|
20917
21418
|
**Related:** ripple, refine, orbit
|
|
20918
21419
|
|
|
20919
21420
|
### Overview
|
|
@@ -21035,9 +21536,9 @@ define('my-counter', {
|
|
|
21035
21536
|
|
|
21036
21537
|
| Import | Purpose |
|
|
21037
21538
|
| --------------------------- | ----------------------------------------------------------------------------- |
|
|
21038
|
-
| `@vielzeug/ore` | Core component API and utilities (`define`, `prop`, `html`, `css`, context)
|
|
21539
|
+
| `@vielzeug/ore` | Core component API and utilities (`define`, `prop`, `html`, `css`, context), plus the everyday template directives (`each`, `when`, `model`, `classMap`, `styleMap`) |
|
|
21039
21540
|
| `@vielzeug/ore/devtools` | `debugFlush` — verbose flush for timing diagnostics (dev only) |
|
|
21040
|
-
| `@vielzeug/ore/directives` |
|
|
21541
|
+
| `@vielzeug/ore/directives` | The advanced/niche directives (`live`, `raw`) and the custom-directive authoring API (`createDirectiveResult`, `createSpreadObject`) |
|
|
21041
21542
|
| `@vielzeug/ore/forms` | `useField`, `createFormContext`, `FORM_CONTEXT_KEY` |
|
|
21042
21543
|
| `@vielzeug/ore/observers` | `resizeObserver`, `intersectionObserver`, `mediaObserver`, `mutationObserver` |
|
|
21043
21544
|
| `@vielzeug/ore/testing` | `mount`, `fire`, `user`, `waitFor`, `cleanup`, and helpers |
|
|
@@ -21088,9 +21589,9 @@ All symbols below (except `useField`/`createFormContext`, under `@vielzeug/ore/f
|
|
|
21088
21589
|
|
|
21089
21590
|
| Import | Purpose |
|
|
21090
21591
|
| ---------------------------- | ------------------------------------------------------------------ |
|
|
21091
|
-
| `@vielzeug/ore` | Core authoring/runtime API
|
|
21592
|
+
| `@vielzeug/ore` | Core authoring/runtime API, including the everyday template directives (`each`, `when`, `classMap`, `styleMap`, `model`) |
|
|
21092
21593
|
| `@vielzeug/ore/devtools` | `debugFlush` — verbose flush for timing diagnostics |
|
|
21093
|
-
| `@vielzeug/ore/directives` |
|
|
21594
|
+
| `@vielzeug/ore/directives` | The advanced/niche directives (`raw`, `live`) plus the custom-directive authoring API (`createDirectiveResult`, `createSpreadObject`) |
|
|
21094
21595
|
| `@vielzeug/ore/forms` | Form-association helpers (`useField`, `createFormContext`) |
|
|
21095
21596
|
| `@vielzeug/ore/observers` | Resize, intersection, mutation, and media observers |
|
|
21096
21597
|
| `@vielzeug/ore/testing` | DOM-oriented test helpers |
|
|
@@ -21344,7 +21845,7 @@ Slot signals update reactively when assigned content changes, including when slo
|
|
|
21344
21845
|
- `ref()` — Create a `Signal` element reference. Set to the element via `ref=` in templates.
|
|
21345
21846
|
- `createId(prefix = 'id')` — Generate a unique incremental string ID (e.g. `'id-1'`, `'id-2'`). Each call returns a new ID — it does not deduplicate by prefix.
|
|
21346
21847
|
- `createStableId(prefix = 'id')` — Generate a unique ID that also embeds a short random tag shared across all IDs generated in the session (e.g. `'field-a3k21'`), reducing collision risk when multiple app instances run on the same page. Like `createId()`, every call returns a new ID.
|
|
21347
|
-
- `
|
|
21848
|
+
- `resetStableIdCounter()` — Reset the `createStableId()` counter to 0. Call in test `beforeEach` for deterministic IDs. Scoped to `createStableId()` only — `createId()` has no public reset (it's for uniqueness, not cross-test determinism).
|
|
21348
21849
|
|
|
21349
21850
|
## Form-Associated API
|
|
21350
21851
|
|
|
@@ -21736,12 +22237,11 @@ define('profile-name', {
|
|
|
21736
22237
|
|
|
21737
22238
|
## directives
|
|
21738
22239
|
|
|
21739
|
-
Ore includes `each`, `classMap`, `styleMap`, `when`, `live`,
|
|
22240
|
+
Ore includes `each`, `classMap`, `styleMap`, `when`, and `model` — the everyday template directives most components need, imported directly from `@vielzeug/ore` alongside `define`/`html`. The advanced/niche ones (`live`, `raw`) live behind a separate `@vielzeug/ore/directives` import — see below.
|
|
21740
22241
|
|
|
21741
22242
|
```ts
|
|
21742
22243
|
import { signal } from '@vielzeug/ripple';
|
|
21743
|
-
import { classMap, each, styleMap, when } from '@vielzeug/ore
|
|
21744
|
-
import { define, html } from '@vielzeug/ore';
|
|
22244
|
+
import { classMap, define, each, html, styleMap, when } from '@vielzeug/ore';
|
|
21745
22245
|
|
|
21746
22246
|
define('task-list', {
|
|
21747
22247
|
setup() {
|
|
@@ -21913,8 +22413,7 @@ define('button-wrapper', {
|
|
|
21913
22413
|
## slots and emits
|
|
21914
22414
|
|
|
21915
22415
|
```ts
|
|
21916
|
-
import { when } from '@vielzeug/ore
|
|
21917
|
-
import { define, html, useEmit, useSlots } from '@vielzeug/ore';
|
|
22416
|
+
import { define, html, useEmit, useSlots, when } from '@vielzeug/ore';
|
|
21918
22417
|
|
|
21919
22418
|
define('card-with-footer', {
|
|
21920
22419
|
setup(_props) {
|
|
@@ -26841,7 +27340,7 @@ label.dispose();
|
|
|
26841
27340
|
- **`.replace(fn)`** — derive next state from current via a function; same-reference return is a no-op
|
|
26842
27341
|
- **`.reset()`** — restore the initial state baseline
|
|
26843
27342
|
- **`.lens(path)`** — cached writable `Signal` for a property or dot-path; writes produce an immutable copy
|
|
26844
|
-
- **`storeWithHistory(storeOrInit, options?)`** — store with explicit snapshot history; accepts an existing `Store` (not owned) or a plain object; call `.push()` / `.pushNamed(label)` to save checkpoints; `undo()`, `redo()`, `historyAt(i)` returns `HistoryEntry`; reactive `canUndo` / `canRedo`
|
|
27343
|
+
- **`storeWithHistory(storeOrInit, options?)`** — store with explicit snapshot history; accepts an existing `Store` (not owned) or a plain object; call `.push()` / `.pushNamed(label)` to save checkpoints; `undo()`, `redo()`, `historyAt(i)` returns `HistoryEntry`; reactive `canUndo` / `canRedo`; import via `@vielzeug/ripple/history`
|
|
26845
27344
|
- **`getDevToolsHook()`** — returns the currently installed DevTools hook, or `null`; install via `@vielzeug/ripple/devtools`
|
|
26846
27345
|
- **Glitch-free propagation** — computed signals propagate in dependency order; effects always observe a consistent snapshot
|
|
26847
27346
|
- **Infinite loop detection** — built-in guard against effect re-entry cycles (100 iterations default)
|
|
@@ -26885,7 +27384,7 @@ label.dispose();
|
|
|
26885
27384
|
| `scope()` | Isolated cleanup context | Sync | Must call `scope.run()` to activate; `dispose()` is LIFO |
|
|
26886
27385
|
| `debugEffect()` | Effect that logs changed sources before re-run | Sync | Sub-path only: `@vielzeug/ripple/devtools`; tree-shaken from production |
|
|
26887
27386
|
| `store()` | Create object-like state container | Sync | Store is a branded signal; use `.patch()`, `.replace()`, `.reset()` |
|
|
26888
|
-
| `storeWithHistory()` | Store with snapshot-based undo/redo history | Sync |
|
|
27387
|
+
| `storeWithHistory()` | Store with snapshot-based undo/redo history | Sync | Sub-path only: `@vielzeug/ripple/history`; call `.push()` / `.pushNamed()` explicitly to record a checkpoint; `maxHistory` caps the buffer |
|
|
26889
27388
|
| `installDevTools()` | Install DevTools observation hook | Sync | Sub-path only: `@vielzeug/ripple/devtools`; pass `null` to uninstall |
|
|
26890
27389
|
| `getDevToolsHook()` | Return current DevTools hook | Sync | Returns `null` if none installed |
|
|
26891
27390
|
| `isSignal()` | Type guard for any signal/computed/store | Sync | Uses an internal symbol marker, not duck-typing |
|
|
@@ -26898,6 +27397,7 @@ label.dispose();
|
|
|
26898
27397
|
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26899
27398
|
| `@vielzeug/ripple` | All core exports and types |
|
|
26900
27399
|
| `@vielzeug/ripple/devtools` | `installDevTools`, `debugEffect`, and hook types (`RippleDevToolsHook`, `WriteEvent`, `NamedEvent`, `DisposeEvent`, `MutateEvent`) — dev-only, tree-shaken from prod |
|
|
27400
|
+
| `@vielzeug/ripple/history` | `storeWithHistory` and its types (`StoreWithHistory`, `HistoryEntry`) — tree-shaken unless imported |
|
|
26901
27401
|
| `@vielzeug/ripple/ssr` | SSR tracking isolation helpers (`setTrackingProvider`, `createAsyncProvider`, `withProvider`, `runWithProvider`). Node.js only — do not import in browser builds. |
|
|
26902
27402
|
|
|
26903
27403
|
## Signal Primitives
|
|
@@ -27409,6 +27909,8 @@ Creates a reactive store for the given object state. Stores accept `effect()`, `
|
|
|
27409
27909
|
|
|
27410
27910
|
### `storeWithHistory`
|
|
27411
27911
|
|
|
27912
|
+
`storeWithHistory` is exported from `@vielzeug/ripple/history`, not the main entry point. This keeps it tree-shaken from bundles that never use it — for async commands or history over anything other than a `Store`, use `@vielzeug/ledger` instead.
|
|
27913
|
+
|
|
27412
27914
|
```ts
|
|
27413
27915
|
function storeWithHistory(
|
|
27414
27916
|
storeOrInitial: Store | T,
|
|
@@ -27423,6 +27925,8 @@ The initial state is saved as the first snapshot automatically. Snapshots are de
|
|
|
27423
27925
|
**Ownership:** when called with an initial value (`T`), the adapter creates and owns the underlying store — `dispose()` also disposes it. When called with an existing `Store`, the adapter does **not** own it — `dispose()` leaves the store alive.
|
|
27424
27926
|
|
|
27425
27927
|
```ts
|
|
27928
|
+
import { storeWithHistory } from '@vielzeug/ripple/history';
|
|
27929
|
+
|
|
27426
27930
|
const editor = storeWithHistory({ text: '' }, { maxHistory: 100 });
|
|
27427
27931
|
|
|
27428
27932
|
editor.patch({ text: 'hello' }); // direct — StoreWithHistory extends Store
|
|
@@ -28051,6 +28555,20 @@ batch(() => {
|
|
|
28051
28555
|
|
|
28052
28556
|
Nested `batch()` calls merge into the outermost — only one flush occurs.
|
|
28053
28557
|
|
|
28558
|
+
## Error Handling
|
|
28559
|
+
|
|
28560
|
+
Ripple has no single error-handling mode — each primitive picks the surface that fits how it's normally consumed. This is one deliberate spectrum, not three unrelated designs:
|
|
28561
|
+
|
|
28562
|
+
| Primitive | Failure surface | Why |
|
|
28563
|
+
| ------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
28564
|
+
| `signal()` / `computed()` | Throws synchronously | Sync code — the caller is already in a position to catch |
|
|
28565
|
+
| `effect()` | Throws synchronously (rethrown from the run) | Same as above — no async gap between cause and observation |
|
|
28566
|
+
| `watch()` | Throws synchronously on an invalid callback return | Programmer-error guard, not a runtime failure mode |
|
|
28567
|
+
| `effectAsync()` | Routes to `onError` (default: rethrown via `queueMicrotask`) | There's no synchronous caller left to catch by the time the factory rejects — needs an explicit escape hatch |
|
|
28568
|
+
| `resource()` | Never throws — lands in `ResourceState.status === 'error'` | Failures are render state (show an error UI), not exceptions to unwind past |
|
|
28569
|
+
|
|
28570
|
+
Rule of thumb: if you're inside a synchronous callback, ripple throws (`RippleError` subtypes — see [Errors](#errors)). If the failure only exists after an `await`, ripple gives you a place to observe it instead of throwing into a call stack that's already gone — `onError` for `effectAsync()`, `ResourceState.error` for `resource()`.
|
|
28571
|
+
|
|
28054
28572
|
### Usage Guide
|
|
28055
28573
|
|
|
28056
28574
|
## Basic Usage
|
|
@@ -28469,7 +28987,7 @@ effect(() => {
|
|
|
28469
28987
|
`storeWithHistory(initial, options?)` wraps a store with snapshot-based undo/redo. Mutations do **not** automatically push snapshots — call `.push()` (or `.pushNamed(label)`) explicitly after each logical change. History navigation with `undo()` and `redo()` never re-runs logic — it replays snapshots directly.
|
|
28470
28988
|
|
|
28471
28989
|
```ts
|
|
28472
|
-
import { storeWithHistory } from '@vielzeug/ripple';
|
|
28990
|
+
import { storeWithHistory } from '@vielzeug/ripple/history';
|
|
28473
28991
|
|
|
28474
28992
|
const editor = storeWithHistory({ text: '', cursor: 0 }, { maxHistory: 100 });
|
|
28475
28993
|
|
|
@@ -29092,7 +29610,6 @@ effect(() => {
|
|
|
29092
29610
|
- Scope & onCleanup (id: `scope-cleanup`)
|
|
29093
29611
|
- Scope — setup shorthand (id: `scope-setup`)
|
|
29094
29612
|
- Store — patch, lens & computed (id: `store-basics`)
|
|
29095
|
-
- Store History — Undo/Redo (id: `store-history`)
|
|
29096
29613
|
- Store — fine-grained lens reactivity (id: `store-lenses`)
|
|
29097
29614
|
- Store - Todo List (id: `store-todo-list`)
|
|
29098
29615
|
- Watch, Lens & Map (id: `watch-and-subscribe`)
|
|
@@ -34241,7 +34758,7 @@ define('virtual-list', {
|
|
|
34241
34758
|
|
|
34242
34759
|
**Category:** data
|
|
34243
34760
|
**Keywords:** pagination, filtering, sorting, search, data-source, query, remote, local, cursor, infinite-scroll
|
|
34244
|
-
**Key exports:** createLocalSource, createRemoteSource, createCursorSource, createInfiniteSource, deriveSource, mergeSource,
|
|
34761
|
+
**Key exports:** createLocalSource, createRemoteSource, createCursorSource, createInfiniteSource, deriveSource, mergeSource, SourcererDisposedError, SourcererError, SourcererTimeoutError, sourceState, itemRange, prefetchSource (+38 more)
|
|
34245
34762
|
**Related:** courier, ripple, wayfinder
|
|
34246
34763
|
|
|
34247
34764
|
### Overview
|
|
@@ -34341,6 +34858,8 @@ console.log(source.current, source.meta.totalItems);
|
|
|
34341
34858
|
| `createCursorSource()` | Server fetch | Cursor tokens | `patch()`, `ready()`, `queryKey` |
|
|
34342
34859
|
| `createInfiniteSource()` | Server fetch | Append (`loadMore`) | `patch()`, `loadedPages`, `ready()`, `queryKey` |
|
|
34343
34860
|
|
|
34861
|
+
`@vielzeug/sourcerer/devtools`: opt-in `debugSource()` for `console.debug` state-transition logging across any source type, tree-shaken from production.
|
|
34862
|
+
|
|
34344
34863
|
## Documentation
|
|
34345
34864
|
|
|
34346
34865
|
- [Usage Guide](./usage.md)
|
|
@@ -34365,10 +34884,9 @@ console.log(source.current, source.meta.totalItems);
|
|
|
34365
34884
|
| `createInfiniteSource()` | Async append-mode (infinite scroll) collection | Async | `loadMore()` is a no-op once `meta.hasMore` is `false` |
|
|
34366
34885
|
| `deriveSource()` | Create a reactive projection of another source | Sync | Derived source disposes automatically when parent disposes |
|
|
34367
34886
|
| `mergeSource()` | Combine multiple sources into one `MergedSource` | Sync | No `meta` field — returned type is `MergedSource`, not `ReactiveSource` |
|
|
34368
|
-
| `applyQuery()` | Apply a partial query patch to any source with `patch()` — fires one fetch | Async | Ignores `page` on Cursor/InfiniteSource — no page concept there |
|
|
34369
34887
|
| `SourcererError` | Base error class for all sourcerer errors; carries `message`, `cause`, `context`, `attempt` | Class | Extends `Error`; access context via getters, not object spread |
|
|
34370
|
-
| `
|
|
34371
|
-
| `
|
|
34888
|
+
| `SourcererTimeoutError` | Error thrown when `ready()` times out; has `timeoutMs` property | Class | Extends `SourcererError`; also caught by `instanceof SourcererError` |
|
|
34889
|
+
| `SourcererDisposedError` | Error thrown by `ready()` when the source is disposed | Class | Extends `SourcererError`; catch separately from `SourcererTimeoutError` if needed |
|
|
34372
34890
|
| `sourceState()` | Derive a discriminated union (`loading`/`error`/`success`) from any source | Sync | Returns `'loading'` when `isSearchPending` is true too |
|
|
34373
34891
|
| `itemRange()` | Compute 1-based display range from `SourceMeta` | Sync | Returns `{ start: 0, end: 0 }` when `totalItems === 0` |
|
|
34374
34892
|
| `prefetchSource()` | SSR: fetch first page, return serialisable snapshot; source is disposed immediately | Async | **Throws `SourcererError`** if fetch fails |
|
|
@@ -34384,11 +34902,12 @@ console.log(source.current, source.meta.totalItems);
|
|
|
34384
34902
|
| `SearchOptions` | Options bag for `search()` — only field is `immediate?: boolean` | Type | `search()` always returns `Promise`; debounced unless `{ immediate: true }` |
|
|
34385
34903
|
| `DecodeQueryOptions` | Options for `decodeQuery()` — `defaultLimit` and `strict` | Type | `strict: true` throws on malformed JSON; default silently drops it |
|
|
34386
34904
|
|
|
34387
|
-
## Package Entry
|
|
34905
|
+
## Package Entry Points
|
|
34388
34906
|
|
|
34389
|
-
| Import
|
|
34390
|
-
|
|
|
34391
|
-
| `@vielzeug/sourcerer`
|
|
34907
|
+
| Import | Purpose |
|
|
34908
|
+
| -------------------------------- | ------------------------------------------------------ |
|
|
34909
|
+
| `@vielzeug/sourcerer` | Main exports and types |
|
|
34910
|
+
| `@vielzeug/sourcerer/devtools` | Opt-in `debugSource()` — tree-shaken from production |
|
|
34392
34911
|
|
|
34393
34912
|
## Core Factories
|
|
34394
34913
|
|
|
@@ -34586,7 +35105,7 @@ All methods return `Promise` unless noted.
|
|
|
34586
35105
|
| `patch(changes)` | Apply one or more query changes atomically — a single recompute for any combination of `limit`, `page`, `search`, `filter`, `sort` |
|
|
34587
35106
|
| `prev()` | Navigate to the previous page (no-op at first page) |
|
|
34588
35107
|
| `query` | Current state as a `SourceQuery` (`limit`/`page`/`search` only — filter/sort aren't part of the query snapshot) — read-only snapshot; stable between changes |
|
|
34589
|
-
| `ready(timeout?)` | Resolve when no async computation is pending and no debounce is scheduled; rejects with `
|
|
35108
|
+
| `ready(timeout?)` | Resolve when no async computation is pending and no debounce is scheduled; rejects with `SourcererDisposedError` if already disposed; optional timeout rejects with `SourcererTimeoutError` |
|
|
34590
35109
|
| `reset()` | Restore initial config and return to page 1 |
|
|
34591
35110
|
| `search(query, opts?)` | Always returns `Promise`. Debounced by default; pass `{ immediate: true }` to cancel debounce and await immediately |
|
|
34592
35111
|
| `setData(data)` | Replace the dataset and reset to page 1 |
|
|
@@ -34608,7 +35127,7 @@ All methods return `Promise` except `optimisticUpdate` and `subscribe`.
|
|
|
34608
35127
|
| `patch(changes)` | Apply one or more query changes atomically — a single fetch for any combination of `limit`, `page`, `search`, `filter`, `sort` |
|
|
34609
35128
|
| `prev()` | Previous page (no-op at first page) |
|
|
34610
35129
|
| `query` | Current state as a `RemoteSourceQuery` — read-only snapshot; stable between changes |
|
|
34611
|
-
| `ready(timeout?)` | Resolve when no requests are pending; rejects with `
|
|
35130
|
+
| `ready(timeout?)` | Resolve when no requests are pending; rejects with `SourcererDisposedError` if already disposed; optional timeout rejects with `SourcererTimeoutError` |
|
|
34612
35131
|
| `refresh()` | Re-fetch the current query |
|
|
34613
35132
|
| `reset()` | Restore initial config and refetch |
|
|
34614
35133
|
| `search(query, opts?)` | Always returns `Promise`. Debounced by default; pass `{ immediate: true }` to cancel debounce and await immediately |
|
|
@@ -34640,7 +35159,7 @@ optimisticUpdate(
|
|
|
34640
35159
|
| `patch(changes)` | Apply `limit` and/or `search` atomically — a single fetch; resets cursor position |
|
|
34641
35160
|
| `prev()` | Go back using `prevCursor` (no-op if none) |
|
|
34642
35161
|
| `query` | Current state as a `CursorSourceQuery` — read-only snapshot; stable between changes |
|
|
34643
|
-
| `ready(timeout?)` | Resolve when idle; rejects with `
|
|
35162
|
+
| `ready(timeout?)` | Resolve when idle; rejects with `SourcererDisposedError` if already disposed; optional timeout rejects with `SourcererTimeoutError` |
|
|
34644
35163
|
| `refresh()` | Re-fetch current cursor position |
|
|
34645
35164
|
| `reset()` | Clear cursors and fetch from the start |
|
|
34646
35165
|
| `search(query, opts?)` | Always returns `Promise`. Debounced by default; pass `{ immediate: true }` to cancel debounce and await. Resets cursor position. |
|
|
@@ -34656,33 +35175,22 @@ optimisticUpdate(
|
|
|
34656
35175
|
| `loadMore()` | Fetch the next page and append to `current` (no-op when `meta.hasMore === false`) |
|
|
34657
35176
|
| `patch(changes)` | Apply `limit` and/or `search` atomically — **clears items immediately** and fetches from page 1 |
|
|
34658
35177
|
| `query` | Current state as an `InfiniteSourceQuery` — read-only snapshot; stable between changes |
|
|
34659
|
-
| `ready(timeout?)` | Resolve when idle; rejects with `
|
|
35178
|
+
| `ready(timeout?)` | Resolve when idle; rejects with `SourcererDisposedError` if already disposed; optional timeout rejects with `SourcererTimeoutError` |
|
|
34660
35179
|
| `reset()` | Clear accumulated items **immediately** and fetch from page 1 |
|
|
34661
35180
|
| `search(query, opts?)` | Always returns `Promise`. Debounced by default — **clears items immediately**; fetch fires after debounce. Pass `{ immediate: true }` to skip the window. |
|
|
34662
35181
|
| `subscribe(listener)` | Subscribe; returns unsubscribe |
|
|
34663
35182
|
|
|
34664
35183
|
## Query Utilities
|
|
34665
35184
|
|
|
34666
|
-
|
|
34667
|
-
|
|
34668
|
-
```ts
|
|
34669
|
-
applyQuery): Promise }>(
|
|
34670
|
-
source: T,
|
|
34671
|
-
changes: Partial,
|
|
34672
|
-
): Promise
|
|
34673
|
-
```
|
|
34674
|
-
|
|
34675
|
-
Applies a partial `SourceQuery` (`limit`/`page`/`search`) patch to any source that exposes a compatible `patch()` — delegates directly to `source.patch(changes)`. Fires a single fetch or recomputation for any combination of changed fields. No-op when `changes` is empty or all values are unchanged, per each source's own `patch()` implementation.
|
|
34676
|
-
|
|
34677
|
-
`CursorSource` and `InfiniteSource` have no page-number concept (keyset/append navigation) — their `patch()` only reads `limit`/`search`, so a `page` field from `decodeQuery()` output is silently ignored on those two source types.
|
|
35185
|
+
Restoring URL-decoded state onto a source is a direct `source.patch(decodeQuery(...))` call — no separate wrapper function. `CursorSource` and `InfiniteSource` have no page-number concept (keyset/append navigation), so their `patch()` type only accepts `limit`/`search`; passing a `page` field from `decodeQuery()`'s output to either is a compile-time error, not a silent no-op.
|
|
34678
35186
|
|
|
34679
35187
|
**Example:**
|
|
34680
35188
|
|
|
34681
35189
|
```ts
|
|
34682
|
-
import {
|
|
35190
|
+
import { decodeQuery } from '@vielzeug/sourcerer';
|
|
34683
35191
|
|
|
34684
35192
|
const q = decodeQuery(new URLSearchParams(location.search));
|
|
34685
|
-
await
|
|
35193
|
+
await source.patch(q);
|
|
34686
35194
|
```
|
|
34687
35195
|
|
|
34688
35196
|
---
|
|
@@ -34701,13 +35209,13 @@ class SourcererError extends Error {
|
|
|
34701
35209
|
}
|
|
34702
35210
|
```
|
|
34703
35211
|
|
|
34704
|
-
Base class for all sourcerer errors. Thrown (and stored as `meta.error`) when a fetch fails. `cause` is the original thrown value. `
|
|
35212
|
+
Base class for all sourcerer errors. Thrown (and stored as `meta.error`) when a fetch fails. `cause` is the original thrown value. `SourcererTimeoutError` and `SourcererDisposedError` both extend this class, so a single `instanceof SourcererError` check covers all sourcerer errors.
|
|
34705
35213
|
|
|
34706
|
-
### `
|
|
35214
|
+
### `SourcererTimeoutError`
|
|
34707
35215
|
|
|
34708
35216
|
```ts
|
|
34709
|
-
class
|
|
34710
|
-
readonly name = '
|
|
35217
|
+
class SourcererTimeoutError extends SourcererError {
|
|
35218
|
+
readonly name = 'SourcererTimeoutError';
|
|
34711
35219
|
readonly timeoutMs: number;
|
|
34712
35220
|
// message: 'Source.ready() timed out after Nms'
|
|
34713
35221
|
}
|
|
@@ -34715,11 +35223,11 @@ class SourceTimeoutError extends SourcererError {
|
|
|
34715
35223
|
|
|
34716
35224
|
Thrown by `ready(timeout)` when the timeout expires before the source becomes idle. Also caught by `instanceof SourcererError`.
|
|
34717
35225
|
|
|
34718
|
-
### `
|
|
35226
|
+
### `SourcererDisposedError`
|
|
34719
35227
|
|
|
34720
35228
|
```ts
|
|
34721
|
-
class
|
|
34722
|
-
readonly name = '
|
|
35229
|
+
class SourcererDisposedError extends SourcererError {
|
|
35230
|
+
readonly name = 'SourcererDisposedError';
|
|
34723
35231
|
// message: 'Source disposed while waiting for ready()'
|
|
34724
35232
|
}
|
|
34725
35233
|
```
|
|
@@ -34899,11 +35407,34 @@ Not generic — `filter`/`sort` on the result are always typed `unknown`. Narrow
|
|
|
34899
35407
|
**Example:**
|
|
34900
35408
|
|
|
34901
35409
|
```ts
|
|
34902
|
-
import {
|
|
35410
|
+
import { decodeQuery } from '@vielzeug/sourcerer';
|
|
34903
35411
|
|
|
34904
35412
|
// Pass URLSearchParams directly — filter/sort come back as `unknown`, narrow before use
|
|
34905
35413
|
const query = decodeQuery(new URLSearchParams(location.search), { defaultLimit: 20 });
|
|
34906
|
-
await
|
|
35414
|
+
await source.patch(query);
|
|
35415
|
+
```
|
|
35416
|
+
|
|
35417
|
+
## Devtools (`@vielzeug/sourcerer/devtools`)
|
|
35418
|
+
|
|
35419
|
+
```ts
|
|
35420
|
+
debugSource>(
|
|
35421
|
+
source: { current: readonly unknown[]; meta: TMeta; subscribe(listener: () => void): () => void },
|
|
35422
|
+
options?: { label?: string },
|
|
35423
|
+
): () => void
|
|
35424
|
+
```
|
|
35425
|
+
|
|
35426
|
+
Attaches a `console.debug`-based observer to any source (or `deriveSource`/`mergeSource` result) — logs a line per changed `meta` field (works generically across `SourceMeta`/`CursorMeta`/`InfiniteMeta`) and `current`'s item count. Development-only by default; a no-op when `__SOURCERER_PROD__` is set. Import from the dedicated `/devtools` sub-path so it's tree-shaken from production bundles.
|
|
35427
|
+
|
|
35428
|
+
```ts
|
|
35429
|
+
import { createRemoteSource } from '@vielzeug/sourcerer';
|
|
35430
|
+
import { debugSource } from '@vielzeug/sourcerer/devtools';
|
|
35431
|
+
|
|
35432
|
+
const source = createRemoteSource({ fetch: fetchUsers, limit: 20 });
|
|
35433
|
+
const detach = debugSource(source, { label: 'users' });
|
|
35434
|
+
// [sourcerer:devtools:users] meta.isLoading: false → true
|
|
35435
|
+
// [sourcerer:devtools:users] current: 0 → 20 items
|
|
35436
|
+
|
|
35437
|
+
detach(); // stop logging
|
|
34907
35438
|
```
|
|
34908
35439
|
|
|
34909
35440
|
## Types
|
|
@@ -35007,50 +35538,50 @@ type FetchEvent = Readonly;
|
|
|
35007
35538
|
|
|
35008
35539
|
See [Error Utilities > `SourcererError`](#sourcererror) above.
|
|
35009
35540
|
|
|
35010
|
-
### `
|
|
35541
|
+
### `SourcererTimeoutError`
|
|
35011
35542
|
|
|
35012
35543
|
```ts
|
|
35013
|
-
class
|
|
35014
|
-
readonly name = '
|
|
35544
|
+
class SourcererTimeoutError extends SourcererError {
|
|
35545
|
+
readonly name = 'SourcererTimeoutError';
|
|
35015
35546
|
readonly timeoutMs: number;
|
|
35016
35547
|
// message: 'Source.ready() timed out after Nms'
|
|
35017
35548
|
}
|
|
35018
35549
|
```
|
|
35019
35550
|
|
|
35020
|
-
Thrown by `ready(timeout)` when the source has not become idle within the specified `timeout` milliseconds. Check with `instanceof
|
|
35551
|
+
Thrown by `ready(timeout)` when the source has not become idle within the specified `timeout` milliseconds. Check with `instanceof SourcererTimeoutError` for typed catch blocks:
|
|
35021
35552
|
|
|
35022
35553
|
```ts
|
|
35023
|
-
import {
|
|
35554
|
+
import { SourcererTimeoutError } from '@vielzeug/sourcerer';
|
|
35024
35555
|
|
|
35025
35556
|
try {
|
|
35026
35557
|
await source.ready(5000);
|
|
35027
35558
|
} catch (err) {
|
|
35028
|
-
if (err instanceof
|
|
35559
|
+
if (err instanceof SourcererTimeoutError) {
|
|
35029
35560
|
console.warn('Source did not load in time:', err.message);
|
|
35030
35561
|
}
|
|
35031
35562
|
}
|
|
35032
35563
|
```
|
|
35033
35564
|
|
|
35034
|
-
### `
|
|
35565
|
+
### `SourcererDisposedError`
|
|
35035
35566
|
|
|
35036
35567
|
```ts
|
|
35037
|
-
class
|
|
35038
|
-
readonly name = '
|
|
35568
|
+
class SourcererDisposedError extends SourcererError {
|
|
35569
|
+
readonly name = 'SourcererDisposedError';
|
|
35039
35570
|
// message: 'Source disposed while waiting for ready()'
|
|
35040
35571
|
}
|
|
35041
35572
|
```
|
|
35042
35573
|
|
|
35043
|
-
Thrown by `ready()` when `dispose()` is called on the source while a `ready()` call is still pending. Use `instanceof
|
|
35574
|
+
Thrown by `ready()` when `dispose()` is called on the source while a `ready()` call is still pending. Use `instanceof SourcererDisposedError` to distinguish it from `SourcererTimeoutError`:
|
|
35044
35575
|
|
|
35045
35576
|
```ts
|
|
35046
|
-
import {
|
|
35577
|
+
import { SourcererDisposedError, SourcererTimeoutError } from '@vielzeug/sourcerer';
|
|
35047
35578
|
|
|
35048
35579
|
try {
|
|
35049
35580
|
await source.ready(5000);
|
|
35050
35581
|
} catch (err) {
|
|
35051
|
-
if (err instanceof
|
|
35582
|
+
if (err instanceof SourcererDisposedError) {
|
|
35052
35583
|
// source was torn down — skip cleanup
|
|
35053
|
-
} else if (err instanceof
|
|
35584
|
+
} else if (err instanceof SourcererTimeoutError) {
|
|
35054
35585
|
console.warn('timed out');
|
|
35055
35586
|
}
|
|
35056
35587
|
}
|
|
@@ -35133,13 +35664,13 @@ await source.reset(); // restore initial filter/sort, reset to page 1
|
|
|
35133
35664
|
|
|
35134
35665
|
### Restoring from URL state
|
|
35135
35666
|
|
|
35136
|
-
Use `
|
|
35667
|
+
Use `decodeQuery()` + `source.patch()` to restore URL-decoded state in a single atomic recompute.
|
|
35137
35668
|
|
|
35138
35669
|
```ts
|
|
35139
|
-
import {
|
|
35670
|
+
import { decodeQuery } from '@vielzeug/sourcerer';
|
|
35140
35671
|
|
|
35141
35672
|
const query = decodeQuery(new URLSearchParams(location.search), { defaultLimit: 10 });
|
|
35142
|
-
await
|
|
35673
|
+
await source.patch(query);
|
|
35143
35674
|
```
|
|
35144
35675
|
|
|
35145
35676
|
## Remote Source
|
|
@@ -35222,13 +35753,13 @@ await source.refresh(); // re-fetch current query
|
|
|
35222
35753
|
|
|
35223
35754
|
### Restoring from URL state
|
|
35224
35755
|
|
|
35225
|
-
`
|
|
35756
|
+
`source.patch()` is a no-op when `changes` is empty — safe to call on every page load.
|
|
35226
35757
|
|
|
35227
35758
|
```ts
|
|
35228
|
-
import {
|
|
35759
|
+
import { decodeQuery } from '@vielzeug/sourcerer';
|
|
35229
35760
|
|
|
35230
35761
|
const query = decodeQuery(new URLSearchParams(location.search), { defaultLimit: 25 });
|
|
35231
|
-
await
|
|
35762
|
+
await source.patch(query);
|
|
35232
35763
|
```
|
|
35233
35764
|
|
|
35234
35765
|
### Optimistic updates
|
|
@@ -35331,13 +35862,13 @@ await source.reset(); // clear all, restart from page 1
|
|
|
35331
35862
|
|
|
35332
35863
|
### Restoring from URL state
|
|
35333
35864
|
|
|
35334
|
-
Use `
|
|
35865
|
+
Use `decodeQuery()` + `source.patch()` to restore `limit` and `search`. `patch()` clears accumulated items and refetches from page 1 if any value changed.
|
|
35335
35866
|
|
|
35336
35867
|
```ts
|
|
35337
|
-
import {
|
|
35868
|
+
import { decodeQuery } from '@vielzeug/sourcerer';
|
|
35338
35869
|
|
|
35339
35870
|
const query = decodeQuery(new URLSearchParams(location.search), { defaultLimit: 20 });
|
|
35340
|
-
await
|
|
35871
|
+
await source.patch({ limit: query.limit, search: query.search });
|
|
35341
35872
|
```
|
|
35342
35873
|
|
|
35343
35874
|
## Error Handling
|
|
@@ -35446,7 +35977,7 @@ const params = encodeQuery(source.query);
|
|
|
35446
35977
|
|
|
35447
35978
|
// Restore from URLSearchParams directly
|
|
35448
35979
|
const query = decodeQuery(new URLSearchParams(location.search), { defaultLimit: 25 });
|
|
35449
|
-
await
|
|
35980
|
+
await source.patch(query);
|
|
35450
35981
|
```
|
|
35451
35982
|
|
|
35452
35983
|
`decodeQuery` is fault-tolerant by default — malformed `filter`/`sort` JSON is silently dropped. Pass `{ strict: true }` to throw instead.
|
|
@@ -35607,12 +36138,12 @@ effect(() => {
|
|
|
35607
36138
|
- Pass the `AbortSignal` from the `fetch` callback to your HTTP client so superseded requests are cancelled.
|
|
35608
36139
|
- Call `ready()` in server-side rendering or test setup — not in every render cycle.
|
|
35609
36140
|
- Always call the unsubscribe function returned by `subscribe()` when the component is torn down.
|
|
35610
|
-
- For URL sync, use `decodeQuery()` + `
|
|
36141
|
+
- For URL sync, use `decodeQuery()` + `source.patch()` rather than reconstructing source state from params manually.
|
|
35611
36142
|
- Use `staleTime` with `refreshInterval` for stale-while-revalidate patterns on dashboards.
|
|
35612
36143
|
- If you use `optimisticUpdate()`, call `refresh()` after mutation confirmation; it bypasses `staleTime` while optimistic state is active.
|
|
35613
36144
|
- Only one `optimisticUpdate()` can be active at a time — always handle the thrown error or check before calling.
|
|
35614
36145
|
- When using `decodeQuery()`, validate the parsed `filter` and `sort` with a type guard before passing to the server — they are returned as-is without runtime validation.
|
|
35615
|
-
- For infinite sources, pass `{ limit: query.limit, search: query.search }` to `
|
|
36146
|
+
- For infinite sources, pass `{ limit: query.limit, search: query.search }` to `source.patch()` for URL state sync — `page` isn't part of `InfiniteSource`'s `patch()` type at all, since items accumulate across pages rather than jumping to one.
|
|
35616
36147
|
|
|
35617
36148
|
### Examples
|
|
35618
36149
|
|
|
@@ -35639,7 +36170,7 @@ effect(() => {
|
|
|
35639
36170
|
- LocalSource patch() with filter/sort (id: `local-source-patch`)
|
|
35640
36171
|
- Presets (filterContains, filterEquals, filterRange, sortBy) (id: `presets`)
|
|
35641
36172
|
- Remote Source (id: `remote-source`)
|
|
35642
|
-
- sourceState &
|
|
36173
|
+
- sourceState & SourcererTimeoutError (id: `source-state`)
|
|
35643
36174
|
|
|
35644
36175
|
|
|
35645
36176
|
---
|