VueでTodoリストを作ろう #6 快調

Webサイト制作記第六弾、行きます。
今日でTodoリストの機能としては完成までこぎつけたいところ。

前回までのあらすじ

コード類

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>練習:Todo</title>
    <link ref="stylesheet" href="./src/main.css" />
    <script type="importmap">
      { "imports": {
        "vue":        "https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-beta.14/vue.esm-browser.js",
        "vue-router": "https://cdnjs.cloudflare.com/ajax/libs/vue-router/4.0.0-alpha.12/vue-router.esm.js",
        "vuex":       "https://cdnjs.cloudflare.com/ajax/libs/vuex/4.0.0-beta.2/vuex.esm-browser.js"
      } }
      </script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">  
      <h1>{{message}}</h1>
    </div>
    <script type="module" src="./src/main.js" ></script>
  </body>
</html>

App.js

import app from './App.js';

export default new Vue({
  el: '#app',
  name: 'main',
  components: { app },
  template: `<app></app>`,
});

Main.js

const component = Vue.component('app', {
  name: 'app',
  template: `
  <div>
  <input placeholder="タスクを入力" v-model="message">
  <button v-on:click="handleClick">出力</button>
  <table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(value, key) in task">
      <td>
        <input type=checkbox>
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>
  </div>`,
  data: function () {
    return { task: {}, message: '' };
  },
  methods: {
    handleClick: function () {
      this.$set(this.task, this.message, false);
    },
  },
});

export default component;

実行結果

フォームに入力した後「出力」ボタンをクリックすると、タスクが配列に登録され表として表示されるようになっています。
もうほぼ完成と言って差し支えない。

今日は選択ボックスを用いて完了状況をいじったりタスクを削除したりという機能を実装していこうと思います。
余裕があればレイアウトにも触れていきたい。

選択された内容を保存する

タスクの削除や完了状況の変更を行うためには、選択ボックスの状況とそれに対応するタスクデータを取得する必要があります。

こうなってたら{task1: false}と{task3: false}が保存されていなくてはなりません。現状は選択ボックスをどういじっても特に何も保存されるようになっていません。ただ青くなったり白くなったりするだけ。

とりあえず勘で実装を進めてみます。前回くらいまでは環境設定みたいなもんだったのでガンガン調べながら進めていましたが、ここからはある程度自力で実装していくようにします。そうすると恐らく思うように動作しないのでそうなったら調べて改善するようにしようと思います。

とはいえチェックボックス周りの仕様をすっかり忘れてしまったので公式リファレンスを見ながら調べます。話の軸が無くて申し訳ない。

const component = Vue.component('app', {
  name: 'app',
  template: `
  <div>
  <input placeholder="タスクを入力" v-model="message">
  <button v-on:click="handleClick">出力</button>
  <table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(value, key) in task">
      <td>
        <input type=checkbox value="key" v-model="selected">
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>
  </div>`,
  data: function () {
    return { task: {}, message: '', selected: [] };
  },
  methods: {
    handleClick: function () {
      this.$set(this.task, this.message, false);
    },
  },
});

export default component;

なんとなく実装してみました。主な変更結果は以下の通り。

<input type=checkbox value="key" v-model="selected">

data: function () {
    return { task: {}, message: '', selected: [] };
  },

チェックボックスのvalueにタスク名を保存するようにし、v-modelでdataに追加した配列selectedに選択されたタスク名を格納するようになっています。v-modelはチェックボックスが単体の場合はbooleanの値を格納しますが、配列に接続することでvalueを格納するようになるよう。ここは動作が怪しいポイントかもしれません。タスクを一つしか保存していない場合が不安だ。

しかしこんな面倒そうな操作がたった20文字そこらでできる。それがいいところなのです。Vueに限らずフレームワーク系の。(一応技術記事なのでこういうことも書いていくことにした)

とりあえず動作確認してみます。

Oops‼valueの書き方を間違えたのでkeyと保存されてしまっています。
これは痛恨のミス。valueの値に変数を適用する際はv-bindを設定するのが必須なようです。
v-bind:value=”key”と記載するのが正しい書き方です。
v-bindは:value=”key”と省略することもできます。

修正したところ無事選択したタスクの名前が格納されるよう変更されました。

タスクが増えてもこの通り正常に動作しています。(タスク1以外を選択しています)
これで選択された内容の保存は完成しました。
次はタスクの完了/未完了の切り替えです。

タスクの完了状況を切り替える

まずは切り替えボタンと切り替えるためのメソッドを用意します。

const component = Vue.component('app', {
  name: 'app',
  template: `
  <div>
  <input placeholder="タスクを入力" v-model="message">
  <button v-on:click="handleClick">出力</button>
  <table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(value, key) in task">
      <td>
        <input type=checkbox :value="key" v-model="selected">
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>
  <button v-on:click="toggleTaskStatus" >完了状況切替</button>
  </div>`,
  data: function () {
    return { task: {}, message: '', selected: [] };
  },
  methods: {
    handleClick: function () {
      this.$set(this.task, this.message, false);
    },

    toggleTaskStatus: function () {
      console.log('動いてます');
    },
  },
});

