VueでTodoリストを作ろう #7 虚言

Webサイト制作記の第七弾を書いていきましょう。
今日は前回大体Todoリストとしての機能が定まってきてたのでいろいろ機能追加をやっていきましょうか。

VueRouterというページ遷移の機能(ライブラリ?)があるらしいのでそれについても勉強していきたいところ。

前回までのあらすじ

実装状況

タスクの追加が出来る。

選択したタスクを完了にすることが出来る。未完了に戻すことも。

こんな風に選択した項目を….

削除することが出来る。

コード

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>

main.js(HTMLと実際に書いているスクリプトを繋げるコード)

import app from './App.js';

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

App.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 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) => {
        this.$delete(this.tasks, e);
      });
    },
  },
});

export default component;

今回の目標

そういえば前回設定していた目標があったので観てみましょう。

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

なるほど。先週の私はこの三つをやって終わるつもりだったようです。
ただ、レイアウト調整は後回しにしてVueRouter関係に着手したいと思います。
具体的にはタスクの開始日や終了日などのタスク詳細を保存するようにし、それを別ページで表示するようにしたいと思います。この説明で伝わりますかね。とりあえずその辺りは後で着手するのでその時にまた詳しく説明を。

一旦上二つに書かれているバグ修正をしましょう。

バグ修正

さて、バグを修正していきましょう。
今日はここで終わるかもしれない。新しいライブラリを理解するのは大変なことなので。中世キリスト教徒が地動説や進化論を理解するくらい難しいです。そこまでではないかも。

一つ目:登録済みのタスクを登録しようとしたときの挙動調整

こちらどういうことかという説明から始めましょう。

現在こういう風にtask1とtask3が完了で登録されていますね。内部データ的にはこうなっています。

はい、タスク名がキーで完了状況のbooleanがバリューの辞書型で管理しています。ここでtask1を入力フォームから追加するとどうなるでしょうか。

こうなります。task1がもう一つ追加されるのでも何も起きないのでもなく、task1が未完了になってしまいました。これは何かおかしいです。

handleClick: function () {
      this.$set(this.tasks, this.message, false);
    },

原因はこの部分。handleClickという関数名もイケていないので変更したいですね。pushTaskとかの方がいい。
そして先ほどの問題が起きる理由としては$setを用いてthis.message(入力フォームの内容)がキーになっているものにfalseをセットするというだけの関数になっていることになります。

これに対してどう対処するか、については二つほど対処法があります。
一つは入力された名前のキーが既に存在する場合、後ろに”(1)”などとつけていく方法。task1があるときにtask1が追加されたらtask1(1)を追加するような感じです。
もう一つはタスク名が重複した場合エラーメッセージを表示してタスク追加を行わない方法です。

どちらを取るかについては悩みどころですが、エラーメッセージの表示は勉強していきたいので後者を取ります。前者は”(“+count+”)”みたいなのを追加するだけなので。

修正を実装する前にデータ名とかをちょこっと変えました。具体的にはフォームの内容格納変数とタスク追加ボタンの名前です。デモ版から変わっておらず、実態に沿っていなかったので。message→task、出力→追加に変更。

pushTask: function () {
      if (this.tasks.filter((e) => Object.keys(e) === this.task)) return;
      this.$set(this.tasks, this.task, false);
    },

一旦このような感じに実装してみました。エラーメッセージの表示は置いておいて、タスク名が重複した場合に配列の操作を行わないコードです。
Array.filter()を用いることでfor文などを用いなくても配列の全探査が出来ます。この場合だとthis.tasksの内容を一つずつeに格納し、e.keys()とthis.taskが一致しているものをフィルタリングしています。返り値は(e) => {~~~}の~~~に当てはまる要素を集めた配列です。

この場合、同じキーの要素があるかだけでいいので同様の動作で配列に当てはまる要素が一件でも存在するかを返すArray.findの方がいいかもしれません。Array.includesならbooleanを直接受け取ることも出来ます。今気づきました。どうせ何かしらうまく動かないと思うのでそのタイミングで修正します。

