【Vue.js】Vue-CLI開発環境下で作るTODOアプリ作成


#コンポーネントの作成
まずは見た目から整えていきましょう。

作るコンポーネントは、以下の四つです。

- Header
- Main
- Sidebar
- List


コンポーネントの配置は以下のようになっています。

![As2jH1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/a807a2d2-2661-219a-6f81-c85bce37f44a.png)

それぞれコードを記述していきます。 

コンポーネントのディレクトリは、**sample-app/src/components**に入れます。 
まずは、見た目だけを整えたいので、そのままコピペしてもらっても大丈夫です。


##Header.vue
```vuejs:header.vue
<template>
  <header class="header">
    <h1 class="header__ttl">タスク管理アプリ</h1>
  </header>
</template>

<style lang="scss" scoped>
  .header {
    position: sticky;
    top: 0;
    background-color: #1876d1;
    padding: 1rem 3rem;
    z-index: 999;
    &__ttl {
      color: #fff;
    }
  }
</style>
```
##Sidebar.vue
```vuejs:sidebar.vue
<template>
  <aside class="sidebar">
    <ul class="sidebar__list">
      <li class="sidebar__item">
        <a class="sidebar__link" href="#">TODOリスト</a>
      </li>
      <li class="sidebar__item">
        <a class="sidebar__link" href="#">このサイトについて</a>
      </li>
    </ul>
  </aside>
</template>

<style lang="scss" scoped>
  .sidebar {
    position: sticky;
    top: 5rem;
    height: 100%;
    min-height: calc(100vh - 5rem);
    background: #f0f1f4;
    flex-basis: 17rem;
    width: 17rem;
    &__list {
      padding: 2rem;
      list-style-type: none;
    }
    &__item:not(:first-child) {
      margin-top: 2rem;
    }
    &__link {
      font-size: 1.25rem;
      text-decoration: none;
      color: #333;
      font-weight: bold;
    }
  }
</style>
```
##Main.vue
```vuejs:main.vue
<template>
  <main class="main">
    <slot />
  </main>
</template>

<style lang="scss" scoped>
  .main {
    flex-basis: calc(100% - 17rem);
    width: calc(100% - 17rem);
    padding: 5rem;
  }
</style>
```
##List.vue
```vuejs:list.vue
<template>
  <div class="tasks">
    <ul class="tasks__list">
      <li class="tasks__item">タスク１</li>
      <li class="tasks__item">タスク2</li>
      <li class="tasks__item">タスク3</li>
    </ul>
    <div class="form">
      <form class="form__body">
        <input type="text" plabceholder="新しいタスク" class="form__input" />
        <input type="submit" class="form__submit" />
      </form>
      <input type="button" class="form__done" value="タスクを完了にする" />
    </div>
  </div>
</template>

<style scoped lang="scss">
  .tasks {
    margin-top: 3rem;
    &__list {
      font-weight: bold;
      list-style: none;
    }
    &__item {
      padding-bottom: 0.75rem;
      border-bottom: 1px solid #dedede;
    }
    &__item:not(:first-child) {
      margin-top: 1.25rem;
    }
  }
  .form {
    margin-top: 2rem;
    display: flex;
    justify-content: space-between;
    &__body {
      display: flex;
      width: 80%;
      border: 1px solid #dedede;
    }
    &__input {
      flex: 1;
      padding: 0 0.5rem;
      border: none;
      outline: none;
    }
    &__submit {
      width: 180px;
      background-color: #1876d1;
      color: #fff;
      padding: 0.5rem 0;
    }
    &__done {
      width: 180px;
      background-color: #1876d1;
      color: #fff;
      padding: 0.5rem 0;
      border: 1px solid #fff;
      margin-left: 3rem;
    }
  }
</style>


```
それぞれコンポーネントが作成できたら、App.vueを編集していきましょう。
App.vueは、**全てのコンポーネントの親のコンポーネント**です。

App.vueは、sample-app/src/App.vueにあります。
ここに、全ページで共通の、Header,Sidebar,Main コンポーネントを出力させます。

