記事の概要
Vue.jsの勉強で作ったTodoリストっぽいものについて備忘録がてらつらつらと書いていきます。
ソースコードはGitHubに置いてあります。
以下の動画が大まかな動作イメージです。
前置き
Vue.jsに入門したはいいもののコンポーネントの理解がいまいちふわふわしていたので、とりあえず何か作ってみようと思って休日を使って頑張ってみました。
備忘録として詰まった部分を中心に機能の実装の概要なんかを書いていきますので、間違い等ございましたらやさしく教えて頂けると嬉しいです。(╹◡╹)
前置き(背景)
とりあえずTodoリストをコンポーネント管理の練習で作りたいなーとは思っていたのですが、Todoリストは他の言語でも散々やってるし、ちょっとは凝ったものにしたいなー的な欲望も渦巻いていました。
最近は季節の変わり目でプログラミング関連のことをやるかきくうし活動に勤しむぐらいしかできていなかったので、きくうし活動に絡めた何かを作りたいなと思い立ち、上記のものが出来上がりました。
少々前置きが長くなりましたが、以下では実装の過程をざっくりと書いていきます。
機能概要
まずはアプリで使うイラストとかをごりごり描きながら「こんな画面にしたいな」的な感じのものを少しずつ練っていきます。
頭の中のイメージをこねくり回してできあがった画面のイメージは以下の図のようになります。
ここで、機能概要としてアプリの機能をざっとリストアップします。
- 初期表示としてTodoリストがいくつか表示される
- Addボタンでフォームの入力値からタスクを新規登録
- 各タスクはクリックで完了とする
- Delボタンがクリックされるとモードが切り替わり、クリック時には完了ではなく「削除」とする
- メニュー部分にはTodoリストの状態が記述される
- 状態には「タスク」、「完了数」、「残数」が存在する
- 残数が0となった状態を完了とし、ジータさんが喜ぶ
以下ではそれぞれの機能について、詰まった部分を中心に書いていきます。
モックアップの作成
最初のステップとして、HTMLとSassを利用して画面を作り込んでいきます。
例としてタイトルコンポーネントという超シンプルなコンポーネントを見てみます。
<template>
<div class="title">
<h2 class="title--text">● やることリスト</h2>
</div>
</template>
<script>
</script>
<style lang="scss">
.title{
&--text {
width: 40%;
text-align: center;
font-size: 32px;
margin: 20px auto 5px auto;
border-bottom: 2px solid #e3e3e3;
}
}
</style>
最近のVue CLIさんは初期設定でSassを利用します、と設定するとstyleタグのlang属性に「scss」を指定するだけでSassが使えるようにしてくれます。めっちゃ便利なので画面づくりが更に捗りそうですね。
外部ファイル読み込めないんですけど問題
mixinやベースの設定なんかを使いたいなと思ったときもimport文を書いてあげれば解決できるのですが、よく使うものを毎回importするのは面倒。
一括importも設定をあれこれ弄ればいけるっぽいなと思って色々試してみたのですが、バージョンによって設定ファイルの記述がまちまちで中々上手くいかず。
最終的にVue CLI3先生に乗り換えることでサクッと解決しちゃいました。参考
Sassのimportで大分ハマってしまいましたが、とりあえずごりごりとモックアップを書いて、以下のような画面を作りました。
最初はコンポーネント単位でHTMLやらスタイルやらを書いていくのは面倒そうだな〜と思っていましたが、実際やってみるともの凄くスムーズに作れちゃいました。
Sassの勉強に使ってもいい感じですし、個人の開発ではコンポーネントをどんどん取り入れていきたいと思います。
さて、以下では実際にVue.jsを利用して機能部分を作り込んでいきます。
新規登録機能
ここではタスクを追加する機能について見ていきます。
<template>
<!-- 入力エリア -->
<li class="menu__item">
<textarea v-model="newRoutine" class="menu__item--input"></textarea>
</li>
<ul class="header__menu-button-list">
<!-- アクションボタン -->
<li class="menu__item">
<button v-on:click="addNew" class="button--add">日課を増やす</button>
</li>
</ul>
</template>
<script>
export default {
data: function() {
return {
newRoutine: ''
}
},
methods: {
/**
* 要素を追加 親要素へemit
*/
addNew: function() {
this.$emit('todo-added', this.newRoutine);
this.newRoutine = '';
}
}
}
</script>
やっていることとしてはシンプルで、ボタンがクリックされると親要素の「todo-added」ディレクティブが発火し、紐づけられた処理で新規登録を行う、という流れとなっています。
ここではだいぶ頭を悩まされた命名規則について書いていきます。
ケバブorキャメル
Vue.jsに入門したてだと随所でやれケバブがどうとかキャメルがどうとかが出てくるので、どちらをいつ使うべきか、ということがこんがらがってしまいます。
迷ったときは公式ドキュメントをば、ということで公式さんを探しているとそれっぽい記述が書かれていました。内容をざっくり要約すると以下のようになります。
これだけでは、Propsなんかもケバブケースじゃないとまずい感じがしますが、公式さんいわく「string templates」を利用している場合はこの限りではないようです。
で、例のごとく「string templates」ってなんぞやとなりましたが、対となる言葉が「DOM templates」となっているように、JavaScriptの文字列形式で定義されたテンプレートを表しているようです。参考3
「.vueファイル」ではテンプレートをHTMLっぽく書けてはいますが、vue-loaderさんによってVueコンポーネントのtemplateオプションへ変換されているので、内部的には文字列として扱われます。
やや長くなりましたが、上記の性質からイベント名は原則ケバブケース、ということだけ意識しておけば良いのかなと思います。
こっちの方が良い、などございましたら教えて頂けるとうれしいです。
タスクをクリックで完了させる
やることは変わりましたが、流れは基本同じでクリック対象がボタンからタスクになっただけのこと...なのですが、ここでまたハマりました。
<template>
<div class="todo">
<ul class="todo-list">
<!-- event -[要素クリック(要素id)]
props -[モード]
-->
<item
v-on:click.native="todoClicked(index)"
v-for="(todo, index) in todoList"
v-bind:key="todo.id"
v-bind:delMode="delMode"
>
<span v-html="todo.value"></span>
</item>
</ul>
</div>
</template>
<script>
import Todo from "./Todo.vue";
export default {
props: ['todoList', 'delMode'],
components: {
Item : Todo
},
methods: {
/**
* 各要素クリック時に呼ばれる処理
* @param {Number} index -クリック要素を判別するインデックス
*/
todoClicked: function(index) {
this.$emit('todo-clicked', index);
}
}
}
</script>
ソースコードもあまり違いはないのですが、注目したいのは、クリックイベントに「.native」という修飾子が付与されていることです。
テンプレートの部分を見て頂くと分かるかと思いますが、今回はクリック対象はHTMLの標準の要素ではなく、Vueコンポーネントのitemタグとなっています。
このようにカスタムタグを利用した場合、v-onディレクティブに指定されたイベントはカスタムイベントと解釈されてしまうので、今回のクリックイベントのようなDOM標準のイベントハンドリングを行うには「.native」修飾子が必要となります。(参考)
以下のように親要素のAppコンポーネントでは各コンポーネントで発火したイベントをv-onディレクティブのカスタムイベントで拾っています。つまり、DOM標準の「クリックされた」とか「エンターキーが押された」といったイベントをカスタムタグで拾いたいならネイティブ修飾子を忘れないでね、という感じになるようです。
<template>
<div class="container">
<app-title></app-title>
<!-- event -[要素追加, モード切り替え]
props -[モード, Todoリストの状態, リストの長さ]
-->
<app-header
v-on:todo-added="createNewRoutine"
v-on:mode-changed="toggleMode"
v-bind:delMode="delMode"
v-bind:routineStatus="routineStatus"
v-bind:routineCount="todoList.length"
>
</app-header>
<!-- event -[要素クリック]
props -[Todoリスト, モード]
-->
<app-todo-list
v-on:todo-clicked="removeRoutine"
v-bind:todoList="todoList"
v-bind:delMode="delMode"
>
</app-todo-list>
</div>
</template>
ロジック周り
さて、ここまででデータの受け渡し、イベントの発火について見てきました。
残りのロジック部分はVue.jsというよりJavaScriptの話がメインとなってきますが、全体についてさらっと見ていきたいと思います。
<script>
export default {
data: function() {
return {
// 連番 完了数 残数
routineStatus: {serial: 8, done: 0, rest: 8},
//今回は初期値として手入力でセット
//SpringBootと組み合わせるときにDBから取ったりできるようにしたい
todoList: [
{id: 0, value: 'グリームニルをしばく'},
(以下省略)
],
//true...削除モード false...削除モード
delMode: false
};
},
//コンポーネント定義
components: {
appTitle: Title,
appHeader: Header,
appTodoList: TodoList
},
methods: {
/**
* 入力値で新規登録
* @param {String} newRoutine -フォームの入力値 改行コードは要変換
*/
createNewRoutine: function(newRoutine) {
//改行コードはスペースとなってしまうのでHTMLのbrタグへ変換
this.todoList.unshift(
{id: this.routineStatus.index ,
value: newRoutine.replace(/\r?\n/g, '<br>')
});
this.routineStatus.serial++;
this.routineStatus.rest++;
},
/**
* リストから要素を削除する
* 完了モード→doneを更新して削除 削除モード→doneを更新せずに削除
* @param {Number} index -削除対象のインデックス
*/
removeRoutine: function(index) {
this.todoList.splice(index, 1);
if (this.delMode) {
this.routineStatus.rest--;
}
if (!this.delMode) {
this.routineStatus.rest--;
this.routineStatus.done++;
}
},
/**
* ボタン押下によって各要素クリック時の挙動を切り替える
* delMode→削除 !delMode→完了
*/
toggleMode: function() {
this.delMode = !this.delMode;
}
}
}
</script>
<style lang="scss">
</style>
新規登録処理
ここでの留意事項は二点あります。
一つは、有名な話ですが、Vue.jsではループするリストにはユニークなキーを割り当てる必要があります。公式さんの説明では、リスト内で順番が変更されると、DOM要素に対して実際に並び順を替えるのではなく「その場で」描画順を割り当てる処理を行うため、Vue先生へ各ノードの識別子を提供することを推奨しているようです。(参考)
そしてもう一つはVueとはあまり関係の無い話ですが、フォームの入力値はテキストエリア形式で受け取っているため、何も処理せずに描画すると、改行がスペースへ変換されてしまいます。
このため、正規表現を利用して「br」タグへ変換し、表示する際にも「v-html」ディレクティブを使用する必要があります。
しかし、「v-html」ディレクティブはVue先生のXSS対策を無効化してしまうので、実際に公開するアプリで利用する際にはXSS対策を自力で講じられるよう注意が必要です。
削除関連処理
この辺の処理はボタンで切り替わる状態に応じてタスクを削除するか完了するかをやっているだけなので、特に問題は無いかと思います。
コンポーネントの単位問題
さて、これでアプリのロジック、そしてVue.jsのコンポーネントを利用したイベントの発火・データの受け渡しをざっと眺めてきましたが、もう一つ大きな話が残っています。
それは、どこまでをコンポーネントの単位とし、ロジックをどう切り分けるか、という話です。
ロジックもコンポーネントにぶち込んで一つのコンポーネントで描画も機能も全部解決しちゃえばいいんじゃねーの
数週間前の私はこんな風に考えていました。
実際わざわざPropsで渡してemitでイベント拾って...って面倒でしょとか思っていたのですが、実際にプログラムを組んでみて両者の大事さを実感しました。
コンポーネントは再利用可能なもの
大事なのは上記の原則です。
例えば、今回はロジック部分をApp.vueへ集約させた為、Todoリストのコンポーネントは、以下の「機能」を持ったコンポーネントとなりました。
- JavaScriptで受け取ったオブジェクトをループで描画
- 各要素をクリックするとイベントが発火
- 状態としてモードプロパティを持つ
これはつまり、クリックされることで発火するイベントはTodoコンポーネントは関与せず、
親要素に一任されることとなります。
よって、Todoコンポーネントには変更を加えることなく、ロジック部分を変更するだけですてきな機能を拡張することができてしまいます。
これを逆転させ、Todoコンポーネントへロジックも描画も集約させるとどうなるでしょうか。
クリックされたときの挙動がTodoコンポーネントへ定義されてしまっているため、機能を拡張するにはコンポーネントを直接書き換える必要があります。
これでは「再利用」できているとは言えませんね。
こういった面から、プロパティをemit、Propsで親要素とやり取りし、ロジックは親要素へ一任する、という方針にするのがよいかと思われます。これは公式さんで明文化されている、という感じでもなさそうなので何かいい感じのドキュメントとかご存知でしたらぜひぜひ。
余談(今後やりたいこと)
ひとまず今回作った部分については一通り記述できましたので、最後にだらだらと今後の展望なんかも書いておきたいと思います。
トランジションで描画をいい感じにしたい
トランジション関連の処理もVue.jsの強力な機能の一つらしいとは聞いているのですが、まだそこまで学習できていないので、学習が進んだら実践がてら演出面を頑張りたいと思います。
ダイアログとか表示させたい
実際のアプリケーションだとダイアログを表示させる処理は鉄板なのでVue.jsでも上手い感じにできるようにしたいなと。
SpringBootと連携させたい
SpringBoot先生で青い鳥さん風のアプリを作ったので、それと上手い感じに連携させられる地力を手に入れるためCRUDとかダイアログ表示とかと組み合わせる練習がてら今回のやつを拡張したいです。頑張りたい。
(もうちょっとイラストのバリエーション増やしたい...)
まとめ
Vue.jsのコンポーネントは何となく難しそうということで敬遠気味でしたが、今回実際に使ってみてもっと触りたくなってきました。
コンポーネントの更なる機能はもちろんのこと、webpackもまだまだ不慣れなのでちょっとずつ勉強して更に色々作ってみたいです。
とりあえずトランジションとか勉強してミニゲームとか作ってみたい。頑張りたい。

