wcstack 3.5.4 → 4.0.0-rc.3
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/README.md +123 -15
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ Do not guess at syntax that is not documented here. wcstack has little presence
|
|
|
23
23
|
|
|
24
24
|
Both are spelled out under [What does not work](#what-does-not-work).
|
|
25
25
|
|
|
26
|
+
This guide describes **`@wcstack/state` 4.0** and the packages released with it. Upgrading an existing 3.x app: read the [3.x → 4.0 migration guide](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.md) instead.
|
|
27
|
+
|
|
26
28
|
---
|
|
27
29
|
|
|
28
30
|
## Loading wcstack — single packages, or this package's SPA-core bundle
|
|
@@ -49,7 +51,18 @@ Loading the bundle alongside an individual package's `/auto` is safe: whichever
|
|
|
49
51
|
|
|
50
52
|
If you want npm packages for local development, install the individual ones (`@wcstack/state`, `@wcstack/router`, …); this package publishes only the bundle and this guide.
|
|
51
53
|
|
|
52
|
-
`@wcstack/state` also ships **split entries** for a page that deliberately leaves features out
|
|
54
|
+
`@wcstack/state` also ships **split entries** for a page that deliberately leaves features out. The full-package `/auto` above includes every feature, needs none of this, and stays the default. The features are `temporal` (`$watch` / `$stream`), `list-keys` (`$listKeys`), `scopes` (`bind-component`, `mount=`, DCC), `recursion` (`$recursion`), `ssr` (`enable-ssr`), `formats` (the locale formatting filters), `diagnostics` (full message sentences and development warnings) and `devtools`. A declaration whose feature is missing throws `[wcs/feature-not-installed]`. Two ways to load them:
|
|
55
|
+
|
|
56
|
+
- **The split auto entry**, one tag and no import map. `features="…"` on the document's root `<wcs-state>` names what must be there before any `<wcs-state>` starts (`scopes`) and the development aids (`diagnostics`, `devtools`); each state's `$features` names what it needs (`$features: ["temporal", "formats"]`), loaded before that state is built. An unknown name fails with `[wcs/feature-unknown]`.
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@<version>/dist/split/auto.js"></script>
|
|
60
|
+
<wcs-state features="scopes diagnostics">…</wcs-state>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **A bundler or an import map**: import `bootstrapState` and `installFeatures` from `@wcstack/state/core`, then call `installFeatures([...])` with the features from `@wcstack/state/features/<name>` **before** `bootstrapState()`.
|
|
64
|
+
|
|
65
|
+
Load the split files from jsDelivr's plain, version-pinned `/npm/…/dist/split/` paths or a bundler — **never `esm.run`**, which re-bundles a separate engine into each entry and breaks the split auto entry's relative loads. See `npm view @wcstack/state readme`.
|
|
53
66
|
|
|
54
67
|
---
|
|
55
68
|
|
|
@@ -178,7 +191,7 @@ Multiple bindings are separated by `;`:
|
|
|
178
191
|
| `attr.NAME` | Set an attribute (SVG-aware) |
|
|
179
192
|
| `radio` | Radio group (two-way) |
|
|
180
193
|
| `checkbox` | Checkbox group bound to an array (two-way) |
|
|
181
|
-
| `onclick`, `on*` | Event handler |
|
|
194
|
+
| `onclick`, `on*` | Event handler (common bubbling events are delegated to the root — see [Event handlers](#event-handlers)) |
|
|
182
195
|
| `.NAME` | Explicit property (3.1) — the same binding as `NAME:`, except it is never an event |
|
|
183
196
|
|
|
184
197
|
**A property whose name starts with `on` needs the dotted form (3.1).** `online: x` is parsed as an event binding: it listens for a `"line"` event and **never writes the element's `online` property**. Put a dot in front to bind the property itself:
|
|
@@ -196,11 +209,12 @@ Multiple bindings are separated by `;`:
|
|
|
196
209
|
| `#ro` | Read-only — disables the two-way write-back |
|
|
197
210
|
| `#prevent` | `event.preventDefault()` on handlers |
|
|
198
211
|
| `#stop` | `event.stopPropagation()` on handlers |
|
|
212
|
+
| `#direct` | On an `on*:` binding only: listen on the element itself instead of delegating to the root (see [Event handlers](#event-handlers)) |
|
|
199
213
|
| `#onchange` | Use `change` instead of `input` for two-way binding |
|
|
200
214
|
| `#init=<authority>` | Who wins the **initial** sync: `element` (the element's own value seeds the state — use with `<wcs-storage>` and monitors), `state`, `auto`, `none`. Without it the default comes from the member's `wcBindable` declaration |
|
|
201
215
|
| `#sync=<timing>` | When an element-authority binding reads the element: `call` (default — when the binding attaches) or `connect` (once the element is in the document) |
|
|
202
216
|
|
|
203
|
-
Combine after one `#`, comma separated: `value#ro,init=none: path`, `value#init=element,sync=connect: path`. A second `#` is a parse error.
|
|
217
|
+
Combine after one `#`, comma separated: `value#ro,init=none: path`, `value#init=element,sync=connect: path`, `onclick#direct,stop: save`. A second `#` is a parse error.
|
|
204
218
|
|
|
205
219
|
### Paths
|
|
206
220
|
|
|
@@ -213,13 +227,17 @@ Combine after one `#`, comma separated: `value#ro,init=none: path`, `value#init=
|
|
|
213
227
|
|
|
214
228
|
### Splitting state across files (`mount=`)
|
|
215
229
|
|
|
216
|
-
A second `<wcs-state mount="cart" src="./cart.js">` grafts its module onto the root tree at `cart`; the page then reads `cart.total
|
|
230
|
+
A second `<wcs-state mount="cart" src="./cart.js">` grafts its module onto the root tree at `cart`; the page then reads `cart.total`, and calls the volume's methods by path too (`onclick: cart.add`). A volume holds data, getters, methods and its own `$connectedCallback` / `$disconnectedCallback`, with `this` rooted at its mount path — nothing else:
|
|
217
231
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```
|
|
232
|
+
- **It cannot read root paths.** There is no injection on the volume element (`data-wcs="state.taxRate: …"` is refused). A value that combines both belongs in a root getter, written with full paths:
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
// root state
|
|
236
|
+
get cartTotalWithTax() { return this["cart.subtotal"] * (1 + this["settings.taxRate"]); }
|
|
237
|
+
```
|
|
221
238
|
|
|
222
|
-
|
|
239
|
+
- **Declarations go on the root state**, with full paths (`$watch: { "cart.total"(cur) { … } }`). A volume that declares `$watch`, `$listKeys`, `$renderedCallback`, `$stream`, `$recursion`, `$behavior` or `$features`, or carries an injection, is not grafted (`console.error` says why). `$commandTokens`, `$eventTokens`, `$on` and `$errorCallback` in a volume are ignored with a `console.warn`.
|
|
240
|
+
- The root's `$renderedCallback(paths)` receives the whole tree's paths (`cart.items.*.name`); filter them by prefix.
|
|
223
241
|
|
|
224
242
|
### Structural directives
|
|
225
243
|
|
|
@@ -232,6 +250,11 @@ Always on a `<template>` element:
|
|
|
232
250
|
<template data-wcs="else:"> ... </template>
|
|
233
251
|
```
|
|
234
252
|
|
|
253
|
+
- **`for:` takes no filters.** `for: items|take(2)` fails initialization with `[wcs/binding-syntax]` #121. Declare a getter that returns the list to show (`get firstTwo() { return this.items.slice(0, 2); }`) and loop over it (`for: firstTwo`).
|
|
254
|
+
- **`outerHTML:` / `outerText:` cannot be used inside `for:` / `if:` templates** (`[wcs/template-syntax]` #203): a row or branch keeps its nodes by position. Bind `innerHTML:` on a wrapper element instead.
|
|
255
|
+
- **Inside a row, a `*` means the row of the `for:` around it.** `{{ b.*.y }}` inside `for: a` throws `[wcs/wildcard-rank]` #1403; read another list's row in a getter with `$resolve(path, indexes)`.
|
|
256
|
+
- `{{ }}` and `$1` (the loop index) work inside templates. The elements 4.0 clones for rows and branches do not keep their `data-wcs` attribute, so do not select them with `[data-wcs…]` in CSS or tests — give them a class.
|
|
257
|
+
|
|
235
258
|
### Computed values
|
|
236
259
|
|
|
237
260
|
Plain getters. Wildcard getters compute per row, and `$getAll` aggregates across rows:
|
|
@@ -258,6 +281,22 @@ removeItem(event, index) {
|
|
|
258
281
|
}
|
|
259
282
|
```
|
|
260
283
|
|
|
284
|
+
**`on*:` handlers of `click`, `dblclick`, `input`, `change`, `submit`, `keydown`, `keyup`, `mousedown`, `mouseup`, `pointerdown` and `pointerup` are delegated**: one listener per event type sits on the root the state binds (the document, a shadow root, or a Light DOM component's host) and runs the handlers of the elements the event passed, innermost first. Other event types, and two-way bindings (`value:`, `checked:`), listen on the element. With delegation:
|
|
285
|
+
|
|
286
|
+
- `event.currentTarget` is the root, not the element. Find the element with `event.target.closest("li")`, or use the loop index or a wildcard path.
|
|
287
|
+
- `#stop` stops the outer `on*:` handlers, but not listeners your own code added to an ancestor (they already ran). Your own `stopPropagation()` on an ancestor keeps the `on*:` handlers inside it from running. An element moved under another root (a dialog moved to `document.body`) loses its handler.
|
|
288
|
+
|
|
289
|
+
Where a handler needs the element-level behaviour, write `on*#direct:`. The listener is then on the element itself: `currentTarget` is the element, `#stop` stops your ancestor listeners, an ancestor's `stopPropagation()` does not affect it, and it follows the element anywhere. It combines with `#prevent` / `#stop` (`onclick#direct,stop: save`). An outer `#direct` handler runs before the delegated handlers inside it, so an inner `#stop` stops it only if the inner binding is `#direct` too.
|
|
290
|
+
|
|
291
|
+
### Initialization and errors
|
|
292
|
+
|
|
293
|
+
- `<wcs-state>` loads its state, builds and applies every binding under its root, then runs `$connectedCallback`. By then the bindings exist, so `$connectedCallback` can emit commands to a defined element right away. A custom element whose class is not defined yet gets its property / `command.` / `eventToken.` bindings when it is defined: `await customElements.whenDefined("wcs-fetch")` before poking it.
|
|
294
|
+
- A state source that fails (a `src=` that throws, a `src="*.json"` that cannot be fetched, a `state="id"` with no `<script type="application/json" id="id">`) or a binding at page level that cannot be read (a syntax error, an unknown filter, #121, #203, #1403) fails initialization: `connectedCallbackPromise` rejects with the error, and the console reports it once — `[@wcstack/state] <wcs-state src="./state.js"> failed to initialize.` followed by the error. Run the lint (below) before you deploy.
|
|
295
|
+
- A `$connectedCallback` that throws or rejects is reported as `… $connectedCallback failed.`; `connectedCallbackPromise` rejects, but `getBindingsReady()` resolves — the page is bound.
|
|
296
|
+
- A binding that fails inside a `for:` / `if:` row, or that fails to apply later, goes to `$errorCallback(error, { path, bindingType, node })` when the state declares one, else to the console; the rest of the page keeps working.
|
|
297
|
+
- One root `<wcs-state>` per document or shadow root: a second one fails to initialize. Graft more state with `mount=`.
|
|
298
|
+
- On `/core` without the `diagnostics` feature, messages carry a code, a number and the values (`[@wcstack/state] [wcs/filter-unknown] #501 "uc"`) instead of the sentence: look the number up in [`docs/state-errors.md`](https://github.com/wcstack/wcstack/blob/main/docs/state-errors.md).
|
|
299
|
+
|
|
261
300
|
---
|
|
262
301
|
|
|
263
302
|
## What does not work
|
|
@@ -273,11 +312,27 @@ GOOD: <input data-wcs="onkeydown: add"> <!-- check event.key inside add()
|
|
|
273
312
|
BAD: <div data-wcs="for: items"> ... </div>
|
|
274
313
|
GOOD: <template data-wcs="for: items"> ... </template>
|
|
275
314
|
|
|
276
|
-
<!-- `{{ }}` outside a template causes FOUC. -->
|
|
315
|
+
<!-- `{{ }}` outside a template causes FOUC (the raw text shows until state loads). -->
|
|
277
316
|
BAD: <p>{{ count }}</p>
|
|
278
317
|
GOOD: <p><span data-wcs="textContent: count"></span></p>
|
|
318
|
+
GOOD: <p><!--@@: count--></p> <!-- comment binding: no FOUC, no extra element -->
|
|
319
|
+
|
|
320
|
+
<!-- `for:` takes no filters. -->
|
|
321
|
+
BAD: <template data-wcs="for: items|take(2)">
|
|
322
|
+
GOOD: <template data-wcs="for: firstTwo"> <!-- get firstTwo() { return this.items.slice(0, 2); } -->
|
|
323
|
+
|
|
324
|
+
<!-- In a delegated handler, currentTarget is the root. -->
|
|
325
|
+
BAD: select(event) { event.currentTarget.classList.add("on"); } <!-- with onclick: select -->
|
|
326
|
+
GOOD: select(event) { event.target.closest("li").classList.add("on"); }
|
|
327
|
+
GOOD: <li data-wcs="onclick#direct: select"> <!-- currentTarget is the <li> -->
|
|
328
|
+
|
|
329
|
+
<!-- Markup inside an element whose content a binding sets is not bound. -->
|
|
330
|
+
BAD: <div data-wcs="textContent: title"><b data-wcs="textContent: count"></b></div>
|
|
331
|
+
GOOD: <div><span data-wcs="textContent: title"></span><b data-wcs="textContent: count"></b></div>
|
|
279
332
|
```
|
|
280
333
|
|
|
334
|
+
A comment binding is `<!--@@: expr-->` (or `<!--@@wcs-text: expr-->`): the same text binding as `{{ expr }}`, filters included, at page level and inside templates; the comment is replaced by a text node. It binds even with `$behavior.enableMustache: false`. It is not a binding inside `<textarea>` or `<title>`, which the browser parses as text. The same goes for the children of `<noscript>` and `<iframe>`.
|
|
335
|
+
|
|
281
336
|
```javascript
|
|
282
337
|
// State must be REASSIGNED. In-place mutation is not tracked.
|
|
283
338
|
BAD: this.items.push(x);
|
|
@@ -290,28 +345,81 @@ GOOD: this["user.name"] = "new";
|
|
|
290
345
|
// Immutable array methods are the idiom.
|
|
291
346
|
this.items = this.items.toSpliced(index, 1);
|
|
292
347
|
this.items = this.items.map(t => t.id === id ? { ...t, done: true } : t);
|
|
348
|
+
|
|
349
|
+
// Writing a list element replaces the value AT THAT POSITION: the row (focus, typed text,
|
|
350
|
+
// <details> open state) stays. To move rows with their values, assign a new array —
|
|
351
|
+
// rows follow their objects.
|
|
352
|
+
const items = this.items.slice();
|
|
353
|
+
[items[0], items[1]] = [items[1], items[0]];
|
|
354
|
+
this.items = items;
|
|
355
|
+
|
|
356
|
+
// $scan, substr and the 3.2 old names are gone (see the table below).
|
|
357
|
+
BAD: $scan: { total: { from: "amount", initial: 0, fold: (sum, cur) => sum + cur } }
|
|
358
|
+
GOOD: total: 0, $watch: { amount(cur) { this.total = this.total + cur; } }
|
|
359
|
+
|
|
360
|
+
// bootstrapState() takes markup spelling only; behaviour is declared in the state.
|
|
361
|
+
BAD: bootstrapState({ enableMustache: false }); // throws
|
|
362
|
+
GOOD: export default { $behavior: { enableMustache: false }, /* … */ };
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Configuration
|
|
368
|
+
|
|
369
|
+
A page that loads `/auto` calls nothing. With the module entries (`@wcstack/state`, `@wcstack/state/core`), `bootstrapState(config)` takes only how the page **spells** the markup — `bindAttributeName`, `tagNames.state` / `tagNames.ssr`, `commentForPrefix` / `commentIfPrefix` / `commentElseIfPrefix` / `commentElsePrefix` — plus `locale` and `enableContractAnalyzer`. The default `locale` is `<html lang>`, read when the module is evaluated: write it in the markup.
|
|
370
|
+
|
|
371
|
+
How a state tree behaves, and which features it needs, is declared in the state:
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
export default {
|
|
375
|
+
$behavior: { enableMustache: false }, // enableMustache, sameValueGuard, enableDirectionalInitialSync — booleans, all true by default
|
|
376
|
+
$features: ["temporal"], // the features this state needs (checked; loaded by the split auto entry)
|
|
377
|
+
count: 0,
|
|
378
|
+
};
|
|
293
379
|
```
|
|
294
380
|
|
|
381
|
+
- `$behavior` covers the tree of the `<wcs-state>` that declares it, its volumes included. Every root state that needs it declares it — the page's root, a root in a shadow root, a mounted component, a DCC; nothing is inherited, and a volume cannot declare it. An unknown key or a non-boolean throws (#44); a re-set (`setInitialState()`) cannot change it (#45). It works in JSON states and on `/auto` pages too.
|
|
382
|
+
- **Every package's `bootstrapXxx(config)`** — `bootstrapState`, `bootstrapRouter`, `bootstrapFetch`, … — throws on an option it does not have, on a value whose type differs from the option's default, and on a `tagNames` key it does not define, and then applies nothing. An `undefined` value is skipped.
|
|
383
|
+
|
|
295
384
|
---
|
|
296
385
|
|
|
297
|
-
##
|
|
386
|
+
## User content and security
|
|
298
387
|
|
|
299
|
-
|
|
388
|
+
`@wcstack/state` does not sanitize values. Values are safe where they are text, and only there:
|
|
300
389
|
|
|
301
|
-
|
|
390
|
+
- **Bind user text with `textContent:`, `{{ }}` or a comment binding.** A value a binding writes is never read as a binding: `{{ }}` or `data-wcs` inside a value stays literal.
|
|
391
|
+
- **`innerHTML:` / `html:` only with HTML you sanitized** — install a sanitizing Trusted Types policy (`docs/csp.md` §7).
|
|
392
|
+
- **`href:`, `src:` and `attr.href:` accept `javascript:` URLs, and `attr.on*:` accepts handler strings.** state rejects neither: validate the value, and use a CSP whose `script-src` has no `'unsafe-inline'`.
|
|
393
|
+
- **Never put user content into the page's markup** — a server template that is not wcstack SSR, or HTML your code inserts. The page walk reads every `{{ … }}`, `data-wcs` attribute and comment binding (`<!--@@: x-->`) in the markup as a binding (client-side template injection), and `$behavior.enableMustache: false` does not stop comment bindings. 4.0 has no attribute that excludes a subtree from the walk (there is no `data-wcs-ignore`). Deliver such content as state data — a `<script type="application/json">` read with `state="id"`, a `src=` module, `fetch` — and bind it with `textContent:`.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Names 4.0 removed
|
|
398
|
+
|
|
399
|
+
Older names appear all over the training data. 4.0 rejects them: write the name in the left column.
|
|
400
|
+
|
|
401
|
+
| Write | Removed in 4.0 |
|
|
302
402
|
|---|---|
|
|
303
403
|
| `upper` · `lower` · `capitalize` | `uc` · `lc` · `cap` |
|
|
304
404
|
| `add` · `sub` | `inc` · `dec` |
|
|
305
405
|
| `toFixed` · `padStart` | `fix` · `pad` |
|
|
306
406
|
| `repeat` · `reverse` | `rep` · `rev` |
|
|
307
407
|
| `nullIfEmpty` | `null` |
|
|
408
|
+
| `slice(start, start + length)` (an end index, not a length) | `substr(start, length)` |
|
|
308
409
|
| `$stream` (async producer → state) | `$streams` |
|
|
309
410
|
| `$renderedCallback` (hook) | `$updatedCallback` |
|
|
411
|
+
| `$watch` (fold a path's changes) · `$on` (fold event tokens) into an ordinary key | `$scan` |
|
|
310
412
|
| `this.$dependOn(path)` · `this.$untracked(fn)` | `$trackDependency` · `$untrackDependency` |
|
|
311
413
|
|
|
312
|
-
|
|
414
|
+
An old filter name or `substr` fails initialization with `[wcs/filter-unknown]` (the full entries name the replacement); `$streams`, `$updatedCallback` and `$scan` throw when the state loads; `$trackDependency` / `$untrackDependency` throw when read. The lint reports the declarations and API names as errors, and the filters as `wcs/filter-unknown` warnings (a page may register filters of its own) — which still throw at run time. `$renderedCallback` reports the **bindings that were applied**, not every state change — use `$watch` for those.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Known limitations
|
|
419
|
+
|
|
420
|
+
- **One object at two positions of one list, or in two arrays no getter relates** (`backup = items; items = items.filter(…)`, both rendered): a write below one row does not reach the other row's bindings and row getters. Do not write below such a row; replace the object in the top-level list instead, as the app above does (`toggle()` reassigns `todos` with a new object for the changed item, and the checkbox is `checked#ro:`). Arrays a getter relates are fine: with `get shown()` returning a filtered copy of `todos`, a write below a row of either list reaches the other's row, `todos.*` readers and the filter.
|
|
313
421
|
|
|
314
|
-
|
|
422
|
+
Details: the migration guide, [§5](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.md#5-known-limitations).
|
|
315
423
|
|
|
316
424
|
---
|
|
317
425
|
|
|
@@ -325,7 +433,7 @@ npx @wcstack/lint index.html
|
|
|
325
433
|
npx @wcstack/lint --errors-only index.html
|
|
326
434
|
```
|
|
327
435
|
|
|
328
|
-
Exit code `0` means no error-severity finding, `1` means at least one, `2` means a usage or read failure. **Warnings and info diagnostics do not change the exit code** — `0` is not the same as "no findings", so read the output too (or pass `--strict`, which makes warnings exit `1` as well). Diagnostics carry stable `wcs/*` codes and `source:line:col` ranges.
|
|
436
|
+
Exit code `0` means no error-severity finding, `1` means at least one, `2` means a usage or read failure. **Warnings and info diagnostics do not change the exit code** — `0` is not the same as "no findings", so read the output too (or pass `--strict`, which makes warnings exit `1` as well). Two warnings throw at run time in 4.0 — `wcs/filter-unknown` and `wcs/wildcard-rank` — so treat them as errors. Diagnostics carry stable `wcs/*` codes (the same codes the runtime prints) and `source:line:col` ranges.
|
|
329
437
|
|
|
330
438
|
---
|
|
331
439
|
|