🤖

Structiveの驚異的なシンプルさを体感してみる

に公開

はじめに

現代のフロントエンドフレームワークは多機能化が進み、それに伴って学習コストやコードの記述量も増える傾向にあります。そんな中、Structiveというユニークなフレームワークは、「驚異的なシンプルさ」でモダンなUI開発のコア機能を実現できる可能性を秘めています。

この記事では、Structiveがどのようにしてそのシンプルさを実現しているのか、具体的なコード例を通して体感し、その魅力に迫ります。

Structiveの基本構造

Structiveは、UIの構造とロジックを明確に分離する、コンポーネントベースのフレームワークです。

  • UI構造: 標準的なHTML <template> タグ内に記述します。
  • ロジック(状態と振る舞い): 標準的なJavaScriptクラスを <script type="module"> タグ内に記述します。
<template>
  <h1>{{ pageTitle }}</h1>
</template>

<script type="module">
  export default class {
    // ここにコンポーネントの状態をプロパティとして定義
    pageTitle = "こんにちは、Structive!";

    // ここにコンポーネントの振る舞いをメソッドとして定義
    // ...
  }
</script>

Structiveで定義されたコンポーネントは、内部的にはWeb Components標準に準拠し、customElements.defineを使ってカスタム要素として登録されます。これにより、他のWeb技術との高い相互運用性を持つことができます。

驚異的なシンプルさの秘密:コードで体感する

Structiveのシンプルさは、その独自のデータバインディングと状態管理のアプローチにあります。いくつかの機能を見ていきましょう。

1. ステートと表示

コンポーネントの状態(ステート)は、クラスのプロパティとして定義します。テンプレートからその値を表示するには、二重波括弧 {{ }} でプロパティ名を囲むだけです。

<template>
  <p>メッセージ: {{ message }}</p>
</template>

<script type="module">
  export default class {
    message = "これはシンプルなステートです。";
  }
</script>

クラスのmessageプロパティの値が変わると、テンプレートの表示も自動的に更新されます。

2. リスト表示と「構造パス」

Structiveの最もユニークな点の一つが「構造パス」とリスト表示の連携です。配列データを表示するには{{ for }}ブロックを使います。配列内の各要素のプロパティにアクセスするには、コレクション.*.プロパティ という形式の「構造パス」を使用します。

<template>
  <h2>ユーザーリスト</h2>
  <ul>
    {{ for:users }}
      <li>{{ users.*.name }}</li>
    {{ endfor: }}
  </ul>
</template>

<script type="module">
  export default class {
    users = [
      { name: "Alice" },
      { name: "Bob" },
      { name: "Charlie" }
    ];
  }
</script>

users.*.name という「構造パス」は、「users配列のそれぞれの要素(*)の name プロパティ」を指します。*がループ内の現在の要素を表す、Structive独自の簡潔な記法です。

3. イベント処理とステート更新

UI要素のイベント(クリックなど)をクラスメソッドに紐づけ、ステートを更新することでリアクティブなインタラクションを実現します。data-bind属性を使用します。

<template>
  <div>
    <p>カウンター: {{ count }}</p>
    <button data-bind="onclick:increment">増やす</button>
  </div>
</template>

<script type="module">
  export default class {
    count = 0;

    // increment メソッド:count を増やし、ステートを更新
    increment() {
      this.count = this.count + 1; // プロパティへの直接代入でOK
    }
  }
</script>

data-bind="onclick:increment" がボタンのクリックと increment メソッドを結びつけます。注目すべきは、ステートの更新が this.count = this.count + 1; というプロパティへの直接代入で行える点です。他の多くのフレームワークで必要となる専用のセッター関数などは不要です。これがリアクティビティをトリガーします。

4. データバインディングの多様性(値とクラス)

data-bind属性は、イベントだけでなく、フォーム要素の値やクラス属性の制御にも使用できます。

<template>
  <div>
    <input type="text" data-bind="value:userName" placeholder="名前を入力">
    <p>入力された名前: {{ userName }}</p>

    <hr>

    <h2>ユーザーリスト(選択機能付き)</h2>
    <ul>
      {{ for:users }}
        <li data-bind="
          class.selected:users.*.selected;
          onclick:selectUser;
        ">
          {{ users.*.name }}
          </li>
      {{ endfor: }}
    </ul>
  </div>
