VueでTodoリストを作ろう #5 妥協

お久しぶりです。Webサイト制作記、第何弾だったか忘れましたがやっていきましょう。第五弾かな?

前回の振り返り

前回は重篤な誤字を修正し、本実装に進む用意が整ったところまででした。

具体的なコードと動作はこちら。

ファイル構成

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

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>
  </div>`,
  data: function () {
    return { task: {}, message: '' };
  },
  methods: {
    handleClick: function () {
      console.log(this.message);
    },
  },
});

export default component;

実動作
1.画面

2.入力してボタンを押したときの動作(入力内容がコンソールに表示される)

3.入力内容がApp.js内のmessageに格納される

どうでもいいですが、noteは画像にタイトルが付けれないので見づらいですね。コードスニペットがあったのでそちらも試してみましたが、単色テキストで見にくかったのでボツに。(どうやらハイライトされないのは編集中のみのようなのでコードスニペットを使っています。)
最近沢山エディタのアップデートが入っているのでその辺にも期待したいところ。

今回の目標

今回はTodoリストらしく入力したタスクを配列に保存し、テーブルとして表示できるようにするのが目的です。
CSSレイアウトに関してはいったん放置し、動作系を完成させてしまいましょう。

余裕があればセレクトボックスを利用したタスクの完了/未完了フラグや削除なども実装していきたいです。

タスクの配列化

では早速タスクの配列化を実装していきましょう。

App.jsにはすでにtaskオブジェクトを用意しているので、messageの内容をtaskにpushしてあげればよさそうです。こういう横文字や英字が混じった文章は意識高い系のように見えてもやっとしますが、変数や関数名なので仕方がありません。

messageの内容はv-modelによって入力フォームの内容と連動しているので、handleClick内でtaskにpushするよう設定するだけです。
よく見ると過去の自分がtaskを辞書型にしていたので、恐らくkeyにタスク名を設定してvalueをtrue/falseにすることで完了/未完了を管理する想定だったのでしょう。そのように設計してあげます。

data: function () {
    return { task: {}, message: '' };
  },
  methods: {
    handleClick: function () {
      task[message] = false
    },
  },

ひとまずこんな感じで出来るでしょうか。クリックイベントが発生した際にmessageに対応するtaskの要素にアクセスし、falseを代入するコードです。
辞書型は存在しないkeyのデータを代入すると自動的に生成してくれることが多いので行けそうですが.…?試してみます。

this.を付け忘れたので怒られました。
自コンポーネントの要素にアクセスするときはthis.をつける。これ何度もやらかすので絶対に覚えておきましょう。絶対に。

this.task[this.message] = false;

thisを設定し、再度トライ。

無事追加できました。同名タスクが追加されたらどうするのかとかについては考慮が漏れていますが、一旦はいいでしょう。

良い感じに追加できていますね。

タスクをテーブルで表示する

配列化できても一覧表示が出来なくては意味がありませんね。
やっていきましょう。
Vueのテーブルについて私はよく知らないので調べてみます。

vue-good-tableというコンポーネントを使うか<td><th>を使って自作するかが主流のようです。せっかくなので自作しちゃいましょう。もうtdだのthだののやり方は覚えていませんが。

テーブルを表示するためのコンポーネントはApp.jsと別に用意します。
TaskTable.jsでいいでしょう。

const component = Vue.component('TaskTable', {
  name: 'TaskTable',
  template: `
  <table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
  </table>
  `,
  props: ['task'],
  data: {},
  methods: {},
});

export default component;

こんな感じで作りました。propsとしてApp.jsのtaskを受け取るようにしていますが、現状はヘッダーしか用意していないので意味をなしていません。
また、これに伴いApp.jsにも変更を加えました。

import taskTable from './TaskTable.js';

const component = Vue.component('app', {
  name: 'app',
  template: `
  <div>
  <input placeholder="タスクを入力" v-model="message">
  <button v-on:click="handleClick">出力</button>
  <taskTable ></taskTable>
  </div>`,
  component: {
    taskTable,
  },
  data: function () {
    return { task: {}, message: '' };
  },
  methods: {
    handleClick: function () {
      this.task[this.message] = false;
    },
  },
});

export default component;

TaskTable.jsを子コンポーネントとして受け取る用意を整え、templateに追記しています。この状態で描画するとどうなるでしょうか。

レイアウトを設定していないので当然ですが、質素なヘッダーが出力されました。これからtaskをpropsとして渡し、実際に表として出力していきます。

App.jsでtaskTableにtaskを渡すよう追記します。

<taskTable :task="task"></taskTable>

おわり。

TaskTable.jsで受け取ったtaskを表示するよう変更します。

<table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(key, value) in task">
      <td>
        <input type=checkbox>
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>

おわり。動かしてみましょう。

とりあえずタスクを三つほど追加してみましたがうんともすんとも言わず。

TaskTable.jsにデータを渡すことは問題なくできているようなので、描画に問題がありそうです。propsが更新されたときに再描画するような動作が必要なのでしょうが、おそらく何かそういうオプションみたいなのが抜けているのでしょう。

data: function () {
    return { task: { task1: false }, message: '' };
  },

一応初期タスクを設定してちゃんとコードが書けているのかは確認しておきましょう。

一応描画はできましたが、何か変ですね。本来タスク欄にはkeyの文字列が表示されるはずですが、valueの値が表示されています。また完了状況はvalueがtrueの際に完了になるはずなのでこちらも変です。keyとvalueが逆になっていると予想できます。
Vueの公式リファレンスを確認しましたが、やはりkeyとvalueの引数設定が逆になっていました。

<tr v-for="(key, value) in task">

ここです。(key, value)ではなく(value, key)が正しいです。

治りました。
後は更新の問題を何とかしなくては.…

const component = Vue.component('TaskTable', {
  name: 'TaskTable',
  template: `
  <table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(value, key) in tasks()">
      <td>
        <input type=checkbox>
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>
  `,
  props: ['task'],
  computed: {
    tasks() {
      return this.task;
    },
  },
  methods: {},
});

export default component;

これでどうでしょうか。computedを用いてpropsを都度確認するよう変更しました。できてるかな。

ダメでした。何も変わらない。
一度コンポーネント分割せずに表示できるかを試します。

import taskTable from './TaskTable.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>
  <taskTable :task="task"></taskTable>
  </div>`,
  component: {
    taskTable,
  },
  data: function () {
    return { task: { task1: false }, message: '' };
  },
  methods: {
    handleClick: function () {
      this.task[this.message] = false;
    },
  },
});

