Vue で動的にコンポーネントを追加

Vue で動的にコンポーネントを追加したい

よくある TODO アプリで言うと、ボタンを押したらTODO を追加したい。
UIとしたらこんな感じ
f:id:ororog:20180806191238p:plain
TODO追加ボタンを押すたびにコンポーネントを追加したい。

new するやり方(正しくないやり方)

先に書いておくとこのやり方はたぶん正しくないが、最初にこのやり方で調べてしまったので書いておく。

TodoItem を次のように定義する。

<template>
<li class="todo-item">{{text}}</li>
</template>

<script>
export default {
  name: 'TodoItem',
  props: {
    'text': String
  }
}
</script>

これをボタンが押されるたびに追加する。

TodoList の方はこう

<template>
<div class="todo-list">
  <div>
    <ul ref="todo-list">
    </ul>
  </div>
  <form v-on:submit.prevent="onSubmit">
    <textarea v-model="text"/>
    <input type="submit" value="TODO追加"/>
  </form>
</div>
</template>

<script>
import TodoItem from './TodoItem'
import Vue from 'vue'

export default {
  name: 'BadTodoList',
  components: {
    'todo-item': TodoItem
  },
  data () {
    return {
      text: ''
    }
  },
  methods: {
    onSubmit () {
      let clazz = Vue.extend(TodoItem)
      let instance = new clazz({
        propsData: {
          text: this.text
        }
      }).$mount()
      this.$refs['todo-list'].appendChild(instance.$el)
      this.text = ''
    }
  }
}
</script>

<style scoped>
.todo-list {
    text-align: left;
}
</style>

ポイントは、Vue.extend。Vue のコンポーネントはインポートしただけでは class の設計図の状態で、new できないようだ。そこで Vue.extend することで new できる形になる。
また、new しただけではまだ dom は作られていないので、$mount() をよんで上げることで実体化する。実体化したコンポーネントのdom は $el で参照できるので、それを appendChild してやればよい

data を使うやり方(正しそうなやり方)

そもそも、todo を vue のデータに乗っけないで自分で管理するのはおかしい。なので、以下のやり方が正しいはず。

<template>
<div class="todo-list">
  <div>
    <ul>
      <todo-item v-for="todo in todos" v-bind="todo" />
    </ul>
  </div>
  <form v-on:submit.prevent="onSubmit">
    <textarea v-model="text"/>
    <input type="submit" value="TODO追加"/>
  </form>
</div>
</template>

<script>
import TodoItem from './TodoItem'

export default {
  name: 'TodoList',
  components: {
    'todo-item': TodoItem
  },
  data () {
    return {
      text: '',
      todos: []
    }
  },
  methods: {
    onSubmit () {
      this.todos.push({
        text: this.text
      });
      this.text = ''
    }
  }
}
</script>

<style scoped>
.todo-list {
    text-align: left;
}
</style>

v-for として todo-item を呼び出して、todo 自体は array で管理する。
なんとなくだけど、前半のやり方は他のライブラリを呼び出す用途以外では基本的に間違っている気がする。