@wcstack/state 1.30.0 → 1.32.0
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.ja.md +265 -6
- package/README.md +267 -6
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +82 -3
- package/dist/index.esm.js +1441 -192
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +28 -8
- package/dist/parser.esm.js +28 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -769,19 +769,20 @@ export default {
|
|
|
769
769
|
+ this["regions.*.prefectures.*.cities.*.name"];
|
|
770
770
|
},
|
|
771
771
|
|
|
772
|
-
// Prefecture level — aggregate from cities
|
|
772
|
+
// Prefecture level — aggregate from cities. `indexes` omitted: it defaults to
|
|
773
|
+
// the loop context ([$1, $2]), so only this prefecture's cities are summed
|
|
773
774
|
get "regions.*.prefectures.*.totalPopulation"() {
|
|
774
|
-
return this.$getAll("regions.*.prefectures.*.cities.*.population"
|
|
775
|
+
return this.$getAll("regions.*.prefectures.*.cities.*.population")
|
|
775
776
|
.reduce((a, b) => a + b, 0);
|
|
776
777
|
},
|
|
777
778
|
|
|
778
|
-
// Region level — aggregate from prefectures
|
|
779
|
+
// Region level — aggregate from prefectures (context [$1] narrows to this region)
|
|
779
780
|
get "regions.*.totalPopulation"() {
|
|
780
|
-
return this.$getAll("regions.*.prefectures.*.totalPopulation"
|
|
781
|
+
return this.$getAll("regions.*.prefectures.*.totalPopulation")
|
|
781
782
|
.reduce((a, b) => a + b, 0);
|
|
782
783
|
},
|
|
783
784
|
|
|
784
|
-
// Top level —
|
|
785
|
+
// Top level — no loop context; [] means "every match"
|
|
785
786
|
get totalPopulation() {
|
|
786
787
|
return this.$getAll("regions.*.totalPopulation", [])
|
|
787
788
|
.reduce((a, b) => a + b, 0);
|
|
@@ -872,6 +873,36 @@ Two-way binding works with path setters — editing the input calls the setter,
|
|
|
872
873
|
|
|
873
874
|
4. **Direct index access** — You can also access specific elements by numeric index: `this["users.0.name"]` resolves as `users[0].name` without needing loop context.
|
|
874
875
|
|
|
876
|
+
### Getters must be pure with respect to state
|
|
877
|
+
|
|
878
|
+
A getter's cache is invalidated **only** through the dependency graph, and the graph only records what the getter read **through `this`**. Anything else a getter reads is invisible to invalidation, so the first value computed is the value you keep:
|
|
879
|
+
|
|
880
|
+
```javascript
|
|
881
|
+
// ❌ Never recomputes — nothing in the dependency graph ever changes
|
|
882
|
+
get stamp() { return `${this.label} @ ${Date.now()}`; } // Date.now() is untracked
|
|
883
|
+
get theme() { return document.body.dataset.theme; } // the DOM is untracked
|
|
884
|
+
get total() { return this.price * exchangeRate; } // a module variable is untracked
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
The rule: **read only through `this`, and don't write state or touch the DOM from a getter.** For the cases where an untracked input genuinely has to participate, put the input into state and assign to it (the normal path-assignment contract), or use the escape hatches:
|
|
888
|
+
|
|
889
|
+
| API | Use it for |
|
|
890
|
+
|---|---|
|
|
891
|
+
| `this.$trackDependency(path)` | Register an extra dependency so this getter is dirtied when that path changes |
|
|
892
|
+
| `this.$postUpdate(path)` | Announce that an untracked input changed, from outside the getter |
|
|
893
|
+
| `this.$untrackDependency(fn)` | Read a path *without* registering it as a dependency (the inverse) |
|
|
894
|
+
|
|
895
|
+
```javascript
|
|
896
|
+
// ✅ The clock ticks in state; the getter stays pure
|
|
897
|
+
export default {
|
|
898
|
+
now: Date.now(),
|
|
899
|
+
get stamp() { return `${this.label} @ ${this.now}`; },
|
|
900
|
+
$connectedCallback() { setInterval(() => { this.now = Date.now(); }, 1000); },
|
|
901
|
+
};
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
Getters that throw are not swallowed: the exception surfaces where the getter was evaluated (a binding apply, a `$watch` evaluation, or your own read).
|
|
905
|
+
|
|
875
906
|
### Loop Index Variables (`$1`, `$2`, ...)
|
|
876
907
|
|
|
877
908
|
Inside getters and event handlers, `this.$1`, `this.$2`, etc. provide the current loop iteration index (0-based value, 1-based naming):
|
|
@@ -913,6 +944,7 @@ Inside state objects (getters / methods), the following APIs are available via `
|
|
|
913
944
|
| API | Description |
|
|
914
945
|
|---|---|
|
|
915
946
|
| `this.$getAll(path, indexes?)` | Get all values matching a wildcard path |
|
|
947
|
+
| `this.$setAll(path, indexes, value, options?)` | Write to every address matching a wildcard path |
|
|
916
948
|
| `this.$resolve(path, indexes, value?)` | Resolve a wildcard path with specific indexes |
|
|
917
949
|
| `this.$postUpdate(path)` | Manually trigger update notification for a path |
|
|
918
950
|
| `this.$trackDependency(path)` | Manually register a dependency for cache invalidation |
|
|
@@ -938,6 +970,66 @@ export default {
|
|
|
938
970
|
};
|
|
939
971
|
```
|
|
940
972
|
|
|
973
|
+
`indexes` is a **prefix** over the path's wildcards: missing levels expand fully, and `[]` always means "every match". When `indexes` is **omitted**, it defaults to the enclosing loop context (`[$1, $2, ...]`), applied to the wildcard levels the path shares with that context:
|
|
974
|
+
|
|
975
|
+
```javascript
|
|
976
|
+
export default {
|
|
977
|
+
regions: [ /* { prefectures: [ { population: … }, … ] } */ ],
|
|
978
|
+
// Loop context [$1] — omission narrows to the current region
|
|
979
|
+
get "regions.*.total"() {
|
|
980
|
+
return this.$getAll("regions.*.prefectures.*.population").reduce((a, b) => a + b, 0);
|
|
981
|
+
},
|
|
982
|
+
// No loop context — omission expands everything (same as [])
|
|
983
|
+
get grandTotal() {
|
|
984
|
+
return this.$getAll("regions.*.total").reduce((a, b) => a + b, 0);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
Context levels deeper than the path needs are dropped (a `[$1, $2]` context narrows a one-wildcard path by `[$1]`). But if the path shares **no** wildcard level with a context that does hold loop indexes — say `$getAll("users.*.name")` inside a `regions.*` getter — `$getAll` **throws** instead of silently reading every user: the context indexes belong to a different list, and neither reusing nor ignoring them is what the author meant. Pass indexes explicitly there (`[]` for every match).
|
|
990
|
+
|
|
991
|
+
#### `$setAll` — Update Every Array Element In Place
|
|
992
|
+
|
|
993
|
+
`$setAll` is the write-side counterpart of `$getAll`: it writes to every address a wildcard path matches. The point is not brevity but **keeping the array itself**. Rebuilding it (`this.users = this.users.map(...)`) throws away the list indexes, the per-row getter caches, and the render diff; `$setAll` decomposes into in-place per-row writes instead, so the list identity survives.
|
|
994
|
+
|
|
995
|
+
```javascript
|
|
996
|
+
export default {
|
|
997
|
+
users: [{ selected: false }, { selected: false }],
|
|
998
|
+
|
|
999
|
+
toggleAll(e) {
|
|
1000
|
+
this.$setAll("users.*.selected", [], e.target.checked); // broadcast
|
|
1001
|
+
},
|
|
1002
|
+
invertAll() {
|
|
1003
|
+
this.$setAll("users.*.selected", [], cur => !cur); // mapper
|
|
1004
|
+
},
|
|
1005
|
+
rankTopThree() {
|
|
1006
|
+
// `undefined` skips that address — "leave this row alone"
|
|
1007
|
+
this.$setAll("users.*.score", [], (cur, i) => i < 3 ? cur * 2 : undefined);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Three forms, and the third one has to be asked for explicitly:
|
|
1013
|
+
|
|
1014
|
+
| Third argument | Meaning |
|
|
1015
|
+
|---|---|
|
|
1016
|
+
| a function | **mapper** — called as `(current, ...indexes)` per matched address |
|
|
1017
|
+
| anything else | **broadcast** — the same value is written everywhere, arrays included |
|
|
1018
|
+
| an array **plus** `{ spread: true }` | **spread** — one entry handed to each matched address, in match order |
|
|
1019
|
+
|
|
1020
|
+
Arrays broadcast by default because the target property may itself be array-valued — `$setAll("users.*.tags", [], ["admin"])` would otherwise be ambiguous. Opting into `{ spread: true }` removes the guesswork, and a length that does not equal the match count throws rather than silently misaligning.
|
|
1021
|
+
|
|
1022
|
+
`indexes` works exactly as in `$getAll` — a **prefix**, where missing levels mean "expand all of them" — but it is **required**. Writes get no implicit loop context, so inside a `for` template `this.$setAll("users.*.selected", [], true)` still means *every* user, never the current row.
|
|
1023
|
+
|
|
1024
|
+
```javascript
|
|
1025
|
+
this.$setAll("matrix.*.*", [0], 0); // row 0 only, every column
|
|
1026
|
+
this.$setAll("users.*", [], rows, { spread: true }); // replace each row, keep the array
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
`undefined` is never written — it means "skip this address" in all three forms, which keeps a mapper that forgets to `return` from wiping every row. Use `null` to clear. The return value is the number of addresses actually written.
|
|
1030
|
+
|
|
1031
|
+
One thing `$setAll` is not: a shortcut for the dependency walk. Rendering still coalesces into a single batch, but each write is enqueued individually, so the cost matches the hand-written loop it replaces. What it buys you is the preserved list, not fewer cycles.
|
|
1032
|
+
|
|
941
1033
|
#### `$resolve` — Access by Explicit Index
|
|
942
1034
|
|
|
943
1035
|
`$resolve` reads or writes a value at a specific wildcard index:
|
|
@@ -1632,6 +1724,48 @@ Key rules:
|
|
|
1632
1724
|
|
|
1633
1725
|
See [docs/streams.md](docs/streams.md) for the full contract — lifecycle and ownership, restart semantics, flush granularity, and the out-of-scope list.
|
|
1634
1726
|
|
|
1727
|
+
## Demand roots — what makes a getter run
|
|
1728
|
+
|
|
1729
|
+
Path getters are **lazy**. One that nobody reads is never evaluated. So "does this getter run?" is not answerable from the getter itself — it depends on **where the demand comes from**.
|
|
1730
|
+
|
|
1731
|
+
There are exactly **three** demand roots:
|
|
1732
|
+
|
|
1733
|
+
| Root | Lives in | Depends on rendering |
|
|
1734
|
+
|---|---|---|
|
|
1735
|
+
| **A live DOM binding** | `data-wcs` / mustache / comment bindings | **Yes** — remove the element and the demand goes with it |
|
|
1736
|
+
| **A `$watch` declaration** | the state | No (headless) |
|
|
1737
|
+
| **A `$streams` `args` function** | the state | No (evaluated on start and on every restart) |
|
|
1738
|
+
|
|
1739
|
+
**`$updatedCallback` is not a root.** It reports what the bindings did; it does not create demand.
|
|
1740
|
+
|
|
1741
|
+
### Rendering can change program semantics
|
|
1742
|
+
|
|
1743
|
+
Because the first root lives in the DOM, **an element you think of as display-only can be the actual subscription**. This one was hit for real, in [`examples/state-intersect-scroll`](../../examples/state-intersect-scroll):
|
|
1744
|
+
|
|
1745
|
+
```html
|
|
1746
|
+
<!-- Meant as display. It was the only demand root. -->
|
|
1747
|
+
<b data-wcs="textContent: $streamStatus.pageResult"></b>
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
```javascript
|
|
1751
|
+
// $updatedCallback is binding-driven — delete that <b> and the path stops
|
|
1752
|
+
// appearing in `paths`, so the feed silently stops committing.
|
|
1753
|
+
$updatedCallback(paths) {
|
|
1754
|
+
if (!paths.includes("$streamStatus.pageResult")) return;
|
|
1755
|
+
this.items = this.items.concat(this.pageResult.items);
|
|
1756
|
+
}
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
**The rule:** logic that must not depend on what is rendered belongs on a `$watch` (or a `$streams` `args`). Keep `$updatedCallback` for "follow what was drawn".
|
|
1760
|
+
|
|
1761
|
+
That example now uses `$watch`, and the `<b>` is display-only again. This shape — `$updatedCallback` testing a path that is not bound anywhere — is detected statically as **`wcs/updated-callback-unbound`**.
|
|
1762
|
+
|
|
1763
|
+
### The limitation that remains
|
|
1764
|
+
|
|
1765
|
+
Demand still comes from three separate places. **To know whether a getter is evaluated you have to inspect all three — every binding on the page, every `$watch`, and every `$streams` `args` — and reading the getter's definition will not tell you.** The linter and the DevTools wiring-coverage view exist to make a machine do that cross-check.
|
|
1766
|
+
|
|
1767
|
+
Note that a scalar getter named in `$watch` becomes **eager** (evaluated once at connect, then at the end of every batch touching its dependencies). Wildcard row getters do not become eager, because the first evaluation would walk the whole list.
|
|
1768
|
+
|
|
1635
1769
|
## Watch (`$watch`)
|
|
1636
1770
|
|
|
1637
1771
|
`$updatedCallback` is **binding-driven**: it reports the paths whose live DOM bindings were actually applied in that update, so a value you never render is invisible to it. **`$watch`** is the headless counterpart — it fires on state changes whether or not anything on the page is bound to the path. (One exception, spelled out below: a *wildcard* row path needs `$listKeys` to work headlessly.)
|
|
@@ -1683,6 +1817,8 @@ Firing order is defined in three layers, and only the middle one is yours to ste
|
|
|
1683
1817
|
| Between handlers | declaration order in `$watch` | **reorder the declarations** |
|
|
1684
1818
|
| Between rows of one path | ascending `indexes` | fixed |
|
|
1685
1819
|
|
|
1820
|
+
**The one thing that moves the mechanism layer** is a `<wcs-view-transition>` that accepts the `state` participant. Binding application — and with it `$updatedCallback` — then lands on a frame, while `$watch` and the `$streams` restart stay on the microtask the drain was queued on, because they consume state addresses and not the DOM. For as long as the tag is present the order is `$watch` → `$streams` restart → `$updatedCallback`. Nothing else on the page reorders this layer; see [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
1821
|
+
|
|
1686
1822
|
Key rules:
|
|
1687
1823
|
|
|
1688
1824
|
- **Only its own state** — a path may not carry `@stateName`; watching another state element is rejected at declaration time.
|
|
@@ -1944,6 +2080,101 @@ All hooks except `$disconnectedCallback` support `async` — you can use `async/
|
|
|
1944
2080
|
- `$updatedCallback(paths, indexesListByPath)` receives the paths whose live bindings were applied in that drain. Unbound state writes do not invoke it or appear in `paths`. For wildcard updates, `indexesListByPath` contains the updated index sets. Can be `async`, but the return value is not awaited
|
|
1945
2081
|
- In Web Components, define `async $stateReadyCallback(stateProp)` to receive a hook when the bound state becomes available via `bind-component`
|
|
1946
2082
|
|
|
2083
|
+
## Transition animations
|
|
2084
|
+
|
|
2085
|
+
Enter animations need nothing from this package — a new `for` row and a mounting `if` branch are newly inserted elements, so plain CSS covers them:
|
|
2086
|
+
|
|
2087
|
+
```css
|
|
2088
|
+
li {
|
|
2089
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
2090
|
+
@starting-style { opacity: 0; transform: translateY(-4px); }
|
|
2091
|
+
}
|
|
2092
|
+
```
|
|
2093
|
+
|
|
2094
|
+
**Leaving** and **moving** cannot be reached that way: removed rows are detached synchronously, and a reorder has no intermediate state. Adding [`@wcstack/view-transition`](https://github.com/wcstack/wcstack/tree/main/packages/view-transition) makes the drain apply its DOM changes inside a View Transition, where the browser snapshots the old state for you:
|
|
2095
|
+
|
|
2096
|
+
```html
|
|
2097
|
+
<script type="module" src="https://esm.run/@wcstack/view-transition/auto"></script>
|
|
2098
|
+
<wcs-view-transition naming="auto"></wcs-view-transition>
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
Two consequences to know while that tag accepts the `state` participant:
|
|
2102
|
+
|
|
2103
|
+
- The drain lands on a frame instead of a microtask, so code that writes state and then reads the DOM after `await Promise.resolve()` must wait for the transition. `$updatedCallback` still fires immediately after the bindings are applied — its *position* is unchanged, but it moves a frame later along with them.
|
|
2104
|
+
- Because `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
|
|
2105
|
+
|
|
2106
|
+
Only a batch that actually has bindings to apply is handed to the tag, so a write to a headless path never starts a transition. Without the tag the drain is exactly what it was. See [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
2107
|
+
|
|
2108
|
+
## Diagnostics and failure handling
|
|
2109
|
+
|
|
2110
|
+
### Wiring to a path that does not exist is reported
|
|
2111
|
+
|
|
2112
|
+
When a wired path provably does not resolve against the state, you get one warning at binding time (at declaration time for `$watch`). The diagnostic codes are shared by the console, `@wcstack/lint`, and the VS Code extension:
|
|
2113
|
+
|
|
2114
|
+
```
|
|
2115
|
+
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on state "default":
|
|
2116
|
+
"nmae" is not declared. Did you mean "name"? Updates to this path will be silently
|
|
2117
|
+
dropped. Validate statically: npx @wcstack/lint <file>.
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
| Situation | Behavior |
|
|
2121
|
+
|---|---|
|
|
2122
|
+
| Typo in a nested path (`user.nmae`) | `console.warn` (`wcs/binding-path-missing`). Updates still never arrive — you fix it |
|
|
2123
|
+
| Typo in a top-level path (`cout`) | Throws on read, with the same wording and did-you-mean |
|
|
2124
|
+
| Typo in a `$watch` key | `console.warn` (`wcs/watch-path-missing`), reported even for a single segment |
|
|
2125
|
+
|
|
2126
|
+
The check **under-approximates**: it stays silent for anything it cannot decide statically, because a false alarm costs more than a missed one. None of these warn:
|
|
2127
|
+
|
|
2128
|
+
- A `null` / `undefined` parent (the "seed as `null`, assign later" shape)
|
|
2129
|
+
- Row fields of a list that starts empty (the row shape is unknown)
|
|
2130
|
+
- Sub-properties of an intermediate getter's return value
|
|
2131
|
+
- Mapped `bind-component` child scopes (the parent owns the path)
|
|
2132
|
+
- Reserved `$` namespaces (`$command.*` and friends)
|
|
2133
|
+
|
|
2134
|
+
So **no warning is not a proof of correctness.** For exhaustive checking, run `npx @wcstack/lint <file>`.
|
|
2135
|
+
|
|
2136
|
+
### Index arity, wildcard rank, and getter cycles are checked too
|
|
2137
|
+
|
|
2138
|
+
Anything that follows mechanically from the path string is reported at runtime and by the linter under the same diagnostic code.
|
|
2139
|
+
|
|
2140
|
+
| Diagnostic | What it checks | Fix |
|
|
2141
|
+
|---|---|---|
|
|
2142
|
+
| `wcs/index-arity` | `$resolve(path, indexes)` must match the `*` count **exactly**; `$getAll(path, indexes)` / `$setAll(path, indexes, …)` have it as an **upper bound** (fewer is a legitimate prefix meaning "expand the rest") | Match the count |
|
|
2143
|
+
| `wcs/wildcard-rank` | The path's `*` count (and the N in `$N`) must not exceed the enclosing `for` nesting | Add a `for`, or name the row with `$resolve(path, indexes)` |
|
|
2144
|
+
| `wcs/getter-cycle` | Path getters must not form a dependency cycle | Break the cycle |
|
|
2145
|
+
|
|
2146
|
+
Previously **extra indexes were silently discarded** by both APIs, so a mixed-up call returned a plausible-looking wrong value. Both now throw:
|
|
2147
|
+
|
|
2148
|
+
```javascript
|
|
2149
|
+
// ❌ Only one "*" in the path, two indexes given — used to return items[0]'s value
|
|
2150
|
+
this.$resolve("items.*.price", [row, col]);
|
|
2151
|
+
|
|
2152
|
+
// ✅ Two levels, two indexes
|
|
2153
|
+
this.$resolve("matrix.*.*", [row, col]);
|
|
2154
|
+
// ✅ Fewer is fine for $getAll — it means "expand the remaining levels"
|
|
2155
|
+
this.$getAll("matrix.*.*", [row]);
|
|
2156
|
+
```
|
|
2157
|
+
|
|
2158
|
+
### One failing binding is confined to that binding
|
|
2159
|
+
|
|
2160
|
+
If applying a binding throws, the rest of that batch, `$updatedCallback`, `$watch`, and `$streams` restarts all still run. The failure is not swallowed — it goes to `console.error` and to DevTools (`state:binding-apply-error`):
|
|
2161
|
+
|
|
2162
|
+
```
|
|
2163
|
+
[@wcstack/state] binding "text: items.*.label" failed to apply; the rest of this batch continues.
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2166
|
+
Without that confinement, a single throw left "new values, half-updated DOM" behind, and silently dropped every `$watch` handler and stream restart for the batch — quietly breaking the firing-order contract documented above.
|
|
2167
|
+
|
|
2168
|
+
### Values and the DOM are never rolled back
|
|
2169
|
+
|
|
2170
|
+
Every failure mode reports and continues; nothing already applied is reverted:
|
|
2171
|
+
|
|
2172
|
+
| Mechanism | Limit | On exceeding |
|
|
2173
|
+
|---|---|---|
|
|
2174
|
+
| Propagation hops | 32 | Quarantine the transaction's remaining records |
|
|
2175
|
+
| `$watch` write chain | 32 | Skip watch firing for that batch |
|
|
2176
|
+
| Binding apply failure | — | Skip that one binding |
|
|
2177
|
+
|
|
1947
2178
|
## Configuration
|
|
1948
2179
|
|
|
1949
2180
|
Pass a partial configuration object to `bootstrapState()`:
|
|
@@ -1965,13 +2196,43 @@ All options with defaults:
|
|
|
1965
2196
|
|---|---|---|
|
|
1966
2197
|
| `bindAttributeName` | `'data-wcs'` | Binding attribute name |
|
|
1967
2198
|
| `tagNames.state` | `'wcs-state'` | State element tag name |
|
|
1968
|
-
| `locale` | `'en'` |
|
|
2199
|
+
| `locale` | `<html lang>`, else `'en'` | Locale for the locale-dependent filters (`locale` / `date` / `time` / `datetime`) — see [Locale](#locale) |
|
|
1969
2200
|
| `debug` | `false` | Debug mode |
|
|
1970
2201
|
| `enableMustache` | `true` | Enable `{{ }}` syntax |
|
|
1971
2202
|
| `enableDirectionalInitialSync` | `true` | Direction-aware binding authority (`#init=` / `#sync=` binding modifiers) — see [Binding Authority](#binding-authority-init--sync). Default on; set `false` to opt out |
|
|
1972
2203
|
| `enablePropagationContext` | `true` | Causal propagation tracking across bindings (echo/diamond loop prevention). Default on; set `false` to opt out |
|
|
1973
2204
|
| `enableContractAnalyzer` | `false` | Opt-in dev-time contract analyzer (exposes `analyzeContract`) |
|
|
1974
2205
|
|
|
2206
|
+
### Locale
|
|
2207
|
+
|
|
2208
|
+
Four filters format by locale — `locale`, `date`, `time`, `datetime`. They read
|
|
2209
|
+
`config.locale`, which **defaults to `<html lang>`**:
|
|
2210
|
+
|
|
2211
|
+
```html
|
|
2212
|
+
<html lang="ja-JP">
|
|
2213
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
2214
|
+
```
|
|
2215
|
+
|
|
2216
|
+
Nothing else is needed; `<html lang>` is the standard place to record a page's
|
|
2217
|
+
language, and making it the default keeps one source of truth. It also means the
|
|
2218
|
+
CDN one-liner can set the locale at all — `auto` calls `bootstrapState()` with no
|
|
2219
|
+
arguments, so before this there was no way in. An explicit
|
|
2220
|
+
`bootstrapState({ locale })` still wins, and an invalid BCP-47 tag is reported
|
|
2221
|
+
and ignored rather than left to throw inside `Intl`.
|
|
2222
|
+
|
|
2223
|
+
**Changing `config.locale` later does not re-render anything.** It is a global
|
|
2224
|
+
setting, not state, so it is not part of the dependency graph. The filters do
|
|
2225
|
+
read it on every application rather than capturing it when the binding is built,
|
|
2226
|
+
which means a binding that re-renders for its own reasons will pick up the new
|
|
2227
|
+
value — enough to recover from a mis-ordered startup, not enough to switch a
|
|
2228
|
+
page's language. Set the language before the page renders: writing `<html lang>`
|
|
2229
|
+
in the markup, or from a synchronous `<head>` script, does that structurally.
|
|
2230
|
+
|
|
2231
|
+
Per-call overrides stay available and are fixed at bind time, since they are part
|
|
2232
|
+
of the binding expression: `price|locale(fr-FR)`. For a page that switches
|
|
2233
|
+
language without reloading, see [docs/i18n-design.md](../../docs/i18n-design.md) —
|
|
2234
|
+
the short answer is that translations belong on a path, not in a filter.
|
|
2235
|
+
|
|
1975
2236
|
> These three are **architecture-hardening** features; their normative reference is
|
|
1976
2237
|
> `docs/architecture-hardening/`. `enablePropagationContext` defaults **on** — its
|
|
1977
2238
|
> write-path cost is near-zero for one-way bindings (only echo-capable two-way
|