export default component;<table>
    <tr>
      <th>選択</th>
      <th>タスク</th>
      <th>完了状況</th>
    </tr>
    <tr v-for="(value, key) in taskList">
      <td>
        <input type=checkbox>
      </td>
      <td>{{key}}</td>
      <td>{{value ? "完了" : "未完了"}}</td>
    </tr>
  </table>

こうです。どうでしょうか。
出来ますね。どうやらコンポーネント分割に問題があるようです。子コンポーネントを動的に描画するにはどうするのでしょうか。

しばらく調べましたが進捗が無いので、あきらめましょう。これは厳しい。
同一コンポーネントでテーブル表示までやります。

出来ました。一旦これで良しとします。
そして現状問題が一つあり、出力ボタンを押した段階ではタスクがテーブルに追加されません。入力に触れたタイミングで初めて更新されます。
その問題を解決して終わりにしましょう。

データの反映が遅れる問題を解決する

これはVueのレンダリングのタイミングを調べればおそらく大したことは無いはず。入力フォームの変更ではレンダリングが走るが、ボタンのクリックでは走らないというだけの話。適当に言っています。

どうやらdataの更新は$setメソッドなど特定の方法でやらなければビューが更新されないそう。
なのでhandleClickのデータ更新を$setを使ったものに変更します。

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

こう。これで解決です。
ボタン入力に合わせて正常にデータと表示が更新されるようになりました。

伝わりにくいですが。

終わりに

そろそろ時間切れなので今日のところはここまでとします。
コンポーネント分割については諦めましたが、何とか目標は達成出来たので良いでしょう。

次回はタスクの完了設定、削除等をセレクトボックスを使ってできるように変更していきましょう。レイアウトは最後。
次回にはTodoリストとして完成と言えるものになるのではないかと思います。

それではまた来週。

助けてください。