export default component;

完了です。完了状況切替用の関数toggleTaskStatusを用意し、それを呼び出すボタンを</table>の下に配置しています。

画面はこんな感じ。

ボタンをクリックするとコンソールにログが出るのでtoggleTaskStatusを正常に呼び出せているのも確認できました。この辺はそう書いた以上出来て当然なのでこれ以降は確認しません。
これからtoggleTaskStatusの中身を編集していきます。

と言ってもやることは単純で、taskの中身を確認し、先ほど取得したselectedに格納されている文字列と同じkeyの値を反転させてあげるだけの作業です。

toggleTaskStatus: function () {
      this.selected.forEach((e) => {
        this.tasks[e] = !this.tasks[e];
      });
    },

こんな感じになりました。selectedに格納されている値がtasksに必ず存在する前提なので少し怪しい実装ですが、まぁいいでしょう。そんなことを言ったらそもそも同じタスク名を複数追加した場合の挙動も不安定です。一旦形だけでも完了を目指しましょう。
そういえばタスクを管理する変数の名前をtasksに変更しました。タスクを複数格納するのにtaskはおかしいなと思ったので。

ちゃんと選択したタスクを完了にすることが出来ました。未完了に戻すこともできます。
完了状況の切り替えについては完成でいいでしょう。
次はタスクの削除に移ります。

選択したタスクを削除する

完了状況切替の時と同じようにボタンとメソッドを追加します。
実装以外はやることが全く一緒なので実装も進めます。

const component = Vue.component('app', {
  name: 'app',
  template: `
  <div>
  <input placeholder="タスクを入力" v-model="message">
  <button v-on:click="handleClick">出力</button>
  <table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(value, key) in tasks">
      <td>
        <input type=checkbox :value="key" v-model="selected">
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>
  <button v-on:click="toggleTaskStatus" >完了状況切替</button>
  <button v-on:click="deleteTask" >削除</button>
  </div>`,
  data: function () {
    return { tasks: {}, message: '', selected: [] };
  },
  methods: {
    handleClick: function () {
      this.$set(this.tasks, this.message, false);
    },

    toggleTaskStatus: function () {
      this.selected.forEach((e) => {
        this.tasks[e] = !this.tasks[e];
      });
    },

    deleteTask: function () {
      this.selected.forEach((e) => {
        delete this.tasks[e];
      });
    },
  },
});

export default component;

こんな感じになりました。削除buttonが追加され、deleteTaskメソッドが追加されています。delete演算子は初めて使うので本当に動くか少し心配ですがやってみましょう。

完了状況切替も確認しつつ.…
削除ボタンを押下してみます。

出来ました。
ただタスク1のチェックが入っていることから想像できるかもしれませんが、削除ボタンの押下と画面上での削除が同期しません。

解決しました。
前回もタスクの追加でVueのリアクティブ(データの変更と同時に画面を再描画するやつ)に苦しめられましたが、その際は配列にpushで追加するのではなく$setを使って追加することで解決しました。
Vueの機能を使いたきゃVueのメソッド使わんかいという話のようです。
今回も同様に配列の要素削除用メソッドがあったのでそちらを使います。

変更前

deleteTask: function () {
      this.selected.forEach((e) => {
        delete this.tasks[e];
      });
    },

変更後

deleteTask: function () {
      this.selected.forEach((e) => {
        this.$delete(this.tasks, e);
      });
    },

これでリアクティブが正常に働くはずです。

さて、判定やいかに.…?

無事削除ボタンを押すと同時にタスクが削除されました。

連想配列の追加:this.$set(array, key, value)
連想配列の削除:this.$delete(array, key)

これは必須です。覚えておきましょう。私はVueを使って三年くらい経つのに知らなかった。(忘れていた、別の方法がある可能性もあり)

おわりに

ひとまず今日作成したかったタスクの完了状況設定、削除機能を実装できたので今日はこれで終わりにしましょう。時間もちょうどいい。

本来この記事作成は1.5時間で終わる見積もりなのですが、今回のようにあまり詰まることなく順調に進んでも2時間ほどかかっているので、次回からもうちょっと目標を減らすか最初から見積もり時間を増やすかしなくては。

次回は

・登録済みのタスクを登録しようとしたときの挙動調整(今は既存タスクが未完了になって終わるようになっている)
・タスク削除後のselectedの調整(存在しないタスクが選択済みになってしまう)
・レイアウト調整

の三本でお送りしようと思います。その時のフィーリングで変更の可能性はありますが。

それではまた来週。ありがとうございました。

助けてください。