l12a

白ウサギを追え

Treee の進捗「ラベル・説明テキストの編集UI」編

Tree 2 再発明 改め「Treee」の開発進捗です。

今日までの進捗

ラベル説明テキストの編集UIを実装しました。

f:id:lnly:20190216020141g:plain
ラベルと説明テキストの編集

  • 表示状態と編集状態のボタンおよびインプット要素の実装
  • 自動的に高さを調整する textarea 要素のラッパーライブラリ導入
  • 子孫コンポーネントと vuex の分離

表示状態と編集状態の DOM 要素

f:id:lnly:20190216022235p:plain
編集中と通常時の DOM 要素切り替え
ラベル説明テキストは、表示状態は button 要素、編集状態は input / textarea 要素を切り替えるように実装しました。
編集状態かどうかの state を vuex にまで取り回すのが冗長に感じたので、コンポーネント内に data を持たせています。
これは、

  • コンポーネント内と vuex のそれぞれで「状態」を保持している
  • vuex に保持される「状態」が最終的なデータソースの形と一致している

と一長一短で判断に迷うところで、

  • 編集状態も vuex に state として保持する
  • button 要素との切り替えをやめて常時 input / textarea 要素にする

のどちらかに変更するかもしれません。CSS のスタイリングの自由度次第かなと思います。

textarea 要素のラッパーライブラリ導入

標準のtextarea 要素は、CSS で高さを変更可能ですが、中の要素に追従して大きさが変わったりはしません。
説明テキストを入力中に長文となった場合自動的にサイズを変更できるよう以下のライブラリを導入しました。

www.npmjs.com

子孫コンポーネントと vuex の分離

Treee のコンポーネントと状態管理の設計

Treee は最上位のコンポーネントだけが vuex と接続されていて、子孫コンポーネントへは props でデータを提供し、祖先コンポーネントへは this.$emit() でイベントや入力データを送る設計になっています。
これにより、子孫コンポーネントに下れば下るほど、そのコンポーネントが知りうる知識を限定することができ、結果的にコードをシンプルに保つことができると考えているからです。
どのコンポーネントが action を発行しているかなどを探索する必要もなくなり、コーディング時も楽できます。

反面、子孫・祖先へのバケツリレーがキツくなるといったデメリットもありますが、
これには Atomic Design に着想を得た Organisms > Molecules > Atoms までにコンポーネント粒度に制約を設けて、バケツリレーする階層に上限を作ることでその手間を一定までに抑止しています。

f:id:lnly:20190216015911p:plain
Vuex と コンポーネント群のデータフロー

木構造内の特定ノードのデータを更新する工夫

Treee は任意の階層の木構造を構築できるアプリです。 Vue.js 単体の場合は v-model により特定ノードとコンポーネントを紐づけることが容易にできました。

しかし、状態管理に vuex を使う場合 v-model の使用ではなく mutation によるデータ更新が推奨されていることと、前述したコンポーネントデータフローの兼ね合いから、以下の関数を mutation 内に設けて、特定ノードをシンプルに更新できるようにしました。

function findTargetNodeByNodeIndex (state: ITreeeState, nodeIndex: string): Node {
  return nodeIndex.split('.').reduce((accumulator: Node, currentValue: string) => {
    return accumulator.children.items[parseInt(currentValue, 10) - 1]
  }, state.treee)
}

第二引数の nodeIndex はノードの枝番を '.(ドット)' 繋ぎで表現した '1.1' や '1.1.2.1' などの文字列を取ります。
これを、

  [types.UPDATE_LABEL] (state, payload) {
    const targetNode = findTargetNodeByNodeIndex(state, payload.nodeIndex)
    targetNode.label = payload.value
  }

mutation ではこのように使ってデータを更新します。
先の関数 findTargetNodeByNodeIndex は Getter として一度実装しましたが、 Mutations から Getter を呼ぶことができなかったため、 mutations.ts 内に移しました。

今日はここまでです。