2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue.js - コンポーネントとイベントハンドラの動的交換を、 v-ifを使わず実装する

Last updated at Posted at 2019-08-31

目的

  • コンポーネントを動的に切り替えたい
  • コンポーネント毎にイベントハンドリングの方法を変更したい

制約

v-if は使わない

状態を生やして if(a) { /**/ } else if (b) { /**/ } は、辛いのでやりたくない 。
例えば

<template>
  <div v-if="a">
    <!-- 何か表示 -->
  </div>
  <div v-if="b">
    <!-- 何か表示 -->
  </div>
  <div v-if="c">
    <!-- 何か表示 -->
  </div>
  <!-- 以下略 -->
</template>

  • パターンが増える都度、templateが肥大化する
  • 状態管理がどんどん増えて、scriptが肥大化する
  • 同じようなことを何度も書きたくない

ので、 v-if は使わない。

子コンポーネントはイベントを発火するだけ

<template>
  <div @click="onClick">
</template>

<script>
export default {
  methods: {
    onClick: function() {
      /* 具体的な挙動 */
    }
  }
}
</script>

のように書いてしまえば、後はコンポーネントの切り替えだけを考えれば良いので、話は簡単になる。
しかし、GUI設計的には

  • 子コンポーネントはイベントを発火するだけで、具体的な手続きは記述しない
    • シンプルかつステートレスな、単なる表示役にしたい
  • 親コンポーネントがイベントをハンドリングし、それに応じた処理を行う
  • 全体の統括や調整は、親コンポーネントの責務

という構造を逸脱したくない。
なので、子コンポーネントには具体的なイベントハンドリング内容は記述しない。

実装

<div id="app">
  <h2>switch component and event handler:</h2>
  <ul>
    <li>
      <input type="radio" name="mapping" value="hoge" @input="onSelect"> hoge
    </li>
    <li>
      <input type="radio" name="mapping" value="fuga" @input="onSelect"> fuga
    </li>
    <li>
      <input type="radio" name="mapping" value="piyo" @input="onSelect"> piyo
    </li>
  </ul>
  <component :is="current.component" @click="onClick"></component>
</div>
const hoge = {
  component: Vue.extend({
    template: '<button @click="onClick">hoge</button>',
    methods: {
      onClick: function() {
        this.$emit('click')
      }
    }
  }),
  onClick() {
  	alert('hoge')
  }
}
const fuga = {
  component: Vue.extend({
    template: '<button @click="onClick">fuga</button>',
    methods: {
      onClick: function() {
        this.$emit('click')
      }
    }
  }),
  onClick() {
  	alert('fuga')
  }
}
const piyo = {
  component: Vue.extend({
    template: '<button @click="onClick">piyo</button>',
    methods: {
      onClick: function() {
        this.$emit('click')
      }
    }
  }),
  onClick() {
  	alert('piyo')
  }
}

new Vue({
  el: "#app",
  data: {
    current: hoge,
  },
  computed: {
  	viewModelMap: function () {
    	return {
      	hoge, fuga, piyo
      }
    }
  },
  methods: {
    onClick: function () {
      this.current.onClick()
    },
    onSelect: function (event) {
      const viewModelKey = event.target.value

      this.mapping(viewModelKey)
    },
    mapping: function (viewModelKey) {
      const viewModel = this.viewModelMap[viewModelKey]
      
      if (!viewModel) {
        return
      }
      
      this.current = this.viewModelMap[viewModelKey]    
    }
  }
})

実装の説明

:is プロパティ

<component :is="current.component" @click="onClick"></component>

componentタグと合わせて、動的にコンポーネントを切り替えるための機能is に指定されたタグ or Vueコンポーネントが描画される。
is にはコンポーネント名やオプションオブジェクトの他、 VueConstructor そのものを渡しても良い。
↑のサンプルでは、 Vue.extend によって新たに生成した VueConstructor を渡している。

コンポーネント-イベントハンドリングの対応関係をオブジェクト化

const hoge = {
  component: Vue.extend({
    template: '<button @click="onClick">hoge</button>',
    methods: {
      onClick: function() {
        this.$emit('click')
      }
    }
  }),
  onClick() {
    alert('hoge')
  }
}

