😸

宣言的なJSフレームワーク 「配列の宣言的な記述」の仕組み

2022/05/11に公開約6,800字

概要

このフレームワークの目玉機能!?、「配列の宣言的な記述」の仕組みを解説

フレームワーク

https://github.com/mogera551/data-x.js/

解説

配列表示のサンプルコード

繰り返し要素をバインドする場合、アスタリスクを含むドット記法で指定する。
ここでは、"fruits.*"で、ViewModelのリストfruitsの要素とバインドすることを示している。

index-spa/html/main.html
<template data-x:loop="fruits">
  <div data-x:bind="fruits.*"></div>
</template>

バインドする要素をすべて、ViewModelのクラスのプロパティに宣言する。
ここでは、リスト"fruits"とリストの要素を示す"fruits.*"。

index-spa/module/main.js
class AppViewModel {
  "@fruits" = [ "バナナ", "りんご", "オレンジ" ];
  "@fruits.*";
}
実行結果
<template data-x:loop="fruits">
  <div data-x:bind="fruits.*"></div>
</template>
  <div data-x:bind="fruits.0">バナナ</div>
  <div data-x:bind="fruits.1">りんご</div>
  <div data-x:bind="fruits.2">オレンジ</div>

HTMLの展開

フレームワークは実行時、下記のようにhtmlを展開する。
templateタグのdata-x:loopの値("fruits")より、

  • ViewModelのfruitsプロパティを取得し、
  • Object.keys(viewModel.fruits)でループし、
  • "fruits.0"、"fruits.1"、"fruits.2"のdiv要素を作成する。
HTML
<template data-x:loop="fruits">
  <div data-x:bind="fruits.*"></div>
</template>
  <div data-x:bind="fruits.0"></div>
  <div data-x:bind="fruits.1"></div>
  <div data-x:bind="fruits.2"></div>

ViewModelの展開

フレームワーク実行時、下記のようにViewModelクラスのプロパティを変換・展開する。
"@fruits"は、プライベート変数("__fruits")を作成し、getでその変数にアクセスする。

javascript
  /* "@fruits" = [ "バナナ", "りんご", "オレンジ" ]の変換 */
  "__fruits" = [ "バナナ", "りんご", "オレンジ" ];
  get "fruits"() {
    return this["__fruits"];
  }

"@fruits.*"は、getでインデックス変数$1を介して、"fruits"へアクセスする。

javascript
  /* "@fruits.*"の変換 */
  get "fruits.*"() {
    const [ $1 ] = this.$context.indexes;
    return this["fruits"][$1];
  };

プロパティ"fruits"と"fruits.*"を持つ場合、

  • "fruits"を配列と判断し"fruits"の値を取得し、
  • "fruits.*"を"fruits.0"、"fruits.1"、"fruits.2"へと展開する。
javascript
  /* fruitsの値(配列)より、fruits.*を展開 */
  get "fruits.0"() {
    this.$context.pushIndexes([0], () => {
      return this["fruits.*"];
    })
  };
  get "fruits.1"() {
    this.$context.pushIndexes([1], () => {
      return this["fruits.*"];
    })
  };
  get "fruits.2"() {
    this.$context.pushIndexes([2], () => {
      return this["fruits.*"];
    })
  };

"fruits.0"のプロパティの取得の流れ

"fruits.0"は、[0]をスタックし、"fruits.*"を呼び出す。
"fruits.*"は、スタックの[0]を参照し、this.fruits[0]の値を返す。

javascript
  get "fruits.*"() {
    const [ $1 ] = this.$context.indexes;
    return this["fruits"][$1];
  };
  get "fruits.0"() {
    this.$context.pushIndexes([0], () => {
      return this["fruits.*"];
    })
  };

HTML要素とViewModelをバインド

展開したhtmlのdata-x:bind="fruits.0"のdivとviewModelの"fruits.0"をバインドする。
"fruits.1"、"fruits.2"も同様にバインドする。

全部展開したところ

※Contextクラスは、配列のスタック(pushIndexes)と参照(indexes)を理解するため便宜的に記載

index-spa/module/main.js
class AppViewModel {
  "__fruits" = [ "バナナ", "りんご", "オレンジ" ];
  get "fruits"() {
    return this["__fruits"];
  }
  get "fruits.*"() {
    const [ $1 ] = this.$context.indexes;
    return this["fruits"][$1];
  };
  get "fruits.0"() {
    this.$context.pushIndexes([0], () => {
      return this["fruits.*"];
    })
  };
  get "fruits.1"() {
    this.$context.pushIndexes([1], () => {
      return this["fruits.*"];
    })
  };
  get "fruits.2"() {
    this.$context.pushIndexes([2], () => {
      return this["fruits.*"];
    })
  };
}

