@wcstack/state 1.6.4 → 1.7.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 CHANGED
@@ -1015,6 +1015,106 @@ customElements.define("my-component", MyComponent);
1015
1015
  </template>
1016
1016
  ```
1017
1017
 
1018
+ ## 宣言的カスタムコンポーネント (DCC)
1019
+
1020
+ JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を定義できます。`data-wc-definition` と Declarative Shadow DOM (`<template shadowrootmode>`) を使い、リアクティブな状態を持つ再利用可能なコンポーネントをインラインで宣言します。
1021
+
1022
+ ### 基本的な定義
1023
+
1024
+ ```html
1025
+ <!-- 1. コンポーネントを定義(CSSで非表示) -->
1026
+ <my-counter data-wc-definition>
1027
+ <template shadowrootmode="open">
1028
+ <p>{{ count }}</p>
1029
+ <button data-wcs="onclick: increment">+1</button>
1030
+ <wcs-state>
1031
+ <script type="module">
1032
+ export default {
1033
+ count: 0,
1034
+ increment() { this.count++; },
1035
+ $bindables: ["count"]
1036
+ };
1037
+ </script>
1038
+ </wcs-state>
1039
+ </template>
1040
+ </my-counter>
1041
+
1042
+ <!-- 2. 使う — 各インスタンスが独自の状態を持つ -->
1043
+ <my-counter></my-counter>
1044
+ <my-counter></my-counter>
1045
+ ```
1046
+
1047
+ `<wcs-state>` が `data-wc-definition` 付きのホスト内にあることを検出すると:
1048
+
1049
+ 1. 状態オブジェクトをロード(`<script type="module">` または `src="*.js"`)
1050
+ 2. getter/setter/メソッドをプロトタイプに定義したカスタム要素クラスを生成
1051
+ 3. `customElements.define()` で登録
1052
+
1053
+ 定義要素は非表示になり、各インスタンスはテンプレートを自身の Shadow DOM にクローンして、独自の `<wcs-state>` を初期化します。
1054
+
1055
+ ### 推奨 CSS
1056
+
1057
+ ```css
1058
+ :not(:defined) { display: none; }
1059
+ [data-wc-definition] { display: none; }
1060
+ ```
1061
+
1062
+ ### `$bindables` と wc-bindable プロトコル
1063
+
1064
+ `$bindables` 配列は、変更イベント付きのコンポーネントプロパティとして公開する状態プロパティを宣言します。[wc-bindable プロトコル](https://github.com/nicenemo/nicenemo/blob/main/docs/wc-bindable-protocol.md)に準拠しています:
1065
+
1066
+ ```javascript
1067
+ export default {
1068
+ count: 0,
1069
+ increment() { this.count++; },
1070
+ $bindables: ["count"]
1071
+ };
1072
+ ```
1073
+
1074
+ これにより以下が生成されます:
1075
+
1076
+ - クラスの `static wcBindable` — フレームワークアダプタ用のプロトコルメタデータ
1077
+ - プロトタイプの getter/setter — リアクティブプロキシ経由で読み書き
1078
+ - `CustomEvent` のディスパッチ — 値が変更されるたびに `my-counter:count-changed` が発火
1079
+
1080
+ ### DCC プロパティへのバインディング
1081
+
1082
+ 他の `<wcs-state>` インスタンスから、通常の Web Component と同じように DCC プロパティにバインドできます:
1083
+
1084
+ ```html
1085
+ <my-counter data-wcs="count: parentCount"></my-counter>
1086
+
1087
+ <wcs-state>
1088
+ <script type="module">
1089
+ export default { parentCount: 0 };
1090
+ </script>
1091
+ </wcs-state>
1092
+ <div data-wcs="textContent: parentCount"></div>
1093
+ ```
1094
+
1095
+ ### Shadow Root モード
1096
+
1097
+ `open` と `closed` の両モードに対応しています:
1098
+
1099
+ ```html
1100
+ <my-component data-wc-definition>
1101
+ <template shadowrootmode="closed">
1102
+ <!-- closed shadow DOM -->
1103
+ </template>
1104
+ </my-component>
1105
+ ```
1106
+
1107
+ ### 内部プロパティ
1108
+
1109
+ `$` プレフィックス付きのプロパティは内部用で、コンポーネントのプロトタイプには公開されません:
1110
+
1111
+ | プロパティ | 用途 |
1112
+ |----------|---------|
1113
+ | `$bindables` | 観測可能プロパティの宣言 |
1114
+ | `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
1115
+ | `$disconnectedCallback` | クリーンアップフック |
1116
+ | `$updatedCallback` | 状態変更後に呼ばれる |
1117
+
1018
1118
  ## SVG サポート