の部分。
IFとしては

interface EventHandleMap {
  readonly component: VueConstructor
  onClick(): void
}

な感じ。
実際に実装する時は、 Vue.extend ではなく単一ファイルコンポーネントで作成した内容をimport → component フィールドに設定、となる想定。

やったこと

どのコンポーネントで、どんなイベントハンドリングを行うか の対応関係のオブジェクト化。

効果

  • 子コンポーネントがシンプルかつステートレスに保てる
  • パターンが増えたら、子コンポーネント-イベントハンドリングの対応オブジェクトを追加するだけ
  • 子コンポーネントだけ/イベントハンドリングの方法だけ、という単位で交換可能。個々の実装は変更不要。

考察

最初に気になった点

イベントハンドリングの内容がVueコンポーネントから引き剥がされので、

  • コンポーネントの見通しが悪くなるかも
  • 「コンポーネント」という単位を考えると、イベントハンドリングの内容が外に引き剥がされるのは適切なのか

という点が当初気になった。

でもよく考えると、イベントハンドラの部分に書く内容は、動的な入れ替えが有っても無くても

  • イベントに応じた、コンポーネント依存の処理
    • 状態更新とか
  • イベントに応じた、 コンポーネント非依存の処理 の呼び出し

だけになるはず。
今回のサンプルでは、前者はたしかにイベントハンドラ内部で書かれている。
一方、後者 = コンポーネント非依存の処理 の呼び出しについては、いずれにせよそれ自体を独立したモジュールにして、コンポーネントのイベントハンドラからは、そのメソッドを呼び出すだけにするはず。

であれば

イベントハンドリングの内容が外に引き剥がされる

のはむしろ当然に思える

「イベントハンドリングの内容」と書いていたもの

では、今回の方法を思いついた当初の自分は、一体何を懸念していたのか。何故、このような「懸念」を抱くに至ったのか。
理由は、「イベントハンドリングの内容」として

A. コンポーネント依存の処理
B. コンポーネント非依存の処理

という分別が存在し得ることを、明示的に認識していなかったからと思われる。

Aについては、それはコンポーネント自身に関わることであるから、自身の内部にその内容が記述されるべきだと思う(オブジェクトの状態を変更する場合に、setterを定義せず、内部にその手続きを閉じ込めるのと同じ)(まあ普段は新しいオブジェクトを作っちゃうけど、ここでは「変更しないといけない」ということで)。
これが外部に引き剥がされてしまうのと、それはコンポーネントに関する知識がコンポーネントの外部に流出してしまうことになるので、好ましくない。

一方、Bはそもそもコンポーネント非依存なのだから、コンポーネントからは独立した状態 = 別モジュールとして定義/実装されているはず。
元々コンポーネントからは引き剥がされているのだから、それの呼び出し方が外部化されたとしてもそれは一枚層が増えただけで、状況は変わっていない。

  • コンポーネント依存の処理
    • コンポーネント内部に定義されるべきもの
    • コンポーネントから引き剥がされるべきでないもの
  • コンポーネント非依存の処理
    • コンポーネント外部に定義されるべきもの
    • コンポーネントから引き剥がされるべきもの

という2種類の処理が「イベントハンドリングの内容」には含まれ得る。それを明示的に認識していなかったがゆえに、先述のような「懸念」が湧き上がったのだと思う。

ふわっとしたメモ

表示に関することでも、全てコンポーネントの内部で解決する必要は無さそう。
表示について何かしらのロジックやルールが発生するなら、そのルールを表現するためのオブジェクトを作って細かい所は任せ、コンポーネントからはその結果を参照するのみにするのが良さそう。
今回で言えば

  • 「コンポーネントAを表示している時には、clickイベントに対してXの処理を対応させる」
  • 「コンポーネントBを表示している時には、clickイベントに対してYの処理を対応させる」

というルールが有り、その「コンポーネントとclick時の処理」という対応関係をオブジェクト化、コンポーネントはその内容を参照して、表示とイベントハンドリングを行う、という形。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?