</template>

<script type="module">
  export default class {
    userName = ""; // inputと双方向バインディング

    users = [
      { name: "Alice", selected: false }, // 各要素に状態を持つ
      { name: "Bob", selected: false }
    ];

    // ユーザー選択/非選択をトグル
    selectUser(e, $1) {
      this[`users.${$1}.selected`] = !this[`users.${$1}.selected`];
    }
  }
</script>

<style>
.selected {
  background-color: yellow;
}
</style>

data-bind="value:userName" は、input の入力値と userName プロパティを双方向で結びつけます。data-bind="class.selected:users.*.selected" は、users 配列の各要素の selected プロパティ(真偽値)に応じて、<li> 要素に selected クラスを追加/削除します。

ここで $1 という変数も登場しました。これは、{{ for }} ループの中でイベントハンドラなどが呼ばれた際に、Structiveが自動的に渡してくれる「現在のループのインデックス」です。このように、Structiveは独自の記法で、ループのコンテキスト情報に簡潔にアクセスできます。

5. 算出プロパティとしての「構造パス」ゲッター

Structiveのシンプルさとパワーを象徴するのが「構造パス」ゲッターです。これは、データ構造に直接プロパティを持たなくても、UIが必要とする算出値や派生値を、あたかもそこにプロパティがあるかのように定義できる仕組みです。下記の例では、ユーザーリストの各要素にucNameというプロパティが追加されたかのように振舞います。UI側では、users.*.nameと同じように(プロパティかゲッターかを意識することなく)users.*.ucNameでアクセスすることができます。

<template>
  <h2>ユーザーリスト(大文字名表示)</h2>
  <ul>
    {{ for:users }}
      <li>{{ users.*.ucName }}</li>
    {{ endfor: }}
  </ul>
</template>

<script type="module">
  export default class {
    users = [
      { name: "Alice" },
      { name: "Bob" }
    ];

    // 「構造パス」を名とするゲッター
    // users.*.name が変更されると自動的に再評価される
    get "users.*.ucName"() {
      // this["構造パス"] で現在のアイテムのデータにアクセス
      return this["users.*.name"].toUpperCase();
    }
  }
</script>

get "users.*.ucName"() は、ゲッターの名前自体が「構造パス」になっています。このゲッターは、テンプレートで {{ users.*.ucName }} と参照された際に、users 配列の各要素に対して評価されます。ゲッター内部では、this["users.*.name"] という記法で、評価対象となっている「現在のアイテムの name プロパティ」にアクセスできます。

そして、Structiveはゲッター内で参照した「構造パス」(この場合は users.*.name)を自動的にトラッキングし、その参照元のデータに変更があった場合にのみ、該当ゲッターを再評価してUIを更新します。

データに ucName プロパティを直接持たずとも、UIが必要とする値をゲッターとして定義し、それを通常の「構造パス」と同じように参照できる。この仕組みが、データ構造をシンプルに保ちつつ、UIが必要とする多様な派生データを効率的に扱うことを可能にしています。フィルターよりも複雑な算出や、他の状態(selectedIndexなど)を参照する算出も可能です。

6. コンポーネント間の連携

Structiveでは、親子コンポーネント間の連携も data-bind を応用してシンプルに行えます。親から子の状態プロパティへ、構造パスで直接バインディングが可能です。

<template>
  <h2>親コンポーネント</h2>
  <ul>
    {{ for:users }}
      <child-comp data-bind="state.childName:users.*.name"></child-comp>
    {{ endfor: }}
  </ul>
</template>

<script type="module">
  // child-comp のクラス定義が別途必要
  // import ChildComp from './child-comp.js';
  // customElements.define('child-comp', ChildComp); // または Structive 内部で登録

  export default class {
    users = [
      { name: "Alice" },
      { name: "Bob" }
    ];
  }
</script>

<template>
  <li>
    子: {{ childName }}
    <input type="text" data-bind="value:childName">
  </li>
</template>

