Vue.js始めるおれおれアドベントカレンダー2016 – 4日目

CRUDを全て含むTODOリストを作るのは、新しいフレームワークを試すのにとても良いと言います。というわけで作ってみましたのでチュートリアルって感じでご紹介します。

バージョン

  • Vue 2.1.4

デモ

こちらにございます。

デザイン

こんな感じでガーッとHTMLを作りました。

vue-todo

Bootstrap最高!

何もしないVueアプリ

まずはここから始めることにしましょう。

<div id="app">
  <div class="container">
    <h1>Vue TODO</h1>
    <div>
      <form>
        <div class="input-group">
          <input class="form-control" type="text" placeholder="Buy milk 2L">
          <span class="input-group-btn">
            <button class="btn btn-primary">Add new task</button>
          </span>
        </div>
      </form>
    </div>
    <hr />
    <div class="taskListOperator">
      <button class="btn btn-default">Delete finished tasks</button>
    </div>
    <div class="list-group">
      <label class="list-group-item">
        <input type="checkbox">
        Item 1
      </label>
      <label class="list-group-item">
        <input type="checkbox">
        Item 2
      </label>
      <label class="list-group-item">
        <input type="checkbox">
        Item 3
      </label>
    </div>
  </div>
</div>
.taskListOperator {
    text-align: right;
    margin-bottom: 1em;
}
var app = new Vue({
  el: '#app',
  data: {
  },
  computed: {
  },
  methods: {
  },
});

フォームをコンポーネント化する

なんか機能的に独立してそうな感じするのでさくっとコンポーネント化します。

<div>
  <task-form></task-form>
</div>
<script id="template-task-form" type="text/x-template">
  <form>
    <div class="input-group">
      <input class="form-control" type="text" placeholder="Buy milk 2L">
      <span class="input-group-btn">
        <button class="btn btn-primary">Add new task</button>
      </span>
    </div>
  </form>
</script>
var taskForm = {
  template: '#template-task-form',
};
window.app = new Vue({
  el: '#app',
  components: {
    taskForm: taskForm,
  },
  ...
});

一覧をVue化する

一覧の方もコンポーネント化……と思ったけど、繰り返しになるので先にVue化?してアプリが持つ情報の分だけ出力するようにまとめます。

<div class="list-group">
  <label v-for="task in tasks" class="list-group-item">
    <input type="checkbox">
    {{task.name}}
  </label>
</div>
data: {
  tasks: [
    { name: 'Buy milk 2L' },
    { name: 'Call to Alice' },
    { name: 'Return books' },
  ],
},

これでCRUDのR = Readが完成。なのか?

一覧項目をコンポーネント化する

良い感じになったのでコンポーネント化します。

<div class="list-group">
  <task-item v-for="task in tasks" :task="task"></task-item>
</div>
<script id="template-task-item" type="text/x-template">
  <label class="list-group-item">
    <input type="checkbox">
    {{task.name}}
  </label>
</script>
var taskItem = {
  template: '#template-task-item',
  props: [
    'task',
  ],
};
window.app = new Vue({
  el: '#app',
  components: {
    taskItem: taskItem,
  },
  ...
});

利用者の入力を受け付ける

ちょっと流れが複雑。

  1. アプリ本体側で、入力用のオブジェクト { name: '' } を用意する
  2. :task="newTask" で、フォームへ渡す
  3. :on-submit="newTask_submit" で、変更時のコールバックを指定する
  4. フォーム送信されたら(入力値が空でなければ)先のコールバックを実行
  5. コールバックから入力値を利用して何かする(まだ何もしてない)
  6. 新しい入力用のオブジェクト { name: '' } を設定することでフォームを初期化する

フォームはあくまで利用者の入力を受け付けて、親(アプリ本体)へ結果を伝えるだけ。

フォームの初期化なんかはフォームのコンポーネント内部で完結することもできるけど、なんかアプリ本体側でやった方が中央集権ぽくて良いかなと思った。思ったんだけど、ううん、自信ない。実際はVuexだか何だかでもっとうまくやるからあんまり考えない方が良いか。

<div>
  <task-form :task="newTask" :on-submit="newTask_submit"></task-form>
</div>
<form v-on:submit.prevent="form_submit">
  <div class="input-group">
    <input v-model="task.name" class="form-control" type="text" placeholder="Buy milk 2L">
    <span class="input-group-btn">
      <button class="btn btn-primary">Add new task</button>
    </span>
  </div>
</form>
window.app = new Vue({
  data: {
    newTask: { name: '' },
    ...
  },
  methods: {
    newTask_submit: function(event) {
      console.log(this.newTask.name);  // TODO: implement

      this.newTask = { name: '' };
    },
    ...
  },
  ...
});
var taskForm = {
  template: '#template-task-form',
  props: [
    'task',
    'on-submit',
  ],
  methods: {
    form_submit: function(event) {
      if (!this.task.name) {
        return;
      }

      this.onSubmit(event, this.task);
    },
  },
};

一覧へ追加する

これは簡単。

newTask_submit: function(event) {
  this.tasks.unshift(this.newTask);
  this.newTask = { name: '' };
},

配列に追加するだけで画面側の一覧も増える。はーVue便利。

これでCRUDのCができあがり。

ちなみに array.unshift(item) は配列の先頭に項目を追加するもの。 array.push(item) だと最後に追加する。本来なら期限情報なんかも持ってそれで並び替える感じだろうか。

チェックしたものを削除

今まで作業の情報は名前 name しかなかったけど、これに追加して完了したかどうか finished も持つようにしよう。そうしよう。

で、その finished フラグを見て終わったものを削除する。フィルターで取り除いた結果を代入しなおせば良いだけ。

<div class="taskListOperator">
  <button v-on:click="delete_click" class="btn btn-default">Delete finished tasks</button>
</div>
<label class="list-group-item">
  <input v-model="task.finished" type="checkbox">
  {{task.name}}
</label>
window.app = new Vue({
  methods: {
    delete_click: function(event) {
      this.tasks = this.tasks.filter(v=>!v.finished);
    },
    ...
  },
  ...
});

DRUDのD = Deleteもできたー!

あとUはどうしよう。更新。更新ボタン追加するか。

更新ボタンを用意

<label class="list-group-item">
  <span v-on:click.prevent="edit_click" class="pull-right btn btn-link">Edit</span>
  <input v-model="task.finished" type="checkbox">
  {{task.name}}
</label>
var taskItem = {
  methods: {
    edit_click: function(event) {
      // update if not canceled
      var newName = window.prompt('Task Name', this.task.name);
      if (typeof newName === 'string') {
        this.task.name = newName;
      }
    },
  },
  ...
};

ラベル内をクリックするので、意図せずチェックボックスが押されないよう v-on:click に .prevent をお忘れなく。

入力はなんかもう面倒なので prompt() を使ってしまった。いやーでも楽ちんだ。

そしてCRUDのU = Updateもおしまい。

一覧が空ならメッセージ

あ、忘れてた。全部終わったら空っぽになるので、わかるように何かメッセージを出せると親切。

<div class="list-group">
  <task-item v-for="task in tasks" :task="task"></task-item>
  <p v-show="tasks.length < 1" class="text-muted">No tasks. Yay!</p>
</div>

テンプレートの編集だけで済んじゃった。すげー。

できた

できました。

コンポーネントを橋渡しして新規項目を作成するところがややこしく感じた。んー噂のVuex導入したらサクッとできるのかな。やってみよう。