🙌

Structive サンプル解説:構造パスと Getter で実現する地域別州データテーブル

に公開

はじめに

本記事では、フロントエンドフレームワーク Structive を使って構築されたサンプルコードを解説します。このサンプルを通して、Structive の核となるコンセプトである「構造パス」や、強力な機能である「Getter」、「$getAll」メソッドなどがどのように活用されているかを見ていきましょう。

今回解説するサンプルコード(states-population)は、米国の州データを地域ごとにグループ化し、各地域の州リスト、地域ごとのサマリー、そして全体のサマリーを整形されたテーブル形式で表示するものです。

※下記リポジトリをクローンしてを簡単にサンプルコードを試すことができます。
https://github.com/mogera551/Structive-Example

サンプルコード全体

<template>
  <div class="container">
    <div>
      <table class="table table-striped">
        <colgroup>
          <col class="col-md-3">
          <col class="col-md-3">
          <col class="col-md-2">
          <col class="col-md-2">
          <col class="col-md-2">
        </colgroup>
        <thead>
          <tr>
            <th class="text-center">State</th>
            <th class="text-center">Capital City</th>
            <th class="text-center">Population</th>
            <th class="text-center">Percent of Region's Population</th>
            <th class="text-center">Percent of Total Population</th>
          </tr>
        </thead>
        <tbody>
          {{ for:regions }}
            {{ for:regions.*.states }}
              <tr>
                <td class="text-center">{{ regions.*.states.*.name }}</td>
                <td class="text-center">{{ regions.*.states.*.capital }}</td>
                <td class="text-right" data-bind="
                  class.over : regions.*.states.*.population|ge,5000000;
                  class.under: regions.*.states.*.population|lt,1000000;
                ">{{ regions.*.states.*.population|locale }}</td>
                <td class="text-right">{{ regions.*.states.*.shareOfRegionPopulation|percent,2 }}</td>
                <td class="text-right">{{ regions.*.states.*.shareOfPopulation|percent,2 }}</td>
              </tr>
            {{ endfor: }}
            <tr class="summary">
              <td class="text-center" colspan="2">{{ regions.* }}</td>
              <td class="text-right">{{ regions.*.population|locale }}</td>
              <td></td>
              <td class="text-right">{{ regions.*.shareOfPopulation|percent,2 }}</td>
            </tr>
          {{ endfor: }}
        </tbody>
        <tfoot>
          <tr class="summary">
            <td class="text-center" colspan="2">Total</td>
            <td class="text-right">{{ population|locale }}</td>
            <td></td>
            <td></td>
          </tr>
        </tfoot>
      </table>
    </div>
  </div>
  
</template>  

<style>
  body {
    margin-left: 10px;
  }
  
  .over {
    color: red;
  }
  
  .under {
    color: blue;
  }
  
  tr.summary td {
    background-color: white;
    font-weight: bold;
  }
</style>
  
<script type="module">
import { allStates } from "states";

const summaryPopulation = (sum, population) => sum + population;

export default class {
  stateByRegion = Map.groupBy(allStates, state => state.region);
  regions = Array.from(new Set(allStates.map(state => state.region))).toSorted();

  get "regions.*.states"() {
    return this.stateByRegion.get(this["regions.*"]);
  }

  get "regions.*.states.*.shareOfRegionPopulation"() {
    return this["regions.*.states.*.population"] / this["regions.*.population"] * 100;
  }

  get "regions.*.states.*.shareOfPopulation"() {
    return this["regions.*.states.*.population"] / this.population * 100;
  }

  get "regions.*.population"() {
    return this.$getAll("regions.*.states.*.population", [ this.$1 ]).reduce(summaryPopulation, 0);
  }

  get "regions.*.shareOfPopulation"() {
    return this["regions.*.population"] / this.population * 100;
  }

  get population() {
    return this.$getAll("regions.*.population", []).reduce(summaryPopulation, 0);
  }

}
</script>

州のデータは以下のように定義されています。

allStates.js
/**
 * @typedef {Object} StateInfo
 * @property {string} name
 * @property {string} capital
 * @property {number} population
 * @property {string} region
 */