1019
1119
 
1020
1120
  全てのバインディングが `<svg>` 要素内で動作します。SVG 属性には `attr.*` を使用します:
package/README.md CHANGED
@@ -1015,6 +1015,106 @@ customElements.define("my-component", MyComponent);
1015
1015
  </template>
1016
1016
  ```
1017
1017
 
1018
+ ## Declarative Custom Components (DCC)
1019
+
1020
+ 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.
1021
+
1022
+ ### Basic Definition
1023
+
1024
+ ```html
1025
+ <!-- 1. Define the component (hidden by CSS) -->
1026
+ <my-counter data-wc-definition>
1027
+ <template shadowrootmode="open">
1028
+ <p>{{ count }}</p>
1029
+ <button data-wcs="onclick: increment">+1</button>
1030
+ <wcs-state>
1031
+ <script type="module">
1032
+ export default {
1033
+ count: 0,
1034
+ increment() { this.count++; },
1035
+ $bindables: ["count"]
1036
+ };
1037
+ </script>
1038
+ </wcs-state>
1039
+ </template>
1040
+ </my-counter>
1041
+
1042
+ <!-- 2. Use it — each instance gets its own state -->
1043
+ <my-counter></my-counter>
1044
+ <my-counter></my-counter>
1045
+ ```
1046
+
1047
+ When `<wcs-state>` detects it is inside a `data-wc-definition` host, it:
1048
+
1049
+ 1. Loads the state object (from `<script type="module">` or `src="*.js"`)
1050
+ 2. Generates a custom element class with getter/setter/method properties on the prototype
1051
+ 3. Registers it via `customElements.define()`
1052
+
1053
+ The definition element is hidden; each instance clones the template into its own Shadow DOM and initializes its own `<wcs-state>`.
1054
+
1055
+ ### Recommended CSS
1056
+
1057
+ ```css
1058
+ :not(:defined) { display: none; }
1059
+ [data-wc-definition] { display: none; }
1060
+ ```
1061
+
1062
+ ### `$bindables` and wc-bindable Protocol
1063
+
1064
+ The `$bindables` array declares which state properties are exposed as component properties with change events, following the [wc-bindable protocol](https://github.com/nicenemo/nicenemo/blob/main/docs/wc-bindable-protocol.md):
1065
+
1066
+ ```javascript
1067
+ export default {
1068
+ count: 0,
1069
+ increment() { this.count++; },
1070
+ $bindables: ["count"]
1071
+ };
1072
+ ```
1073
+
1074
+ This generates:
1075
+
1076
+ - `static wcBindable` on the class — protocol metadata for framework adapters
1077
+ - Getter/setter on the prototype — reads/writes go through the reactive proxy
1078
+ - `CustomEvent` dispatch — `my-counter:count-changed` fires on every mutation
1079
+
1080
+ ### Binding to DCC Properties
1081
+
1082
+ Other `<wcs-state>` instances can bind to DCC properties just like any Web Component:
1083
+
1084
+ ```html
1085
+ <my-counter data-wcs="count: parentCount"></my-counter>
1086
+
1087
+ <wcs-state>
1088
+ <script type="module">
1089
+ export default { parentCount: 0 };
1090
+ </script>
1091
+ </wcs-state>
1092
+ <div data-wcs="textContent: parentCount"></div>
1093
+ ```
1094
+
1095
+ ### Shadow Root Mode
1096
+
1097
+ Both `open` and `closed` modes are supported:
1098
+
1099
+ ```html
1100
+ <my-component data-wc-definition>
1101
+ <template shadowrootmode="closed">
1102
+ <!-- closed shadow DOM -->
1103
+ </template>
1104
+ </my-component>
1105
+ ```
1106
+
1107
+ ### Internal Properties
1108
+
1109
+ Properties prefixed with `$` are internal and not exposed on the component prototype:
1110
+
1111
+ | Property | Purpose |
1112
+ |----------|---------|
1113
+ | `$bindables` | Declares observable properties |
1114
+ | `$connectedCallback` | Lifecycle hook (runs on each instance) |
1115
+ | `$disconnectedCallback` | Cleanup hook |
1116
+ | `$updatedCallback` | Called after state mutations |
1117
+
1018
1118
  ## SVG Support
1019
1119
 
1020
1120
  All bindings work inside `<svg>` elements. Use `attr.*` for SVG attributes:
package/dist/index.esm.js CHANGED
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
59
59
  }
60
60
  }
61
61
 
62
- var version$1 = "1.6.4";
62
+ var version$1 = "1.7.0";
63
63
  var pkg = {
64
64
  version: version$1};
65
65
 
@@ -186,6 +186,8 @@ const STATE_CONNECTED_CALLBACK_NAME = "$connectedCallback";
186
186
  const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
187
187
  const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
188
188
  const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
189
+ const STATE_BINDABLES_NAME = "$bindables";
190
+ const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
189
191
 
190
192
  const _cache$4 = new Map();
191
193
  let id = 0;
@@ -4819,6 +4821,153 @@ function createLoopContextStack() {
4819
4821
  return new LoopContextStack();
4820
4822
  }
4821
4823
 
4824
+ function getterFn(name) {
4825
+ return function () {
4826
+ const stateEl = this.stateElement;
4827
+ if (!stateEl)
4828
+ return undefined;
4829
+ let value;
4830
+ try {
4831
+ stateEl.createState("readonly", (state) => {
4832
+ value = state[name];
4833
+ });
4834
+ }
4835
+ catch {
4836
+ return undefined;
4837
+ }
4838
+ return value;
4839
+ };
4840
+ }
4841
+ function setterFn(name) {
4842
+ return function (value) {
4843
+ const stateEl = this.stateElement;
4844
+ if (!stateEl)
4845
+ return;
4846
+ stateEl.initializePromise.then(() => {
4847
+ stateEl.createState("writable", (state) => {
4848
+ state[name] = value;
4849
+ });
4850
+ });
4851
+ };
4852
+ }
4853
+ function callFn(name, isAsync) {
4854
+ if (isAsync) {
4855
+ return function (...args) {
4856
+ const stateEl = this.stateElement;
4857
+ if (!stateEl)
4858
+ return;
4859
+ return stateEl.initializePromise.then(() => {
4860
+ return stateEl.createStateAsync("writable", async (state) => {
4861
+ await state[name](...args);
4862
+ });
4863
+ });
4864
+ };
4865
+ }
4866
+ return function (...args) {
4867
+ const stateEl = this.stateElement;
4868
+ if (!stateEl)
4869
+ return;
4870
+ stateEl.initializePromise.then(() => {
4871
+ stateEl.createState("writable", (state) => {
4872
+ state[name](...args);
4873
+ });
4874
+ });
4875
+ };
4876
+ }
4877
+ function isInternalProperty(name) {
4878
+ return name.startsWith("$");
4879
+ }
4880
+
4881
+ function createWcBindable(tagName, bindables) {
4882
+ const properties = bindables.map((propName) => ({
4883
+ name: propName,
4884
+ event: `${tagName}:${propName}-changed`,
4885
+ }));
4886
+ return {
4887
+ protocol: "wc-bindable",
4888
+ version: 1,
4889
+ properties,
4890
+ };
4891
+ }
4892
+ function createBindableEventMap(tagName, bindables) {
4893
+ const map = {};
4894
+ for (const propName of bindables) {
4895
+ map[propName] = `${tagName}:${propName}-changed`;
4896
+ }
4897
+ return map;
4898
+ }
4899
+
4900
+ function defineDCC(hostElement, shadowRoot, state) {
4901
+ const tagName = hostElement.tagName.toLowerCase();
4902
+ // バリデーション
4903
+ if (!tagName.includes("-")) {
4904
+ raiseError(`DCC: "${tagName}" is not a valid custom element name (must contain a hyphen).`);
4905
+ }
4906
+ if (customElements.get(tagName)) {
4907
+ // 既に登録済みならスキップ
4908
+ return;
4909
+ }
4910
+ // ShadowRoot は cloneNode 不可のため、template 経由で内容をクローン
4911
+ const template = document.createElement("template");
4912
+ template.innerHTML = shadowRoot.innerHTML;
4913
+ const shadowRootMode = shadowRoot.mode;
4914
+ // $bindables から wcBindable + bindableEventMap を生成
4915
+ const bindables = Array.isArray(state[STATE_BINDABLES_NAME])
4916
+ ? state[STATE_BINDABLES_NAME]
4917
+ : [];
4918
+ const wcBindable = bindables.length > 0
4919
+ ? createWcBindable(tagName, bindables)
4920
+ : null;
4921
+ const bindableEventMap = bindables.length > 0
4922
+ ? createBindableEventMap(tagName, bindables)
4923
+ : {};
4924
+ // DCC クラス生成
4925
+ const stateTagSelector = `${config.tagNames.state}:not([name])`;
4926
+ const DCCElement = class extends HTMLElement {
4927
+ static template = template;
4928
+ static shadowRootMode = shadowRootMode;
4929
+ static wcBindable = wcBindable;
4930
+ static bindableEventMap = bindableEventMap;
4931
+ _shadow = null;
4932
+ connectedCallback() {
4933
+ if (this.hasAttribute(DCC_DEFINITION_ATTRIBUTE))
4934
+ return;
4935
+ this._shadow = this.attachShadow({ mode: DCCElement.shadowRootMode });
4936
+ this._shadow.appendChild(DCCElement.template.content.cloneNode(true));
4937
+ // bindableEventMap の設定
4938
+ if (Object.keys(DCCElement.bindableEventMap).length > 0) {
4939
+ const stateEl = this._shadow.querySelector(stateTagSelector);
4940
+ if (stateEl) {
4941
+ stateEl.initializePromise.then(() => {
4942
+ stateEl.setBindableEventMap(DCCElement.bindableEventMap);
4943
+ });
4944
+ }
4945
+ }
4946
+ }
4947
+ get stateElement() {
4948
+ return this._shadow?.querySelector(stateTagSelector);
4949
+ }
4950
+ };
4951
+ // state プロパティを走査して DCC クラスのプロトタイプにgetter/setter/methodを定義
4952
+ const descriptors = Object.getOwnPropertyDescriptors(state);
4953
+ for (const [name, desc] of Object.entries(descriptors)) {
4954
+ if (isInternalProperty(name))
4955
+ continue;
4956
+ const newDesc = { configurable: true, enumerable: true };
4957
+ if (typeof desc.value === "function") {
4958
+ const isAsync = desc.value.constructor?.name === "AsyncFunction";
4959
+ newDesc.value = callFn(name, isAsync);
4960
+ }
4961
+ else {
4962
+ newDesc.get = getterFn(name);
4963
+ newDesc.set = setterFn(name);
4964
+ }
4965
+ Object.defineProperty(DCCElement.prototype, name, newDesc);
4966
+ }
4967
+ // カスタム要素登録
4968
+ customElements.define(tagName, DCCElement);
4969
+ }
4970
+
4822
4971
  /**
4823
4972
  * Cache for resolved path information.
4824
4973
  * Uses Map to safely handle property names including reserved words like "constructor" and "toString".
@@ -5470,6 +5619,17 @@ function setByAddress(target, address, value, receiver, handler) {
5470
5619
  dirty: false
5471
5620
  });
5472
5621
  }
5622
+ // DCC bindable イベントディスパッチ
5623
+ const eventName = stateElement.bindableEventMap[address.pathInfo.path];
5624
+ if (eventName) {
5625
+ const rootNode = stateElement.rootNode;
5626
+ if (rootNode instanceof ShadowRoot) {
5627
+ rootNode.host.dispatchEvent(new CustomEvent(eventName, {
5628
+ detail: value,
5629
+ bubbles: true,
5630
+ }));
5631
+ }
5632
+ }
5473
5633
  }
5474
5634
  }
5475
5635
 
@@ -6469,6 +6629,7 @@ class State extends HTMLElement {
6469
6629
  _rootNode = null;
6470
6630
  _boundComponent = null;
6471
6631
  _boundComponentStateProp = null;
6632
+ _bindableEventMap = {};
6472
6633
  constructor() {
6473
6634
  super();
6474
6635
  this._initializePromise = new Promise((resolve) => {
@@ -6627,6 +6788,37 @@ class State extends HTMLElement {
6627
6788
  }
6628
6789
  });
6629
6790
  }
6791
+ async _initializeDCC(hostElement, shadowRoot) {
6792
+ let state;
6793
+ try {
6794
+ if (this.hasAttribute('src')) {
6795
+ const src = this.getAttribute('src');
6796
+ if (src.endsWith('.js')) {
6797
+ state = await loadFromScriptFile(src);
6798
+ }
6799
+ else {
6800
+ raiseError(`DCC: Unsupported src type: ${src}`);
6801
+ }
6802
+ }
6803
+ else {
6804
+ const script = this.querySelector('script[type="module"]');
6805
+ if (script) {
6806
+ state = await loadFromInnerScript(script, hostElement.tagName.toLowerCase());
6807
+ }
6808
+ else {
6809
+ raiseError(`DCC: No state source found for "${hostElement.tagName.toLowerCase()}".`);
6810
+ }
6811
+ }
6812
+ }
6813
+ catch (e) {
6814
+ raiseError(`DCC: Failed to load state: ${e}`);
6815
+ }
6816
+ defineDCC(hostElement, shadowRoot, state);
6817
+ this._initialized = true;
6818
+ this._rootNode = null; // disconnectedCallbackでのstate参照を防止
6819
+ this._resolveInitialize?.();
6820
+ this._resolveConnectedCallback?.();
6821
+ }
6630
6822
  _callStateDisconnectedCallback() {
6631
6823
  this.createState("writable", (state) => {
6632
6824
  // stateに"$disconnectedCallback"があるか確認し、disconnectedCallbackAPIを呼び出す
@@ -6638,6 +6830,13 @@ class State extends HTMLElement {
6638
6830
  async connectedCallback() {
6639
6831
  this._rootNode = this.getRootNode();
6640
6832
  if (!this._initialized) {
6833
+ // DCC 検出: ShadowRoot 内かつホストに data-wc-definition がある場合
6834
+ const parentNode = this.parentNode;
6835
+ if (parentNode instanceof ShadowRoot &&
6836
+ parentNode.host.hasAttribute(DCC_DEFINITION_ATTRIBUTE)) {
6837
+ await this._initializeDCC(parentNode.host, parentNode);
6838
+ return;
6839
+ }
6641
6840
  await this._initializeBindWebComponent();
6642
6841
  await this._initialize();
6643
6842
  this._initialized = true;
@@ -6707,6 +6906,12 @@ class State extends HTMLElement {
6707
6906
  get boundComponentStateProp() {
6708
6907
  return this._boundComponentStateProp;
6709
6908
  }
6909
+ get bindableEventMap() {
6910
+ return this._bindableEventMap;
6911
+ }
6912
+ setBindableEventMap(map) {
6913
+ this._bindableEventMap = map;
6914
+ }
6710
6915
  _addDependency(map, sourcePath, targetPath) {
6711
6916
  const deps = map.get(sourcePath);
6712
6917
  if (deps === undefined) {