デフォルトの状態だと以下のようになっているかと思います。
##App.vue
```vuejs:App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view />
  </div>
</template>

<style lang="scss">
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
  }

  #nav {
    padding: 30px;
    a {
      font-weight: bold;
      color: #2c3e50;
      ¥ &.router-link-exact-active {
        color: #42b983;
      }
    }
  }
</style>
```
まずは Header,Sidebar,Main コンポーネントをコンポーネントとして登録しましょう。

```vuejs:App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view />
  </div>
</template>

<script>
  // @ is an alias to /src
  import Header from "@/components/Header.vue";
  import Sidebar from "@/components/Sidebar.vue";
  import Main from "@/components/Main.vue";

  export default {
    components: {
      Sidebar,
      Header,
      Main,
    },
  };
</script>

<style lang="scss">
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
  }

  #nav {
    padding: 30px;

    a {
      font-weight: bold;
      color: #2c3e50;

      &.router-link-exact-active {
        color: #42b983;
      }
    }
  }
</style>
```
追加したのは以下の部分です。

```vuejs
<script>
  // @ is an alias to /src
  import Header from "@/components/Header.vue";
  import Sidebar from "@/components/Sidebar.vue";
  import Main from "@/components/Main.vue";

  export default {
    components: {
      Sidebar,
      Header,
      Main,
    },
  };
</script>
```

mport で、コンポーネントを読み込んでから、

```javascript
import Header from "@/components/Header.vue";
import Sidebar from "@/components/Sidebar.vue";
import Main from "@/components/Main.vue";
```

以下の部分でコンポーネントとして登録しています。

```javascript
export default {
  components: {
    Sidebar,
    Header,
    Main,
  },
};
```
次に、読み込んだコンポーネントを使い、レイアウトを整えます。

```vuejs
<template>
  <div id="app">
    <Header></Header>
    <div class="flex">
      <Sidebar></Sidebar>
      <Main>
        <router-view />
      </Main>
    </div>
  </div>
</template>

・ ・ ・ (省略) ・ ・ ・

<style lang="scss">
  /* リセット css */

  *,
  *::before,
  *::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  input[type="submit"],
  input[type="button"] {
    border-radius: 0;
    -webkit-box-sizing: content-box;
    -webkit-appearance: button;
    appearance: button;
    border: none;
    box-sizing: border-box;
    &::-webkit-search-decoration {
      display: none;
    }
    &::focus {
      outline-offset: -2px;
    }
  }
  .flex {
    display: flex;
    align-items: flex-start;
  }
</style>
```
編集を終えて、ページを開いてみると、以下のようになっているかと思います。
![hjFox4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/58715573-98d1-b9af-bc05-d332a1c70e63.png)


続いて、ページ遷移先のコンポーネントを編集していきます。

sample-app/src/views/にあるコンポーネントは、
Vue route から呼び出せるコンポーネントが入っています。

この中に新しく、Todo.vueを作成します。

Todo.vueの中に、先ほど作成した、List コンポーネントを出力させます。

#Todo.vue
```vuejs:todo.vue
<template>
  <div>
    <h2>TODOリスト</h2>
    <List></List>
  </div>
</template>

<script>
  import List from "@/components/List.vue";

  export default {
    components: {
      List,
    },
  };
</script>
```

#Vue router の編集
続いて、 vue routerを編集していきます。

```javascript:router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
```
デフォルトでは、ルートパス(/)のコンポーネントが、Homeになっています。

こちらを先ほど作成した、Todo.vueに変更しましょう。

```javascript:router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Todo from "../views/Todo.vue"; // 変更

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Todo", // 変更
    component: Todo, // 変更
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
  },
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

export default router;
```
ここまでできたら、以下のようになっているかと思います。
![GRH5aA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/96c2d15d-498c-7f79-945f-94fdb569aec3.png)

#タスク追加機能
続いて、タスク作成の機能を作成していきます。

タスク作成に関しては、Listコンポーネントを編集していきます。

