コンポーネント

batonjsにおけるコンポーネントの取り扱いと、defineComponent関数について説明します。

batonjsにおけるコンポーネントの取り扱い

batonjsはコンポーネントを定義するための特別な仕組みを持っておらず、もっぱらウェブコンポーネントを使います。
外部のウェブコンポーネントを使うこともできますし、batonjsを活用してウェブコンポーネントを作ることもできます。
特に、内部状態を持たないコンポーネントを簡単に作るために、batonjsはdefineComponent関数を提供しています。

batonjsでコンポーネントを使う方法をまとめると、次のようになります。

  • 使いたいウェブコンポーネントが既にある場合
    そのウェブコンポーネントを使いましょう。
  • 内部状態を持たないコンポーネントを作りたい場合
    下記のdefineComponentで簡単に作れます。
  • 内部状態を持つコンポーネントを作りたい場合
    ゼロからウェブコンポーネントを作りましょう。内部状態の管理にbatonjsを使うこともできます。

defineComponent

batonjsの仕組みを使ってウェブコンポーネントを定義します。batonjsでは、この関数で作ったウェブコンポーネントを「batonコンポーネント」と呼びます。
batonコンポーネントは内部状態を持つことができず、与えられた属性のみに依存して変わる純粋に関数的なものです。
また、Web標準の観点からは、batonコンポーネントはシャドウDOMを持たないカスタム要素です。

defineComponent(
  'aa-field', 
  `<div class="head">
    <label></label>
    <p class="error-message"></p>
  </div>
  <div class="body">
    <slot></slot>
    <p class="description"></p>
  </div>`, 
  state => ({
    ".head label": {textContent: state.label}, 
    ".head .error-message": {"style-display": state["error-message"] ? "block" : "none", textContent: state["error-message"]}, 
    ".body .description": {textContent: state.description}
  })
)
パラメーター
名前 デフォルト 説明
name string 省略不可 カスタム要素のタグ名
template string | HTMLTemplateElement | function 省略不可 コンポーネントの中身のテンプレート。当DOM要素のinnerHTMLプロパティにあたるもので、複数の要素(DocumentFragment)も可能です。
文字列の場合はそれがHTML文字列として解釈されます。
テンプレート要素の場合はその中身がクローンされます。
関数の場合は、それが呼び出されます。
show function 省略不可 コンポーネントの動的な部分を定義する関数。
baton関数に渡すshowと同じですが、監視中の属性リストがstateになります。
options オブジェクト {} customElements.defineの第3引数と同じです。
options.extends 文字列 N/A customElements.defineの第3引数と同じです。カスタム組み込み要素を定義するのに使われます。
observedAttributes Array|null null カスタム要素のobservedAttributesプロパティに設定される属性名のリスト。ここに列挙した属性は監視対象となり、属性値が変わるとコンポーネントが再描画されるようになります。
省略された場合・nullの場合は当関数が自動検出します。自動検出には限界がありますので注意してください(後述)。
返却値

なし

defineComponentの仕組み

defineComponentで作ったコンポーネントは、batonアプリケーションと同じ仕組みで動作します。
batonアプリケーションとbatonコンポーネントを対比してみると理解が進むでしょう。

比較項目 batonアプリケーション batonコンポーネント
状態 ページ状態。ユーザーの操作等に応じて変わる。 カスタム要素の属性リスト。属性値の変化に応じて変わる。
show関数 HTML文書全体(またはbaseElで指定したDOM要素)を基準とするUI宣言。 当カスタム要素を基準とするUI宣言。
対象となるHTML HTML文書全体(またはbaseElで指定したDOM要素)。 defineComponent関数の第2引数で与えられたテンプレートをクローンしたもの。それが当カスタム要素の中身になる。
スロットの扱い

コンポーネントの中身は2つの経路から与えられます。コンポーネントを作る側はdefineComponentの第2引数(テンプレート)として、コンポーネントを使う側はカスタム要素の子要素として。

通常は、カスタム要素の子要素は削除されて、defineComponentのテンプレートで上書きされます。
ただし、テンプレートがslot要素を含んでいる場合は、そのslot要素が古い中身(=カスタム要素の子要素)で上書きされます。
つまりコンポーネントを作る際、外部から子要素を受け取りたい場合は、子要素の位置にslot要素を書いておけばそこに差し込まれる、ということです。

監視属性の自動検出の限界

defineComponentの第5引数observedAttributesはコンポーネントの監視属性のリストですが、defineComponent関数はこの属性を自動検出できます。
自動検出は第3引数showをtoString関数で文字列にしたものを調査する形で行われますが、javascriptコードが一定の形であることを要求します。
ここでは、show関数でできること・できないことをリストアップします。

functionキーワードを用いた関数式を使えます function (state) {...}
アロー関数式を使えます state => ...
関数のパラメータ数は1以外にはできません (state, otherArg) => ...
関数パラメータの名前は自由に決められます anyName => ...
関数パラメータの分割代入は使えません ({a, b, c}) => ...
関数パラメータをそのまま別の変数に代入することはできません anyName => {var x = anyName; ...}
関数パラメータのプロパティへ、ドット表記法でアクセスできます state => ({foo: state.foo, ...})
関数パラメータのプロパティへ、ブラケット表記法でアクセスできます state => ({foo: state["my-foo"], bar: state['my-bar'], ...})
関数パラメータのプロパティへブラケット表記法でアクセスする際、リテラルでないプロパティ名を使うことはできません state => ({foo: state[someVariable], bar: state['my' + '-bar'], ...})

関数オブジェクトの文字列化はブラウザの進化と共に実装が変わる可能性があります。今は適切に動いていても、将来のいつか動かなくなることもあるかもしれません。
簡便性と保守性を天秤にかけて、検討のうえ使うようにしてください。