class Context {
  stacks = [];
  get indexes() {
    return this.stacks[this.stacks.length - 1];
  }
  pushIndexes(indexes, callback) {
    this.stacks.push(indexes);
    try {
      return callback();
    } finally {
      this.stacks.pop(indexes);
    }
  }
}

配列のリアクティブなプロパティ

アスタリスクを含む配列のプロパティを組み合わせて新しいプロパティを簡単に作ることができる。

javascript
const members = [
  { lastName:"田中", firstName:"一郎" },
  { lastName:"鈴木", firstName:"花子" },
  { lastName:"山田", firstName:"太郎" },
];
class AppViewModel {
  "@members#get" = () => members;
  "@members.*.lastName";
  "@members.*.firstName";
}

lastNameとfirstNameを連結するfullNameというプロパティを作るには、次の一行を加えるだけでよい。

javascript
  "@members.*.fullName#get" = () => `${this["members.*.lastName"]} ${this["members.*.firstName"]}`;

その場合、下記のように展開される。
"member.*"は、
 "members.0"、"members.1"、"members.2"
"member.*.lastName"は、
 "members.0.lastName"、"members.1.lastName"、"members.2.lastName"
"member.*.firstName"は、
 "members.0.firstName"、"members.1.firstName"、"members.2.firstName"
"member.*.fullName"は、
 "members.0.fullName"、"members.1.fullName"、"members.2.fullName"

"members.*.fullName"の定義をするだけで、"member.*.fullName"は、"member.*.lastName"、"member.*.firstName"と同様に展開され、同様にアクセスできる。

javascript
class AppViewModel {
/*
  "@members#get" = () => members;
  "@members.*.lastName";
  "@members.*.firstName";
  "@members.*.fullName#get" = () => `${this["members.*.lastName"]} ${this["members.*.firstName"]}`;
*/
  get members() { return members; }
  get "members.*"() {
    const [ $1 ] = this.$context.indexes;
    return this["members"][$1];
  }
  get "members.0"() {
    this.$context.pushIndexes([0], () => {
      return this["members.*"];
    });
  }
  get "members.1"() {
    this.$context.pushIndexes([1], () => {
      return this["members.*"];
    });
  }
  get "members.2"() {
    this.$context.pushIndexes([2], () => {
      return this["members.*"];
    });
  }
  get "members.*.lastName"() {
    return this["members.*"]["lastName"];
  }
  get "members.0.lastName"() {
    this.$context.pushIndexes([0], () => {
      return this["members.*.lastName"];
    });
  }
  get "members.1.lastName"() {
    this.$context.pushIndexes([1], () => {
      return this["members.*.lastName"];
    });
  }
  get "members.2.lastName"() {
    this.$context.pushIndexes([2], () => {
      return this["members.*.lastName"];
    });
  }
  get "members.*.firstName"() {
    return this["members.*"]["firstName"];
  }
  get "members.0.firstName"() {
    this.$context.pushIndexes([0], () => {
      return this["members.*.firstName"];
    });
  }
  get "members.1.firstName"() {
    this.$context.pushIndexes([1], () => {
      return this["members.*.firstName"];
    });
  }
  get "members.2.firstName"() {
    this.$context.pushIndexes([2], () => {
      return this["members.*.firstName"];
    });
  }
  get "members.*.fullName"() {
    return (() => `${this["members.*.lastName"]} ${this["members.*.firstName"]}`)();
  }
  get "members.0.fullName"() {
    this.$context.pushIndexes([0], () => {
      return this["members.*.fullName"];
    });
  }
  get "members.1.fullName"() {
    this.$context.pushIndexes([1], () => {
      return this["members.*.fullName"];
    });
  }
  get "members.2.fullName"() {
    this.$context.pushIndexes([2], () => {
      return this["members.*.fullName"];
    });
  }
}

"members.2.fullName"にアクセスした場合、下記のように流れる
get "members.2.fullName"(), indexes = [2]をスタック
 get "members.*.fullName"()
  get "members.*.lastName"()
   get "members.*"(), indexes = [2]を参照
  get "members.*.firstName"()
   get "members.*"(), indexes = [2]を参照

Discussion

ログインするとコメントできます