/** @type {StateInfo[]} */
export const allStates = [
  {"name": "Alabama", "capital": "Montgomery", "region": "South", "population": 5108468},
:

コード解説

このコードは、Structive のシングルファイルコンポーネント (SFC) 形式で書かれており、テンプレート (<template>)、スタイル (<style>)、そしてロジック (<script>) が一つのファイルにまとめられています。

<script> の解説

<script> ブロックでは、コンポーネントの状態(データ)とロジックを定義します。

  • データの準備:

    • import { allStates } from "states";:外部モジュールから全ての州データ (allStates) をインポートしています。
    • stateByRegion = Map.groupBy(allStates, state => state.region);:新しい JavaScript の Map.groupBy メソッドを使って、allStates を地域ごとに Map にグループ化しています。
    • regions = Array.from(new Set(allStates.map(state => state.region))).toSorted();:全ての州から地域名のユニークなリストを作成し、ソートしています。この regions 配列が、テンプレートでの外側のループ処理の対象となります。
    • summaryPopulation 関数:人口の合計を計算するためのシンプルなヘルパー関数です。
  • 強力な Getter の活用:
    Structive の <script> ブロックの最大の特徴の一つである Getter が多用されています。Structive では、Getter の名前として構造パスを指定することで、その構造パスに対応するデータを計算し、リアクティブな仮想プロパティとして扱うことができます。

    • get "regions.*.states"():この Getter は、外側のループの現在の地域 (regions.*) に対応する州のリストを返します。stateByRegion.get(this["regions.*"]) で、あらかじめグループ化しておいた Map から該当地域の州リストを取得しています。テンプレートの {{ for:regions.*.states }} で、この Getter が返す値が内側のループの対象となります。
    • get "regions.*.states.*.shareOfRegionPopulation"():各州 (regions.*.states.*) の「地域内人口比率」を計算する Getter です。this["regions.*.states.*.population"] で現在の州の人口を取得し、this["regions.*.population"] で現在の地域の合計人口を取得して計算しています。
    • get "regions.*.states.*.shareOfPopulation"():各州の「総人口比率」を計算する Getter です。現在の州の人口を this.population (全体の合計人口を計算する Getter) で割って計算しています。
    • get "regions.*.population"():各地域 (regions.*) の「合計人口」を計算する Getter です。ここで this.$getAll("regions.*.states.*.population", [ this.$1 ]) という Structive 独自の API が使われています。この $getAll は、指定した構造パスに一致する複数の値を取得するための強力なメソッドです。ここでは、「現在の地域の全ての州 (states.*) の人口 (population)」という構造パスを指定し、取得した人口リストを reduce で合計しています。[ this.$1 ] は、暗黙のインデックスで、現在の外側ループのインデックスを渡すことで、対象を現在の地域に絞り込んでいます。
    • get "regions.*.shareOfPopulation"():各地域の「総人口比率」を計算する Getter です。地域の合計人口を this.population で割って計算しています。
    • get population():全体の「合計人口」を計算する Getter です。ここでも this.$getAll("regions.*.population", []) が使われています。「全ての地域 (regions.*) の合計人口 (population - 前述の地域の合計人口 Getter) 」という構造パスを指定し、取得した地域の合計人口リストを reduce で合計しています。[] は、対象を絞り込まず全ての要素を取得することを示唆しています。

このように、Structive では構造パスを使った Getter と $getAll を組み合わせることで、元のデータ構造から様々なレベルの集計値や比率といった複雑な派生データ(仮想プロパティ)を、非常に効率的かつ宣言的に定義し、それらが依存するデータ(元の人口データや他の Getter)が変更された際に自動的に再計算されるリアクティブな仕組みを構築しています。
Getterの説明を見ると複雑そうですが、実際のGetter内のコードを見れば、構造パスの自己記述性も相まって値の取得方法が宣言的に簡潔に書かれていることが見て取れると思います。また、Getterを 階層構造を持つデータの仮想的なプロパティ として扱っているところは他のフレームワークにはない非常にユニークな特徴です。

<template> の解説

テンプレートブロックでは、UI の構造とデータをどのように表示するかを定義します。Structive 独自の構文と、HTML 要素が組み合わされています。

  • テーブル構造: 標準的な HTML の <table>, <thead>, <tbody>, <tfoot> 構造でテーブルを定義しています。Bootstrap の CSS クラスが適用されており、見た目を整えています。

  • ループ構文 ({{ for: }}):

    • {{ for:regions }}:まず、最上位の regions 配列(地域名のリスト)をループしています。外側のループです。
    • {{ for:regions.*.states }}:外側のループ内で、regions.*.states という構造パスを指定して、内側のループを行っています。これは、外側のループの現在の地域 (regions.*) に対応する州のリスト(<script> の Getter get "regions.*.states" が返す値)をループ処理しています。
  • 構造パスによるデータ表示 ({{ }}):

    • {{ regions.*.states.*.name }} のように、ネストされた {{ for }} ループの中では、ネストされた構造パスを使って現在の要素(この場合は州)のプロパティを参照します。regions.* は外側のループの現在の要素(地域)、states.* は内側のループの現在の要素(州)を指しており、.name で州の名前のプロパティにアクセスしています。この構造パスが、データ構造とテンプレートの表示をリンクする Structive の核です。
    • 地域集計行 (<tr class="summary">) では、{{ regions.* }} で外側のループの現在の地域名を表示しています。
    • フッター行 (<tfoot>) や地域集計行では、 <script> で定義した Getter が計算した全体の合計人口 ({{ population }}) や地域の合計人口 ({{ regions.*.population }}) などを表示しています。
  • data-bind 属性:

    • data-bind="class.over : regions.*.states.*.population|ge,5000000; class.under: regions.*.states.*.population|lt,1000000;":人口を表示する <td> 要素に適用されています。これは Structive の条件付き属性バインディングの例です。人口 (regions.*.states.*.population) の値がフィルター ge (greater than or equal to) と lt (less than) によって評価され、その結果に応じて class.over (人口500万人以上) または class.under (人口100万人未満) という CSS クラスが動的に要素に適用されます。複数のバインディングをセミコロン ; で区切って指定できることが分かります。
  • フィルター (|):

    • {{ regions.*.states.*.population|locale }} のように、マスタッシュ構文の中でパイプ | を使ってフィルターを適用しています。locale フィルターは数値を地域の慣習に合わせた書式(例: 桁区切り)に変換すると推測できます。
    • {{ regions.*.states.*.shareOfRegionPopulation|percent,2 }} のように、percent フィルターは数値をパーセント形式に変換し、パラメータ ,2 で小数点以下2桁まで表示することを指定していると考えられます。フィルターは複数のパラメータを受け取れることが分かります。

<style> の解説

<style> ブロックには、テーブル要素や前述の over, under, summary といったクラスに対する基本的な CSS 定義が含まれています。これにより、人口による文字色の変化や、集計行のスタイルが適用されます。

このサンプルからわかる Structive の強み

このサンプルコードは、Structive の以下のような強みを非常によく示しています。

  • 構造パスによる強力なデータ・UI連携: ネストされたループや、Getter、バインディング、データ表示など、 Structive のあらゆる部分で構造パスが共通言語として使われており、データ構造と UI 構造が密接にリンクされていることが分かります。
  • 構造パスを使った Getter と $getAll による高度な派生データ計算: リストの各要素やグループ全体、全体の合計といった様々なレベルの集計値や比率を、構造パスを使った Getter と $getAll という Structive 独自の強力な API を組み合わせて、効率的かつリアクティブに計算できる表現力の高さが示されています。
  • 柔軟なデータバインディングとフィルター: HTML 要素やの属性に対するバインディング、条件付きクラスバインディング、フィルターを使った表示整形など、データに基づいた動的な UI 表現が豊富に行えることが分かります。
  • 階層データの効率的な表示: ネストされた {{ for }} ループとネストされた構造パスを組み合わせることで、階層的なデータ(地域 > 州)をテンプレートで直感的に表現し、表示できます。
  • SFC による開発のまとまり: 関連するテンプレート、スタイル、ロジックがまとめて管理されており、コンポーネントの見通しが良いです。
  • 認知負荷の低減: UI、状態クラスで構造パスを共通言語としているため、そのUI構造、データ構造理解に頭を切り替える必要が少なく、開発者の認知負荷を下げ、開発体験の向上を期待できます。またレビューやコードリーディングでUIと状態クラスで構造パスが同じものを指すため、開発チーム全体での認知負荷を下げる効果も考えられます。

まとめ

この州データのテーブル表示サンプルは、Structive が単なる基本的なデータ表示フレームワークではなく、「UI構造とデータ構造を一致させ、構造パスでリンクさせる」という核となるコンセプトに基づき、階層的なデータ構造を扱い、構造パスを使った Getter と $getAll で高度な派生データを計算し、それをデータバインディングとフィルターで柔軟に UI に表示するといった、より複雑で動的な Web アプリケーションの構築も、 Structive らしいシンプルさと効率性で実現できることを示しています。


https://github.com/mogera551/Structive

https://zenn.dev/mogera/articles/4e617973a6a65b

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

Discussion