プログラミング
AngularJS
Vue.js
フロントエンド
React

Vue.js初学者でも[確実に]ToDoアプリを動かしてVueコンポーネント間データ渡しを学ぶ


はじめに


前回のおさらいと今回すること

前回の記事ではVue.jsのHelloWorldを動かして基本事項を学びました。またコンポーネントという考えに触れ、実際に改良しました。

 今回は、簡単なToDoアプリケーションを通して、複数のコンポーネントに分割してwebアプリの機能を作ることを初学者の状態から学びます。またその過程で求められるコンポーネント間のデータ渡しについて理解し、実装できるようになります。前回の状態の続きから実装を始めるので、前回の記事を読んでいたら[確実に]動かせるようになります。

コードは更新のたびに、Githubにあげています。todoapp

Vuetify適応版なのでご注意を


一連の連載で[確実に]理解すること一覧


  • Vue.jsを使ってHelloWorldを出力し、プロジェクト内のファイルの役割や仕組みを理解する 前回の記事


  • コンポーネントとは何かを理解し、HelloWorldを改良してページのQRコードを表示してみる。前回の記事


  • コンポーネント間の値の受け渡しの基礎を導入し、Vuexの意義を理解した上で導入する[本記事]


  • WebAPIを導入してフロントエンドとバックエンドでデータをやり取りし、実践的なサイトを作り上げる



ToDo管理アプリを実装してみよう


アプリの機能と構成について

ToDo管理アプリについて考えてみましょう。必要な機能は


  • ToDoタスクリストとして表示すること (=> タスクビュワーコンポーネント)


  • ToDoタスクの終了が管理できること (=> タスクリビュワーコンポーネント)


  • ToDoタスクの追加ができること (=> インプットタスクコンポーネント)


としましょう。今回はこの機能を実現するために上記のTaskViewコンポーネントとInputTaskコンポーネントの二つを実装します。前回同様にこれらのコンポーネントを貼り付けるためのキャンバスの役割を担うApp.vueも利用します。

コンポーネントが何の機能を持つのかは上述しましたが、これらのコンポーネント間でのデータのやり取りはどうなっているでしょうか?ここで一点だけ、全タスクデータは、App.vueが保持するという制約を加えます。それを踏まえて、ここではデータの移動を以下のように考えました。


  • タスクビュワーはタスクリストを受け取り、表示する


  • タスクビュワーは各タスクを終了状態にすることができるボタンを持ち、それを押すとタスクリスト中の該当タスクが終了状態になる


  • インプットタスクは入力フォームを持ち、入力されたタスクがタスクリストに追加される


コンポーネントの概念図は以下の関係になります。概念図のトップのApp.vueは二つのコンポーネントを内部に持ち、配置等を制御するためのコンポーネントであり、親コンポーネントと呼びます。

コンポーネント図.png

一方で、下の二つのコンポーネントは親から呼ばれたコンポーネントで子コンポーネントと呼びます。


TaskViewコンポーネントを実装してみよう。

子コンポーネントは親から値をもらって、表示しなければいけません。その橋渡しをする変数がPropsです。親は子供のProps変数に値を渡します。子はそれを受け取ります。

 子コンポーネントの役割はもう一つあります。それはタスクの終了制御です。子供はタスクが終わった場合、親コンポーネントになんらかのアクションで通知します。親コンポーネントはボタンが押されたことを検知します。この橋渡しをする変数がEventです。PropsもEventも共通することとして親コンポーネントから呼び出すときに親と子で値の受け渡しがあることを宣言しておく必要があるということです。では実際に親から子コンポーネントを呼び出すコードを見てみましょう。

<taskView v-bind:tasks='tasks' v-on:child-event="TaskFinished"/>

この一文が親コンポーネント内のTemplate内に記述したコンポーネント呼び出しであることは前半の記事を読んだ方ならば分かるかと思います。前半のv-bind:tasks='tasks'について

v-bind:'Props名'='データ名'

と書くことで「親は、'データ名'を'Props名'という名前で子に渡しますよ」という宣言です。

後半のv-on:child-event="TaskFinished"については 

v-on:'Event名'='親が呼び出すメソッド名'

ということで、「子から'Event名'のイベントが発火されたら、親は'親が呼び出すメソッドを' 実行しますよ」という宣言です。では実際のコードで見てみましょう。なお、HelloWorldコンポーネントの部分は今回は削除しましたが、あっても動きます。


src/components/TaskView.vue

<template>

<div class='task'>
<table> <!-- テーブル形式にして表示することにします。
<tr>
<th>Task Name</th>
<th>Status</th>
</tr>
<tr v-for='(task, index) in tasks' :key='index'> <!-- tasksの配列の要素をループして表示。
<td>{{ task.name }}</td> <!-- buttonアクション(v-on:click)にclickというメソッドを発火
<td v-if=task.flag ><button v-on:click='click(task.name)'> Done </button></td>
<td v-else ><button v-on:click='click(task.name)'>Not Yet</button></td> <!--if文でflagを参照してボタンに表示名を変えている。
</tr>
</table>
</div>
</template>

<script>
export default {
name: 'TaskView',
props: {
tasks: Array // Array型のPropsを親から受け取ります 宣言
},
methods: { // template の部分のbuttonアクションで書いたものの実態はこれ。$emitで親に'child-event'というイベント名でイベントが起きたことを通知します。第二引数を指定すると、親に値を渡すことができます。
click: function (msg) {
this.$emit('child-event', msg)
}
}
}
</script>

<style scoped>
</style>