まずは、新しく追加するタスクの名前を格納するために、
newTaskNameという変数を用意しましょう。

```vuejs:list.vue
(省略)

<script>
  export default {
    data() {
      return {
        newTaskName: "",
      };
    },
  };
</script>

(省略)
```

次に、フォームのinputと紐付けるために、v-modelで、newTaskNameを指定します。

```vuejs:list.vue
(省略)

<div class="form">
  <form class="form__body">
    <input
      type="text"
      v-model="newTaskName"
      plabceholder="新しいタスク"
      class="form__input"
    />
    <input type="submit" class="form__submit" />
  </form>
  <input type="button" class="form__done" value="タスクを完了にする" />
</div>

(省略)
```
試しに、Vue.js Developtool で、データの中身を確認してみましょう。

List コンポーネントを選択し、データの中身を確認してみてください。正常にできててたら、以下のようになっているかと思います。

![fEg4fd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/c2035cc4-b73d-b123-871b-0a157cde231f.gif)
続いて、TODO リストのデータを配列に保存する処理を追加しましょう。 data に新しく、tasksという空の配列を用意します。

```javascript:list.vue
  data() {
    return {
      tasks: [],
      newTaskName: ""
    };
  },
```
次に、この tasks に、newTaskName を保存する処理を追加します。 methodsに、addTaskというメソッドを追加し、以下のような処理を記述します。

```javascript:list.vue
methods: {
    addTask() {
      this.tasks.push({
        name: this.newTaskName
      });
      this.newTaskName = "";
    }
  }
```
用意した、変数には、thisでアクセスすることができます。

this.tasksで、用意した配列にアクセスし、push メソッドで配列を追加しています。

最後に、「this.newTaskName = "";」で、newTaskNameに空の文字を入れて、input の中身を空にしています。

次に、フォームが送信されたタイミングで、この、addTask、メソッドを実行するようにします。

```javascript:list.vue
<div class="form">
  <form class="form__body" v-on:submit="addTask">
    <!-- 変更 -->
    <input
      type="text"
      v-model="newTaskName"
      plabceholder="新しいタスク"
      class="form__input"
    />
    <input type="submit" class="form__submit" />
  </form>
  <input type="button" class="form__done" value="タスクを完了にする" />
</div>
```
v-on:submit="addTask"で、フォームが送信された時のイベントを追加することができます。
これで、送信されたタイミングで、addTask メソッドが実行されます。
しかし、この状態だと、正常に動作しません。
HTML には、form を送信すると、デフォルトでページがリロードされてしまう性質が あるからです。(試しのこの状態で実行してみると、ページがリロードされてしまうことが確認できると思います)

これは、v-on:submit.preventを用いることで解決します。

```javascript:list.vue
<div class="form">
  <form class="form__body" v-on:submit.prevent="addTask">
    <!-- 変更 -->
    <input
      type="text"
      v-model="newTaskName"
      plabceholder="新しいタスク"
      class="form__input"
    />
    <input type="submit" class="form__submit" />
  </form>
  <input type="button" class="form__done" value="タスクを完了にする" />
</div>
```

また、v-onは、@で省略できるので、以下のように記述してもよいでしょう。

```javascript:list.vue
<div class="form">
  <form class="form__body" @submit.prevent="addTask">
    <!-- 変更 -->
    <input
      type="text"
      v-model="newTaskName"
      plabceholder="新しいタスク"
      class="form__input"
    />
    <input type="submit" class="form__submit" />
  </form>
  <input type="button" class="form__done" value="タスクを完了にする" />
</div>]
```
この状態で、何かしらの値を送信してみましょう。

うまくいっていれば、以下のように、tasks 配列に、値が追加されているのがわかるかと思います。

![eR45aA.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/37341d6c-7004-05da-a4a7-f6ab8a268722.gif)

#追加したタスクの表示
次に、追加したタスクを表示できるようにしましょう。

配列を表示するには、v-forを使用し、リストの部分を以下のように書き換えます。