確かに上手くいきませんでしたが、なんか想定外のエラーです。Array.filterは関数ではないらしいです。嘘を教えてしまったのかもしれません。
いろいろと間違えていました。大噓つきです。

嘘ポイントをまとめます。

・Array.filter、Array.find等は辞書型には使えない
・Object.keysはオブジェクトのkeyを配列として取得するものなので使い方が違う(eには{タスク名:boolean}が入っていてkeyを取得するイメージだった

の二ポイントになります。
これに関して弁明をさせていただくと、this.tasksの型が{タスク名:完了状況,・・・・}になっているものを[{タスク名:完了状況},・・・・]だと勘違いしていたというのが原因になります。後者だったらこの実装で正しかった。正しかったんです。というより後者の方が実装としては賢いものに感じます。
忌呪帯法と同じでもう後戻りはできんので前者に対応した実装に変更します。もっとサクッと終わらせる予定だったんだけどな.…

pushTask: function () {
      const taskNames = Object.keys(this.tasks);
      if (taskNames.includes(this.task)) return;
      this.$set(this.tasks, this.task, false);
    },

こんな感じに変更しました。this.tasksのキーをtaskNamesに格納し、その中身をincludesで検索する方針に。多分これが一番早いと思います。

さて、動作やいかに.…?

この状態でtaskを追加すると、以前であれば既存のtaskが未完了になっていました。未完了にならなければ成功です。

8万回程クリックしてみましたがなりませんでした。成功です。
ではエラーメッセージを出していきましょう。ReactのMaterial-uiみたいにエラーメッセージも一緒に出してくれる入力フォームのコンポーネントがあるかもしれませんが、わからないしせっかくなので自分でやっていきましょう。

<input placeholder="タスクを入力" v-model="task">
  <button v-on:click="pushTask">追加</button>
  <p>{{error}}</p>
pushTask: function () {
      const taskNames = Object.keys(this.tasks);
      if (taskNames.includes(this.task)) {
        this.error = 'このタスクはすでに追加されています';
        return;
      }
      this.$set(this.tasks, this.task, false);
    },

こんな感じにしてみました。dataにerrorを追加し、既存タスクが追加された場合はthis.errorを編集するようになっています。
動作はいかに。

出来ました。なんかメッセージが浮いてるし赤い文字にしたいですがその辺はレイアウトの範疇でしょう。

もう一つのバグを修正してとっとと終わりましょう。時間が無い。

二つ目:タスク削除後のselectedの調整

さて、どういうことかというと、

こういう風に選択されているとき、内部データのselectedは以下のようになっています。

selectedに選択済みのtask2とtask4が追加されていますね。
問題はここで削除を選択した場合です。

task2とtask4を削除しました。さて、内部データは…?

selectedにtask2とtask4が残っていますね。
このまま削除や完了状況の変更を押しても特にエラーは起きない(それはそれで変)ですが、存在しないキーを検索するのは無駄手間ですしselectedに残す意味がありません。
完了状況を更新したりタスク削除を行った後は選択状況を白紙にするようにしましょう。これは簡単。

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

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

出来ました。this.selected.splice(0, this.selected.length);を追加して全削除するようにしただけです。動作確認します。

この状態で完了状況を切り替えると、以前ならtask1が選択されたままになっていました。

無事選択が解除されています。

selectedが空になっているのも確認できました。

削除でも同様です。別にこの編集が何かをもたらすことは現状ありませんが、違和感のある挙動は先につぶしておくのがいいものです。

終わりに

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

今日はこの二点を修正したところで時間が無くなったので終わりです。
次回はタスク情報をもっと詳細に保存できるようにしたり、各タスクを詳細表示するページの作成をやっていこうと思います。きっと一回では終わらないでしょう。#9までに終われば良しです。

参考文献

そういえば最近参考文献を載せていませんでしたが、大体このサイトを見ています。書き方自体はフィーリングでやっているので基本はリファレンスで関数の使い方を確認する程度です。

MDN Web Docs:
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/includes

助けてください。