#コンポーネントの作成
まずは見た目から整えていきましょう。
作るコンポーネントは、以下の四つです。
- Header
- Main
- Sidebar
- List
コンポーネントの配置は以下のようになっています。
それぞれコードを記述していきます。
コンポーネントのディレクトリは、sample-app/src/componentsに入れます。
まずは、見た目だけを整えたいので、そのままコピペしてもらっても大丈夫です。
##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
<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
<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
<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
<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 コンポーネントをコンポーネントとして登録しましょう。
<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>
編集を終えて、ページを開いてみると、以下のようになっているかと思います。
続いて、ページ遷移先のコンポーネントを編集していきます。
sample-app/src/views/にあるコンポーネントは、
Vue route から呼び出せるコンポーネントが入っています。
この中に新しく、Todo.vueを作成します。
Todo.vueの中に、先ほど作成した、List コンポーネントを出力させます。
#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を編集していきます。
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に変更しましょう。
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;
#タスク追加機能
続いて、タスク作成の機能を作成していきます。
タスク作成に関しては、Listコンポーネントを編集していきます。
まずは、新しく追加するタスクの名前を格納するために、
newTaskNameという変数を用意しましょう。
(省略)
<script>
export default {
data() {
return {
newTaskName: "",
};
},
};
</script>
(省略)
次に、フォームのinputと紐付けるために、v-modelで、newTaskNameを指定します。
(省略)
<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 コンポーネントを選択し、データの中身を確認してみてください。正常にできててたら、以下のようになっているかと思います。
続いて、TODO リストのデータを配列に保存する処理を追加しましょう。 data に新しく、tasksという空の配列を用意します。
data() {
return {
tasks: [],
newTaskName: ""
};
},
次に、この tasks に、newTaskName を保存する処理を追加します。 methodsに、addTaskというメソッドを追加し、以下のような処理を記述します。
methods: {
addTask() {
this.tasks.push({
name: this.newTaskName
});
this.newTaskName = "";
}
}
用意した、変数には、thisでアクセスすることができます。
this.tasksで、用意した配列にアクセスし、push メソッドで配列を追加しています。
最後に、「this.newTaskName = "";」で、newTaskNameに空の文字を入れて、input の中身を空にしています。
次に、フォームが送信されたタイミングで、この、addTask、メソッドを実行するようにします。
<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を用いることで解決します。
<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は、@で省略できるので、以下のように記述してもよいでしょう。
<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 配列に、値が追加されているのがわかるかと思います。
#追加したタスクの表示
次に、追加したタスクを表示できるようにしましょう。
配列を表示するには、v-forを使用し、リストの部分を以下のように書き換えます。
<ul class="tasks__list">
<li class="tasks__item" v-for="task in tasks" :key="task.key">
{{ task.name }}
</li>
</ul>
うまくいっていれば以下のように動作します。
#Vuex を用いたデータ管理
次に、コンポーネントのデータを使っていたものを、Vuex で管理してみましょう。
今回のように小規模なアプリケーションでは、Vuex を使うのは助長かもしれませんが、学習のため、Vuex を用いていきます。
Store は、sample-app/src/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)
以上でタスク管理アプリの作成は完了になります。