```javascript:list.vue
<ul class="tasks__list">
  <li class="tasks__item" v-for="task in tasks" :key="task.key">
    {{ task.name }}
  </li>
</ul>
```
うまくいっていれば以下のように動作します。

![hGif53.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/6a99cf10-4d9a-9551-ebdf-a7f5d41521af.gif)

#Vuex を用いたデータ管理
次に、コンポーネントのデータを使っていたものを、Vuex で管理してみましょう。

今回のように小規模なアプリケーションでは、Vuex を使うのは助長かもしれませんが、学習のため、Vuex を用いていきます。
![A3s0Sa.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/63b93c7d-a3a2-d5a5-2b78-5e7b89ecc18f.jpeg)

Store は、sample-app/src/store/index.jsにあります。

``````javascript:store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {},
});
```
データの保持は state で行うので、staet に、空の配列を用意しておきます。

```javascript:store/index.js
(省略)

  state: {
    tasks: []
  },

(省略)
```

また、コンポーネント内の tasks 配列は使用しないので、消しておきましょう。

```javascript:store/index.js
(省略)

data() {
  return {
    newTaskName: ""
  };
},

(省略)
```

まずは、コンポーネントから、アクションを呼び出す処理を記述します。

store を呼び出すには、this.$storeと記述します。

store の中のアクションを呼び出すには、dispatchメソッドを使用します。第一引数に、アクションのメソッド名を入れ、第二引数に、受け渡すデータを記述します。

```javascript:store/index.js
addTask() {
  this.$store.dispatch("addTask", this.newTaskName);
  this.newTaskName = "";
}
```

次に、アクションで、ミューテーションにデータを渡す処理を記述しましょう。

name には、dispatchメソッドの第二引数の値が入ります。

commit メソッドを使うことで、ミューテーションを呼び出すことができます。

```javascript:store/index.js
actions: {
  addTask({ commit }, name) {
    commit('addTask', name)
  },
},
```

次に、store にデータを格納するために、ミューテーションを記述します。

コンポーネントに記載した処理とほとんど同じですが、store を引数に入れており、store にアクセスできるようになっています。

```javascript:store/index.js
mutations: {
  addTask(state, name) {
    state.tasks.push({
      name: name
    });
  }
},
```
最後に、保存したデータを、store からコンポーネントに呼び出すように変更します。

tasks を呼び出す箇所を、state から呼び出すように変更しています。コンポーネントから state にアクセスするには、$store.stateと記述します。

```javascript:list.vue
<ul class="tasks__list">
  <li class="tasks__item" v-for="task in $store.state.tasks" :key="task.key">
    {{ task.name }}
  </li>
