@wcstack/state 1.8.5 → 1.9.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 +38 -8
- package/README.md +144 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +255 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
# @wcstack/state
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**これは便利な既存FWの別実装ではありません。フロントエンド開発の前提を組み替える、別系譜の試みです。**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
多くのライブラリは、UI・状態・コンポーネントの結合点を JavaScript の中に置きます。`@wcstack/state` はそこを選びません。仮想DOMも、コンパイルも、hook も、selector も前提にせず、HTML とパス文字列だけを契約として UI と状態を結びつけます。
|
|
6
6
|
|
|
7
|
-
それが `<wcs-state>` と `data-wcs`
|
|
7
|
+
それが `<wcs-state>` と `data-wcs` のアプローチです。CDNからの読み込みだけで動作し、依存パッケージはゼロ、構文はHTMLそのままです。CDNのスクリプトはカスタム要素の定義を登録するだけで、ロード時にはそれ以外の処理は走りません。`<wcs-state>` 要素がDOMに接続されたときにはじめて、状態ソースを読み取り、同一ルートノード(`document` または `ShadowRoot`)内の `data-wcs` バインディングを走査してリアクティビティを構築します。初期化プロセスはすべて要素のライフサイクルによって駆動されるため、独自の初期化コードを書く必要はありません。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## ここには存在しないもの
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
以下は未実装ではありません。**設計上、存在しません。**
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- 変数を取り出す API
|
|
14
|
+
- 要素ごとに状態を束縛するオブジェクト
|
|
15
|
+
- hook
|
|
16
|
+
- selector
|
|
17
|
+
- reactive primitive をコンポーネントへ引き込むための glue code
|
|
18
|
+
|
|
19
|
+
None of these exist by design.
|
|
20
|
+
|
|
21
|
+
なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
|
|
22
|
+
|
|
23
|
+
## 既存FWとは比較しません
|
|
24
|
+
|
|
25
|
+
これは React / Vue / Solid と同じ問題を別の方法で解いているのではありません。**前提自体が違います。**
|
|
26
|
+
|
|
27
|
+
| 一般的なFWが前提にするもの | `@wcstack/state` が前提にするもの |
|
|
28
|
+
|---|---|
|
|
29
|
+
| コンポーネントが UI と状態の結合点 | パス文字列が UI と状態の結合点 |
|
|
30
|
+
| JavaScript が描画の中心 | HTML と DOM が中心 |
|
|
31
|
+
| state を取り出して component へ流し込む | path を宣言して DOM を状態へ接続する |
|
|
32
|
+
| hook / selector / signal で購読する | 属性とパスで束縛する |
|
|
33
|
+
| フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に薄い reactive layer を足す |
|
|
34
|
+
|
|
35
|
+
比較表を作るより先に、この前提差を理解してください。同じ棚に置いても、解いている問題の切り取り方が違います。
|
|
36
|
+
|
|
37
|
+
## 第一原理: パスが唯一の契約
|
|
14
38
|
|
|
15
39
|
既存の多くのフレームワークでは、**コンポーネント**がUIと状態の結合点になっています。状態ストアを外部に切り出しても、コンポーネント内にフックやセレクタ、リアクティブプリミティブといった**状態を引き込むためのコード**が必ず必要になります。つまり、UIと状態は常にJavaScriptの中で結びついているのです。
|
|
16
40
|
|
|
@@ -40,6 +64,8 @@ CDNのスクリプトはカスタム要素の定義を登録するだけで、
|
|
|
40
64
|
|
|
41
65
|
このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
|
|
42
66
|
|
|
67
|
+
以下の機能はすべて、この原理の帰結です。機能が先にあり、その説明として哲学を後付けしているのではありません。
|
|
68
|
+
|
|
43
69
|
## わずか4ステップで動作
|
|
44
70
|
|
|
45
71
|
```html
|
|
@@ -62,7 +88,7 @@ CDNのスクリプトはカスタム要素の定義を登録するだけで、
|
|
|
62
88
|
|
|
63
89
|
これだけです。ビルドツールも、初期化コードも、重いフレームワークも必要ありません。
|
|
64
90
|
|
|
65
|
-
##
|
|
91
|
+
## この原理から導かれる機能
|
|
66
92
|
|
|
67
93
|
- **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
|
|
68
94
|
- **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
|
|
@@ -216,7 +242,9 @@ this["user.name"] = "Bob"; // パス "user.name"
|
|
|
216
242
|
|
|
217
243
|
### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
|
|
218
244
|
|
|
219
|
-
|
|
245
|
+
これは単なる制約ではなく、**契約境界が見えている箇所**です。
|
|
246
|
+
|
|
247
|
+
通常のプロパティアクセスの書き方だと、まず `this.user` でプレーンな `user` オブジェクトを読み取り(パスの読み取り)、取得したオブジェクトの `.name` を直接書き換える挙動になります。これは「パスに対するプロパティ代入」という契約を通っていません。そのため、システム側は変更を検知しません:
|
|
220
248
|
|
|
221
249
|
```javascript
|
|
222
250
|
// ✅ パスへの代入 — 変更が検知される
|
|
@@ -226,6 +254,8 @@ this["user.name"] = "Bob";
|
|
|
226
254
|
this.user.name = "Bob";
|
|
227
255
|
```
|
|
228
256
|
|
|
257
|
+
`this.user.name = "Bob"` も動くようにすると、一見便利にはなります。しかしその瞬間に「UI と状態はパスだけで結ばれる」という原理が崩れます。どこで依存を追跡し、どこで更新を確定するかが曖昧になり、契約境界が失われます。
|
|
258
|
+
|
|
229
259
|
### 配列
|
|
230
260
|
|
|
231
261
|
配列についても全く同じルールが適用されます。常に**パスに対して新しい配列を代入**してください。`push` や `splice`、`sort` などの破壊的な配列メソッドは、パスへの代入を介さずに状態をその場で(in-placeに)書き換えてしまうため、変更が検知されません。代わりに、新しい配列を返す非破壊的なメソッドを使用します:
|
package/README.md
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
# @wcstack/state
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**This is not another convenient frontend framework. It is a different lineage that rearranges the premises of frontend development.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Most libraries place the coupling point between UI, state, and components inside JavaScript. `@wcstack/state` does not. It assumes no virtual DOM, no compilation step, no hooks, no selectors. UI and state are connected by HTML and path strings alone.
|
|
6
6
|
|
|
7
|
-
That
|
|
7
|
+
That is what `<wcs-state>` and `data-wcs` explore. One CDN import, zero dependencies, pure HTML syntax. The CDN script only registers the custom element definition — nothing else happens at load time. When a `<wcs-state>` element connects to the DOM, it reads its state source, scans all `data-wcs` bindings within the same root node (`document` or `ShadowRoot`), and wires up reactivity. All initialization is driven by the element's lifecycle, not by your code.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## What Does Not Exist Here
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The following are not missing features. **They do not exist by design.**
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- APIs for pulling variables out of state into components
|
|
14
|
+
- Per-element binding objects that mediate state access
|
|
15
|
+
- hooks
|
|
16
|
+
- selectors
|
|
17
|
+
- glue code that imports reactive primitives into component code
|
|
18
|
+
|
|
19
|
+
None of these exist by design.
|
|
20
|
+
|
|
21
|
+
Why: this library does not put the UI-state coupling point inside JavaScript. State is not pulled into components. HTML refers to state through path strings. Elements do not own state, and state does not know elements. The only shared contract is the path.
|
|
22
|
+
|
|
23
|
+
## Do Not Compare This to Existing Frameworks
|
|
24
|
+
|
|
25
|
+
This is not solving the same problem as React / Vue / Solid with a different syntax. **The premises are different.**
|
|
26
|
+
|
|
27
|
+
| What mainstream frameworks assume | What `@wcstack/state` assumes |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Components are the coupling point between UI and state | Path strings are the coupling point between UI and state |
|
|
30
|
+
| JavaScript is the center of rendering | HTML and the DOM are the center |
|
|
31
|
+
| State is pulled into components | Paths are declared and the DOM connects to state |
|
|
32
|
+
| hooks / selectors / signals express subscriptions | Attributes and paths express bindings |
|
|
33
|
+
| The whole app runs inside a framework execution model | A thin reactive layer is added on top of web standards |
|
|
34
|
+
|
|
35
|
+
Before making a comparison chart, understand this difference in premises. These tools may live in the same ecosystem, but they cut the problem space very differently.
|
|
36
|
+
|
|
37
|
+
## First Principle: Path as the Universal Contract
|
|
14
38
|
|
|
15
39
|
In every existing framework, the **component** is the coupling point between UI and state. Components import state hooks, selectors, or reactive primitives, and the binding happens inside JavaScript. No matter how cleanly you separate your state store, there is always glue code in the component that pulls state in.
|
|
16
40
|
|
|
@@ -40,6 +64,8 @@ This is complete separation of UI and state with **no JavaScript intermediary**.
|
|
|
40
64
|
|
|
41
65
|
The path contract works like a URL in a REST API — a simple string that both sides agree on, with no shared code between them. It's the natural result of building on HTML's declarative nature rather than inventing a template language on top of JavaScript.
|
|
42
66
|
|
|
67
|
+
Every feature below is a consequence of this principle. The principle comes first; the features follow from it.
|
|
68
|
+
|
|
43
69
|
## 4 Steps to Reactive HTML
|
|
44
70
|
|
|
45
71
|
```html
|
|
@@ -62,7 +88,7 @@ The path contract works like a URL in a REST API — a simple string that both s
|
|
|
62
88
|
|
|
63
89
|
That's it. No build, no bootstrap code, no framework.
|
|
64
90
|
|
|
65
|
-
## Features
|
|
91
|
+
## Features Derived from This Principle
|
|
66
92
|
|
|
67
93
|
- **Declarative data binding** — `data-wcs` attribute for property / text / event / structural binding
|
|
68
94
|
- **Reactive Proxy** — ES Proxy-based automatic DOM updates with dependency tracking
|
|
@@ -70,6 +96,7 @@ That's it. No build, no bootstrap code, no framework.
|
|
|
70
96
|
- **Built-in filters** — 40 filters for formatting, comparison, arithmetic, date, and more
|
|
71
97
|
- **Two-way binding** — automatic for `<input>`, `<select>`, `<textarea>`
|
|
72
98
|
- **Web Component binding** — bidirectional state binding with Shadow DOM components
|
|
99
|
+
- **Command tokens** — invoke methods on wc-bindable custom elements from state via a pub/sub channel (`command.<method>: tokenName`)
|
|
73
100
|
- **Path getters** — dot-path key getters (`get "users.*.fullName"()`) for virtual properties at any depth in a data tree, all defined flat in one place with automatic dependency tracking and caching
|
|
74
101
|
- **Mustache syntax** — `{{ path|filter }}` in text nodes
|
|
75
102
|
- **Multiple state sources** — JSON, JS module, inline script, API, attribute
|
|
@@ -216,7 +243,9 @@ That's the one rule: **assign to the path, and the DOM updates automatically.**
|
|
|
216
243
|
|
|
217
244
|
### Why `this.user.name = "Bob"` Doesn't Work
|
|
218
245
|
|
|
219
|
-
|
|
246
|
+
This is not just a limitation. It is where the contract boundary becomes visible.
|
|
247
|
+
|
|
248
|
+
`this.user.name` first reads the `user` object via `this.user` (a path read), then sets `.name` on that plain object — this does not go through the contract of path assignment, so the change is not detected:
|
|
220
249
|
|
|
221
250
|
```javascript
|
|
222
251
|
// ✅ Path assignment — change detected
|
|
@@ -226,6 +255,8 @@ this["user.name"] = "Bob";
|
|
|
226
255
|
this.user.name = "Bob";
|
|
227
256
|
```
|
|
228
257
|
|
|
258
|
+
It may seem more convenient to make `this.user.name = "Bob"` reactive too. But doing that would break the principle that UI and state are connected only through paths. Dependency tracking and update boundaries would become implicit and ambiguous. The visible contract boundary is the point.
|
|
259
|
+
|
|
229
260
|
### Arrays
|
|
230
261
|
|
|
231
262
|
The same rule applies: assign a new array to the path. Mutating methods (`push`, `splice`, `sort`, ...) modify the array in place without path assignment, so use non-destructive alternatives:
|
|
@@ -739,6 +770,7 @@ Inside state objects (getters / methods), the following APIs are available via `
|
|
|
739
770
|
| `this.$resolve(path, indexes, value?)` | Resolve a wildcard path with specific indexes |
|
|
740
771
|
| `this.$postUpdate(path)` | Manually trigger update notification for a path |
|
|
741
772
|
| `this.$trackDependency(path)` | Manually register a dependency for cache invalidation |
|
|
773
|
+
| `this.$command.<name>` | Access a `CommandToken` declared in `$commandTokens` (see [Command Token](#command-token-method-binding)) |
|
|
742
774
|
| `this.$stateElement` | Access to the `IStateElement` instance |
|
|
743
775
|
| `this.$1`, `this.$2`, ... | Current loop index (1-based naming, 0-based value) |
|
|
744
776
|
|
|
@@ -1015,6 +1047,110 @@ customElements.define("my-component", MyComponent);
|
|
|
1015
1047
|
</template>
|
|
1016
1048
|
```
|
|
1017
1049
|
|
|
1050
|
+
## Command Token (Method Binding)
|
|
1051
|
+
|
|
1052
|
+
Property binding (`state.message: user.name`) covers data flowing into a component, but it does not cover **invoking a method on a component from state** — `<wcs-fetch>.fetch()`, `<wcs-dialog>.open()`, and so on. **Command tokens** fill that gap with a typed pub/sub channel:
|
|
1053
|
+
|
|
1054
|
+
- The element subscribes via `command.<methodName>: <tokenPath>`
|
|
1055
|
+
- State emits via `this.$command.<tokenName>.emit(...args)`
|
|
1056
|
+
- Arguments passed to `emit` are forwarded to the element's method
|
|
1057
|
+
- One token can fan out to multiple elements; the subscriber order is preserved
|
|
1058
|
+
|
|
1059
|
+
This keeps the path contract intact: state never holds a reference to the element, and the element never imports anything from state. The token is the only shared object.
|
|
1060
|
+
|
|
1061
|
+
### Basic Usage
|
|
1062
|
+
|
|
1063
|
+
```html
|
|
1064
|
+
<wcs-state>
|
|
1065
|
+
<script type="module">
|
|
1066
|
+
export default {
|
|
1067
|
+
$commandTokens: ["fetchUsers", "refreshOrders"],
|
|
1068
|
+
|
|
1069
|
+
onClickFetch() {
|
|
1070
|
+
this.$command.fetchUsers.emit("/api/users", { method: "GET" });
|
|
1071
|
+
},
|
|
1072
|
+
onClickRefresh() {
|
|
1073
|
+
this.$command.refreshOrders.emit();
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
</script>
|
|
1077
|
+
</wcs-state>
|
|
1078
|
+
|
|
1079
|
+
<!-- Subscribers — must be wc-bindable custom elements -->
|
|
1080
|
+
<wcs-fetch data-wcs="command.fetch: fetchUsers"></wcs-fetch>
|
|
1081
|
+
<wcs-fetch data-wcs="command.fetch: refreshOrders"></wcs-fetch>
|
|
1082
|
+
|
|
1083
|
+
<button data-wcs="onclick: onClickFetch">Fetch users</button>
|
|
1084
|
+
<button data-wcs="onclick: onClickRefresh">Refresh orders</button>
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
When `onClickFetch` runs, every element subscribed to the `fetchUsers` token has its `fetch(...)` method called with the forwarded arguments.
|
|
1088
|
+
|
|
1089
|
+
### `$commandTokens` Declaration
|
|
1090
|
+
|
|
1091
|
+
The `$commandTokens` array declares the channels exposed under the `$command` namespace on state. Tokens are accessed as `this.$command.<name>` and are memoized — the same name always returns the same token instance.
|
|
1092
|
+
|
|
1093
|
+
```javascript
|
|
1094
|
+
export default {
|
|
1095
|
+
$commandTokens: ["fetchUsers", "refreshOrders"],
|
|
1096
|
+
|
|
1097
|
+
click() {
|
|
1098
|
+
this.$command.fetchUsers.emit("/api/users");
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
- Entries must be non-empty strings
|
|
1104
|
+
- Duplicate entries throw an error at initialization
|
|
1105
|
+
- The reserved name `$command` itself cannot appear in the array
|
|
1106
|
+
- Tokens are gathered under `$command` so they do not pollute the top-level state namespace; reactive properties with the same name as a token can coexist
|
|
1107
|
+
- Accessing an undeclared name on `$command` (e.g. `this.$command.typo`) throws — typos are caught at access time
|
|
1108
|
+
|
|
1109
|
+
### `command.<methodName>:` Binding
|
|
1110
|
+
|
|
1111
|
+
```html
|
|
1112
|
+
<wcs-fetch data-wcs="command.fetch: fetchUsers"></wcs-fetch>
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
| Part | Description |
|
|
1116
|
+
|---|---|
|
|
1117
|
+
| `command.` | Fixed prefix |
|
|
1118
|
+
| `<methodName>` | The element's method to invoke. Must be listed in `static wcBindable.commands` |
|
|
1119
|
+
| `<tokenPath>` | A path that resolves to a `CommandToken` (typically a name from `$commandTokens`) |
|
|
1120
|
+
|
|
1121
|
+
Validation rules (enforced at binding time):
|
|
1122
|
+
|
|
1123
|
+
- The element must be a custom element exposing `static wcBindable` with `protocol: "wc-bindable"` and `version: 1`
|
|
1124
|
+
- `methodName` must be present in `wcBindable.commands`
|
|
1125
|
+
- The bound value must be a `CommandToken` (assigning a non-token value throws)
|
|
1126
|
+
|
|
1127
|
+
### Token API
|
|
1128
|
+
|
|
1129
|
+
```typescript
|
|
1130
|
+
interface CommandToken {
|
|
1131
|
+
readonly name: string;
|
|
1132
|
+
readonly size: number; // current subscriber count
|
|
1133
|
+
subscribe(fn: (...args) => unknown): () => void; // returns unsubscribe
|
|
1134
|
+
unsubscribe(fn: (...args) => unknown): boolean;
|
|
1135
|
+
emit(...args: unknown[]): unknown[]; // returns subscriber results in subscribe order
|
|
1136
|
+
}
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
`emit` returns an array of return values from each subscriber (in subscribe order). For `Promise`-returning methods, wrap with `Promise.all(token.emit(...))` to await all of them.
|
|
1140
|
+
|
|
1141
|
+
### Subscription Lifecycle
|
|
1142
|
+
|
|
1143
|
+
- The subscriber holds the element via `WeakRef`, so a removed element can still be garbage collected even while it remains in the token's subscriber set
|
|
1144
|
+
- On `emit`, if the WeakRef has been collected or the element is no longer connected (`isConnected === false`), the subscription is purged automatically (lazy purge)
|
|
1145
|
+
- When the owning `<wcs-state>` is disconnected, the entire token registry is cleared
|
|
1146
|
+
|
|
1147
|
+
The element's method is invoked with the arguments from `emit`:
|
|
1148
|
+
|
|
1149
|
+
```javascript
|
|
1150
|
+
this.$command.fetchUsers.emit(url, options);
|
|
1151
|
+
// → element.fetch(url, options) on every subscriber
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1018
1154
|
## Declarative Custom Components (DCC)
|
|
1019
1155
|
|
|
1020
1156
|
Define custom elements **entirely in HTML** — no JavaScript class definition needed. Using `data-wc-definition` and Declarative Shadow DOM (`<template shadowrootmode>`), you can declare reusable components with reactive state inline.
|
package/dist/index.d.ts
CHANGED
|
@@ -133,14 +133,14 @@ type IsAny<T> = 0 extends (1 & T) ? true : false;
|
|
|
133
133
|
* T がドットパス再帰の対象となる「プレーンなデータオブジェクト」かどうかを判定する。
|
|
134
134
|
* プリミティブ、組み込みオブジェクト (Date, Map 等)、関数、配列、any は除外。
|
|
135
135
|
*/
|
|
136
|
-
type IsPlainObject<T> = IsAny<T> extends true ? false : T extends string | number | boolean | null | undefined | symbol | bigint |
|
|
136
|
+
type IsPlainObject<T> = IsAny<T> extends true ? false : T extends string | number | boolean | null | undefined | symbol | bigint | ((...args: any[]) => any) | Date | RegExp | Error | Map<any, any> | Set<any> | WeakMap<any, any> | WeakSet<any> | Promise<any> | readonly any[] ? false : T extends Record<string, any> ? true : false;
|
|
137
137
|
/**
|
|
138
138
|
* T のキーのうち、関数でないもの(データプロパティ・computed getter)を抽出する。
|
|
139
139
|
* メソッド(イベントハンドラ等)はドットパスの対象外。
|
|
140
140
|
* any 型のプロパティは除外せず保持する。
|
|
141
141
|
*/
|
|
142
142
|
type DataKeys<T> = {
|
|
143
|
-
[K in keyof T & string]: IsAny<T[K]> extends true ? K : T[K] extends
|
|
143
|
+
[K in keyof T & string]: IsAny<T[K]> extends true ? K : T[K] extends (...args: any[]) => any ? never : K;
|
|
144
144
|
}[keyof T & string];
|
|
145
145
|
/**
|
|
146
146
|
* 型 T から生成される全てのドットパスの union。
|