src/App.vue

<template>

<div id="app">
<img src="./assets/logo.png">
<taskView v-bind:tasks='tasks' v-on:child-event="TaskFinished"/>
<!-- コンポーネントの作成文。
<!-- 前半は子のtasksというProps名に親のtasksというデータを渡す。
<!-- 後半は子がchild-eventという名前でイベントを投げたら、親はTaskFinishedというメソッドを発火する宣言
</div>
</template>

<script>
import TaskView from './components/TaskView'

export default {
name: 'App',
components: {
TaskView
},
data () { // ここでtaskの配列を与える。今回は{name(タスク名): String, flag(終了フラグ): Boolean}
return {
tasks: [
{
name: 'No1',
flag: false
},
{
name: 'No2',
flag: true
},
{
name: 'No3',
flag: false
}
]
}
},
methods: { // 子からイベント名(event-child)を受け取って発火する関数。具体的に作り込んでいく部分。
TaskFinished: function (msg) {
this.tasks.forEach(value => {
if (value.name === msg) {
if (value.flag === true) {
value.flag = false
} else if (value.flag === false) {
value.flag = true
}
}
})
}
}
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>


表示は以下のようになっていて、ボタンを押して'Done'と'NotYet'が切り替われば成功です。

スクリーンショット 2019-04-15 19.15.41.png


InputTaskを実装してみよう

タスクの登録では、親のもつタスクリストの配列を操作することになります。つまりTaskViewで出てきたようなEvent通知処理でタスク追加を通知し、受け取った親側で配列にタスクを追加します。といっても今回タスクは{タスク名,状態}しか保持しない上に初期状態では状態もfalseであることを考えると、タスク名のみを通知すればよいことがわかるでしょう。


src/components/InputTask.vue

<template>

<div class='InputTask'>
<form v-on:submit.prevent='addtask(taskname)'> <!-- formタグ内に書いていきます。v-on:submitとv-on:clickという2パターンのイベントを書いていますが、発火する関数はどちらもaddtaskです。
<input type='text' v-model='taskname' placeholder='input the task'/> <!-- コンポーネントがもつtasknameというdataにinputの入力値を即時反映させるためにv-modelというオプションにtasknameを指定。
<button v-on:click.prevent='addtask(taskname)'>Submit</button>
</form>
</div>
</template>

<script>
export default {
name: 'InputTask',
data () {
return {
taskname: ''
}
},
methods: { // v-onで定義したアクションから呼ばれる関数です。v-on:clickもv-on:submitもどちらもchild-eventという名前で親に通知します。親からしたらエンターキーで追加されたか、submitボタン押で追加されたかはどうでもよく、イベント発火のみを判断して処理したいからです。
addtask: function (msg) {
this.$emit('child-event', msg)
this.taskname = '' // イベントを通知したら一応、初期化します。
}
}
}
</script>

<style scoped>
.input{
width: 130pt;
height:30pt;
}
.button {
display: block;
position: relative;
margin: 0 auto;
width: 70pt;
border: solid 1px silver;
border-radius: 0.5rem 0.5rem;
padding: 0.5rem 1.5rem;
margin-top: 1rem;
text-decoration: none;
}
</style>



App.vue

<template>

<div id="app">
<img src="./assets/logo.png">
<InputTask v-on:child-event="TaskAdded"/> <!-- コンポーネントを作成。先ほど同様イベント通知を受ける宣言をここに。 受けたときに呼ぶ関数は'TaskAdded'と指定。
<taskView v-bind:tasks='tasks' v-on:child-event="TaskFinished"/>
</div>
</template>

<script>
import TaskView from './components/TaskView'
import InputTask from './components/InputTask'

export default {
name: 'App',
components: {
TaskView,
InputTask
},
data () {
return {
tasks: [
{
name: 'No1',
flag: false
},
{
name: 'No2',
flag: true
},
{
name: 'No3',
flag: false
}
]
}
},
methods: {
TaskFinished: function (msg) {
this.tasks.forEach(value => {
if (value.name === msg) {
if (value.flag === true) {
value.flag = false
} else if (value.flag === false) {
value.flag = true
}
}
})
},
TaskAdded: function (msg) {
// InputTaskのイベントを受けて発火する関数。
// {受け取ったタスクの名前と初期状態false}でデータ配列の末尾に追加。デバッグ用にコンソール出力も
console.log(msg)
this.tasks.push({
name: msg,
flag: false
})
}
}
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>


以下のような画面になり、エンターでもSubmitボタンを押すことでもタスクが追加できたら成功です。

スクリーンショット 2019-04-15 19.43.29.png


終わりに

今回はコンポーネント間でのデータのやり取りの必要性を理解し、実際に親子間でのデータやりとりをToDoアプリを作成することで実践しました。では、例えばコンポーネントが20~30個のアプリになったり親子関係がさらに深くなったり、、、と複雑化していった場合に、全ての変数を今回のようなやり方でやり取りするのが適切でしょうか?

 各コンポーネントから参照可能なデータストアがあれば楽なのにと感じることも出てくるかもしれません。それこそがQiitaでもたくさんの記事が書かれているVuex(ReactユーザならばRedux)の生まれた経緯です。次回はこのVuexを今回作成したToDoアプリに導入してみましょう。


一連の連載


  1. 初学者でもVue/Vuexを[確実に]動かせる記事~はじめてのHelloWorld~


  2. Vue.js初学者でも[確実に]ToDoアプリを動かしてVueコンポーネント間データ渡しを学ぶ(本記事)