</ul>
```

正常にできていれば以下のように動作します。
![hGrg47.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/06067267-1fd4-4f0f-2ff7-5eb9ac948461.gif)

#タスク完了機能
続いて、タスクを完了できるような処理を追加していきます。

まず準備として、tasks 配列に追加するオブジェクトに、id と、isDone を追加します。 id は、どのタスクかを識別することに使用します。isDone は、完了したかどうかを確かめるために追加します。

また、sotre に、最新の id を保存するためのnextTaskIdを追加します。

タスクを追加したら、次に追加するタスクの id が一つ増えているように、state.nextTaskId++と記載しましょう。

```javascript:store/index.js
state: {
  tasks: [],
  nextTaskId: 0, // 追加
},
mutations: {
  addTask(state, name) {
    state.tasks.push(
      {
        id: state.nextTaskId, // 追加
        name: name,
        isDone: false // 追加
      },
    )
    state.nextTaskId++ // 追加
  },
```
試しにタスクを追加してみると、id と、isDone が追加されているのがわかるかと思います。

![gAe23R.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/396c3324-40df-1f51-1d06-376b4f005f93.png)

次に、完了したいタスクを選択し、選択したタスクを配列に格納する処理を追加します。格納するデータ名は、doneTaskIdsとします。

```javascript:list.vue
data() {
  return {
    newTaskName: "",
    doneTaskIds: [] // 追加
  };
},
```
タスクを選択できるように、チェックボックスを用意します。

```javascript:list.vue
<li class="tasks__item" v-for="task in $store.state.tasks" :key="task.key">
  <input
    type="checkbox"
    :id="task.key"
    :value="task.id"
    v-model="doneTaskIds"
  />
  {{ task.name }}
</li>
```

チェックボックスの値を、タスクの id として、それぞれ識別できるようになっています。v-model に、doneTaskIdsとすることで、選択肢ている id が配列として格納されるようになっています。

実際に画面から確かめてみましょう。

このように選択しているタスクの id だけ配列に格納されているはずです。

![gAe23R.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/dabfe98b-7119-adc5-ec47-2030c1b02218.png)


次に、選択肢ているタスクだけ、isDoneをfalseからtrueに書き換えます。

データに変更を加えるのはミューテーションの役割なので、アクションを経由して、doneTaskIdsをミューテーションに渡しましょう。

methods に、doneTasks メソッドを追加し、アクションにdoneTaskIdsを渡します。

また、イベントが発火したら、配列が空になるように、this.doneTaskIds.length = 0;と記載します。

```javascript:list.vue
methods: {
  addTask() {
    this.$store.dispatch("addTask", this.newTaskName);
    this.newTaskName = "";
  },
  doneTasks() { // 追加
    this.$store.dispatch("doneTasks", this.doneTaskIds); // 追加
    this.doneTaskIds.length = 0; // 追加
  } // 追加
}
```
また、「タスクを完了にする」ボタンをクリックしたときに、このイベントが発火するように、v-on を設定しましょう。

```javascript:list.vue
<input
  type="button"
  class="form__done"
  value="タスクを完了にする"
  @click.prevent="doneTasks"
/>
```
アクションで受け取り、ミューテーションに渡します。

```javascript:store/index.js
actions: {
  addTask({ commit }, name) {
    commit('addTask', name)
  },
  doneTasks({ commit }, ids) { // 追加
    commit('doneTasks', ids) // 追加
  } // 追加
},
```
ミューテーションから、配列にあるタスクのisDoneをtrueに変えます。

forEachメソッドで、配列にある id をひとつずつ取り出し、対象のタスクのisDoneを変更しています。

``````javascript:store/index.js
mutations: {
  addTask(state, name) {
    state.tasks.push(
      {
        id: state.nextTaskId,
        name: name,
        isDone: false
      },
    )
    state.nextTaskId++
  },
  doneTasks(state, ids) { // 追加
    ids.forEach(id => { // 追加
      state.tasks[id].isDone = true // 追加
    }) // 追加
  } // 追加
},
```

ここまでできたら、正常にデータが変更されるかを確かめてみましょう。

以下のように、選択しているタスクのみ、データが更新されていれば大丈夫です。
![Ajsj2e.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/2251f13b-5a69-6992-6891-1bacfe41840e.gif)
次に、完了していないタスクのみを一覧表示するように変更しましょう。

データに何かしらの加工を加えて取り出すには、state から直接取り出すのではなく、ゲッターを経由して取り出します。

![A3s0Sa.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/177cda00-09a8-4449-2793-7fec0104d7d5.jpeg)

ゲッターに、isDoneがfalseとなっているタスクのみを返すように処理を追加します。

``````javascript:store/index.js
getters: {
  tasks(state) {
    return state.tasks.filter(task => {
      return task.isDone == false
    })
  }
},
```

#未完了のタスクのみを表示
最後に、タスクの一覧をゲッターから呼び出すように変更します。

```javascript:list.vue
<ul class="tasks__list">
  <li class="tasks__item" v-for="task in $store.getters.tasks" :key="task.key">
    // 変更
    <input
      type="checkbox"
      :id="task.key"
      :value="task.id"
      v-model="doneTaskIds"
    />
    {{ task.name }}
  </li>
</ul>
ここまでできれば、以下のように動作します。

```

![aoiAoa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/d4bfba3f-6b07-2d3b-b517-4e3fc09c87d4.gif)


以上でタスク管理アプリの作成は完了になります。