<script type="module">
  export default class {
    // 親からバインドされるステートプロパティ
    childName = "";
    // ... 他のステートやメソッド
  }
</script>

親のテンプレートで <child-comp data-bind="state.childName:users.*.name"></child-comp> と記述することで、親の users.*.name(ループコンテキストに従う)と、子コンポーネントのクラスの childName プロパティが双方向でバインドされます。他のフレームワークでPropsを明示的に渡したり、イベントを使って親の状態を更新したりするのとは異なる、Structiveらしい直接的な状態連携のアプローチです。

なぜ「驚異的」なシンプルさなのか

Structiveがこれほどシンプルに感じられるのは、その設計思想に基づいています。

  • UI構造とデータ構造の一致: このシンプルさの源泉はUI構造とデータ構造を一致させることによって、UIでもデータでも「構造パス」により同じものを指せるようになることにあります。これによりUIとデータ間の翻訳・変換が必要なくなり大幅にボイラープレートが省けるようになります。
  • 「構造パス」が共通言語: UIのテンプレートも、JavaScriptのスクリプト(状態、ゲッター、アクセス)も、すべて「構造パス」という共通の概念を使ってデータに言及します。これにより、UIとデータ構造の文脈間の意識の切り替えが減り、コードの意図が掴みやすくなります。
  • ボイラープレートの極小化: ステート定義、更新、メソッド定義などが非常に直接的で、他のフレームワークにある多くの定型的な記述や専用API呼び出しが不要になっています。おおよそJavaScriptのクラス構文を理解できれば状態クラスを設計できる作りになっています。
  • ルールの焦点: ルールがないわけではありませんが、その独自のルール(構造パス、$1、data-bind、ゲッター構文)が少なく、かつデータバインディングと算出というコア機能に集約されているため、一度覚えれば広く応用が効きます。
  • 機能とシンプルさの両立: 単に機能を削ぎ落としただけでなく、構造パスゲッターや$1のような独自の強力な機構を導入することで、モダンなUIに必要な機能(宣言的UI、リアクティブ性、複雑な算出、コンテキストアクセスなど)を、シンプルさを保ったまま実現しています。

Structiveのシンプルさは、単にコードが短いというだけでなく、開発者がUIとデータを考える際の認知負荷を大幅に軽減し、本質的なロジックに集中できるよう設計されている点にあります。特定の領域、特にCRUD操作やデータ表示が中心となる管理画面のようなアプリケーションでは、このシンプルさが「爆発的な生産性」に繋がるポテンシャルを秘めていると言えます。

まとめ

Structiveは、Web Components標準を基盤としつつ、「構造パス」という独自の強力な概念を核に、モダンなUI開発のコア機能を驚くほどシンプルに、そしてエレガントに表現できるユニークなフレームワークです。

  • 標準JSクラスでの状態管理とメソッド定義。
  • <template><script>の明確な分離。
  • {{ }}data-bind による直感的で強力なデータバインディング。
  • {{ for }}構造パス、$1によるリスト表示とコンテキストアクセス。
  • get "構造パス"() ゲッターによる、データに持たない派生データのシンプルな算出と自動更新。
  • data-bind="state.子prop:親パス" による直接的なコンポーネント間状態連携。

これらの特徴が組み合わさることで、Structiveはボイラープレートを最小限に抑え、開発者がアプリケーションの本質に集中できるようなシンプルで効率的な開発体験を提供します。特に、データ構造とUI構造の整合性が取りやすいCRUD中心のアプリケーションにおいては、そのシンプルさとパワーが真価を発揮するでしょう。

「構造パス」という一つの概念を深く追求し、それをフレームワーク全体に貫いた結果、Structiveは「驚異的」と呼ぶにふさわしいシンプルさと、モダンなUI開発に必要なコア機能を両立した、非常に興味深いフレームワークになっています。

もしあなたが、既存フレームワークの複雑さに疲れていたり、シンプルで効率的なデータ駆動UI開発のアプローチを探しているなら、ぜひ一度Structiveを体感してみてはいかがでしょうか。


https://github.com/mogera551/Structive

https://zenn.dev/mogera/articles/ad6df27b51ae51

Discussion