1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-07

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

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

  • Header
  • Main
  • Sidebar
  • List

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

As2jH1.png

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

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

##Header.vue

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

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

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

list.vue
<template>
  <div class="tasks">
    <ul class="tasks__list">
      <li class="tasks__item">タスク1</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

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 コンポーネントをコンポーネントとして登録しましょう。

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>

追加したのは以下の部分です。

<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 で、コンポーネントを読み込んでから、

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

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

export default {
  components: {
    Sidebar,
    Header,
    Main,
  },
};

次に、読み込んだコンポーネントを使い、レイアウトを整えます。

<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

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

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

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

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

#Todo.vue

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を編集していきます。

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に変更しましょう。

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

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

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

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

list.vue
(省略)

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

(省略)

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

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
続いて、TODO リストのデータを配列に保存する処理を追加しましょう。 data に新しく、tasksという空の配列を用意します。

list.vue
  data() {
    return {
      tasks: [],
      newTaskName: ""
    };
  },

次に、この tasks に、newTaskName を保存する処理を追加します。 methodsに、addTaskというメソッドを追加し、以下のような処理を記述します。

list.vue
methods: {
    addTask() {
      this.tasks.push({
        name: this.newTaskName
      });
      this.newTaskName = "";
    }
  }

用意した、変数には、thisでアクセスすることができます。

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

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

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

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を用いることで解決します。

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は、@で省略できるので、以下のように記述してもよいでしょう。

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

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

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

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

うまくいっていれば以下のように動作します。

hGif53.gif

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

今回のように小規模なアプリケーションでは、Vuex を使うのは助長かもしれませんが、学習のため、Vuex を用いていきます。
A3s0Sa.jpeg

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

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)


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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?