久しぶりにWEB開発しようとしたら、2-3年前にやってた AngularJS + Grunt + Bower + Yeoman な構成はいまや古い部類の構成になってるようで、、いま時点(2018/12)で、似た感じかつシンプルなフレームワークを探してて Vue.js にたどり着きました。
ってことで、ちょっとさわってみたときの作業メモ。
WEB系のフレームワークで必ずでてくる、TODOリストの構築を行ってみましょう。
ちなみに2019/01/28時点:
- Vue.js の主要な機能をざっくりとつかってみたときのメモ 本記事
- Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編) 次の記事
- Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編) さらにその次
これだけQiita化しています。
前提環境
今回作業している環境です。
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.14.2
BuildVersion: 18C54
$ node --version
v10.15.0
$ npm --version
6.4.1
$
インストール
vue-cli というVue.jsのテンプレを生成してくれるコマンドラインツールをインストールします。
$ npm install -g vue-cli
$ source ~/.bash_profile
$ vue --version
2.9.6
よさそうですね。
やってみる
プロジェクト作成
$ vue init webpack todo-examples
↓とりあえずすべて Yes
? Project name todo-examples
? Project description A Vue.js project
? Author Masatomi KINO <kino@example.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recom
mended) npm
vue-cli · Generated "todo-examples".
# Installing project dependencies ...
# ========================
...
# Project initialization finished!
# ========================
To get started:
cd todo-examples
npm run dev
Documentation can be found at https://vuejs-templates.github.io/webpack
完了しました。
Hello World
最後に cd todo-examples
,npm run dev
とでてましたので、その通りに。。
$ cd todo-examples
$ npm run dev
...
DONE Compiled successfully in 7650ms 15:42:24
I Your application is running here: http://localhost:8080
さて、メッセージ通りにブラウザで http://localhost:8080 をひらいてみます。
よさそうですね。。
TIPS
v-model でデータバインディング
まずはデータバインディングから。 HelloWorld.vue を以下のようにします。
<template>
<div class="hello">
<p><input type='text' v-model='msg'></p> <!-- このへん -->
<p>{{msg}}</p>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Hello World.'
}
}
}
</script>
ブラウザをリロードしてみると(たぶん勝手にリロードされてます)テキストボックスが表示されると思いますが、 その v-modelが設定されたテキストボックスに値を入力すると、{{msg}}
部分も値が更新されると思います。
配列データを v-forで繰り返し,submit ボタンでメソッド呼び出し
つづいて配列をHTML上の v-forタグで走査したり、v-on:submit
もしくはその省略形の@submit
でイベントハンドラを登録したりしてみます。
イベントハンドラとして登録する、画面で使用するメソッドは、methods
に定義していきます。
また、Vue.jsはその画面上で使用する変数をdata()
で定義するようになっていて、todos配列には初期値をセットしてあります。
結果、HelloWorld.vue を以下の通りにしましょう。
<template>
<div class="hello">
<ul>
<li v-for='todo in todos' >{{todo}}</li> <!-- 繰り返し処理 -->
</ul>
<form @submit.prevent='addItem'> <!-- submitされたらaddItemメソッドを呼ぶ -->
<input type='text' v-model='newTask'>
<input type='submit' value='追加' >
</form>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () { // 画面で使用する変数を定義する場所
return {
newTask:'',
todos : [
{ name: "task 1", isDone: false },
{ name: "task 2", isDone: false },
{ name: "task 3", isDone: false },
{ name: "task 4", isDone: false }
]
}
},
methods:{ // 画面やscriptから呼び出すメソッド
addItem: function(){
this.todos.push({
name: this.newTask,
isDone: false
})
this.newTask=''
}
}
}
</script>
data()
で、newTask
とtodos
という変数を定義しました。またmethods
では、追加ボタンをクリックしたときに呼び出す addItem
メソッドを定義しました。
ちなみにそのメソッド内では、this.newTask
などと「this」
をつけることで、data()
で定義した変数にアクセスできます。
以上で、追加ボタンをクリックするとformのsubmitが行われて、addItemメソッドが呼び出された結果、タスクが追加されるようになりました。
v-bind:classでクラスの制御、computedで計算処理
さてつぎは、v-bindです。下記のように li をv-forしている箇所に
<ul>
<li v-for='(todo,index) in todos' >
<input type='checkbox' v-model='todo.isDone'>
<span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
<span @click='deleteTask(index)' class='xButton'>[x]</span>
</li>
</ul>
って<span v-bind:class='{done: todo.isDone}'>...</span>
と書くことで、todo.isDoneのときだけ(タスクが終わってるときだけ) class属性に doneがセットされるようになります。そしてcssで、そのクラスは完了線を引くようにしています。
チェックボックスは、v-model
をつかって todo.isDone
に連動させています。
また
<span @click='deleteTask(index)' class='xButton'>[x]</span>
として、クリックしたときのメソッド deleteTask
を追加しています。引数の「index」
は、v-for を <li v-for='(todo,index) in todos' >
と記述することで、index番号を使用可能にしています。
さいごにdata()
とmethods
のならびに、computed
部を追加し、そこにメソッドremainingTask
を定義しています。computed
に定義したメソッドは、画面上から{{remainingTask}}
とやって呼び出すことができます。
まとめると、全体はこんな感じ。
<template>
<div class="hello">
<h1>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
<ul>
<li v-for='(todo,index) in todos' >
<input type='checkbox' v-model='todo.isDone'>
<span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
<span @click='deleteTask(index)' class='xButton'>[x]</span>
</li>
</ul>
<form @submit.prevent='addTask'>
<input type='text' v-model='newTask'>
<input type='submit' value='追加' >
</form>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
newTask:'',
todos : [
{ name: "task 1", isDone: true },
{ name: "task 2", isDone: false },
{ name: "task 3", isDone: false },
{ name: "task 4", isDone: false }
]
}
},
methods:{
addTask: function(){
this.todos.push({
name: this.newTask,
isDone: false
})
this.newTask=''
},
deleteTask: function(index){
this.todos.splice(index,1)
}
},
computed:{
remainingTask: function(){
return this.todos.filter(function(todo){
return !todo.isDone
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}
.xButton{
cursor: pointer;
font-size: 12px;
color: red
}
li > span.done{
text-decoration: line-through;
color:#bbb
}
.info {
color: #bbb;
font-size: 12px;
}
</style>
ブラウザを見てみると、、、
チェックボックスと削除ボタンが追加され、チェックを入れるとv-model
でバインドしたtodo.isDone
がtrueとなり、完了線がつきます。
削除ボタンをクリックすると、deleteTask
が呼ばれて該当タスクが削除されます。
よさそうです。
ボタンで全削除
つぎに「完了タスク全削除」って機能を追加します。すぐうえでcomputed
に
remainingTask
メソッドを追加ましたが、内容は「いま未完了のタスクを返す」ようにしてあるので、todos
をそれに置き換えるだけですね。
<template>
<div class="hello">
<h1>
<button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
... 省略
</template>
<script>
export default {
... 省略
methods:{
... 省略
deleteEndTask:function(){
this.todos = this.remainingTask;
}
},
... 省略
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
...省略
li > span.done{
text-decoration: line-through;
color:#bbb
}
</style>
LocalStorageからの読み書き、watchによる変数の監視。
つづいて、リロードしても消えないように画面初期化時にlocalStorage
から読み込んでみたり、画面から追加や削除などtodos
に対しての操作を監視し、todos
に変更があったときにlocalStorage
へ保存したり、してみます。
変数の監視は、watch
をつかって実装します。watch
部に
watch:{
todos: {
handler: function() {
localStorage.setItem('todos',JSON.stringify(this.todos))
},
deep: true
}
}
と記述することで該当の変数の変更を検知することが出来ます。
したがって、todos
変数を監視し、変更を検知したらStorageに書き込むようにしました。
また、created
部は、起動時に呼び出される関数なので、そこで
created: function() {
this.todos = JSON.parse(localStorage.getItem("todos")) || [];
}
として、Storageから読み込むか、読めなければカラの配列をセットします。
変更した差分は以下の通りです。
<script>
export default {
name: 'HelloWorld',
data () {
return {
newTask:'',
- todos : [
- { name: "task 1", isDone: true },
- { name: "task 2", isDone: false },
- { name: "task 3", isDone: false },
- { name: "task 4", isDone: false }
- ]
+ todos : []
}
},
+ watch: {
+ todos: {
+ handler: function() {
+ localStorage.setItem('todos',JSON.stringify(this.todos))
+ },
+ deep: true
+ }
+ },
+ created: function() {
+ this.todos = JSON.parse(localStorage.getItem("todos")) || [];
+ },
methods:{
addTask: function(){
this.todos.push({
全体はこんな感じになりました
<template>
<div class="hello">
<h1>
<button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
<ul>
<li v-for='(todo,index) in todos' >
<input type='checkbox' v-model='todo.isDone'>
<span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
<span @click='deleteTask(index)' class='xButton'>[x]</span>
</li>
</ul>
<form @submit.prevent='addTask'>
<input type='text' v-model='newTask'>
<input type='submit' value='追加' >
</form>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
newTask:'',
todos : []
}
},
watch: {
todos: {
handler: function() {
localStorage.setItem('todos',JSON.stringify(this.todos))
},
deep: true
}
},
created: function() {
this.todos = JSON.parse(localStorage.getItem("todos")) || [];
},
methods:{
addTask: function(){
this.todos.push({
name: this.newTask,
isDone: false
})
this.newTask=''
},
deleteTask: function(index){
this.todos.splice(index,1)
},
deleteEndTask:function(){
this.todos = this.remainingTask;
}
},
computed:{
remainingTask: function(){
return this.todos.filter(function(todo){
return !todo.isDone
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}
h1 > button {
float: right;
}
.xButton{
cursor: pointer;
font-size: 12px;
color: red
}
li > span.done{
text-decoration: line-through;
color:#bbb
}
.info {
color: #bbb;
font-size: 12px;
}
</style>
ブラウザで開いたのち、追加・完了・削除などを行ってからリロードしてみたり、ブラウザを閉じて開いたりしてみて、タスクが正しく保存されていることを確認してみてください。
コンポーネントをつくる
いまのところtodoの一画面があるだけですが、たくさん画面を作ったら、共通のヘッダやフッタをつけたくなりますよね。というわけで、ヘッダのコンポーネント(Header.vue) を作成し、App.vue から参照するようにします。
<template>
<header>{{message}}</header>
</template>
<script>
export default {
name: 'Header',
data () {
return {
message:'共通ヘッダ'
}
},
computed:{
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
<template>
<div id="app">
<Header /> <!-- 追加 -->
<router-view/>
</div>
</template>
<script>
import Header from '@/components/Header' // コンポーネントをimportして
export default {
name: 'App',
components:{
Header // つかうことを宣言する
}
}
</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>
ブラウザを開いてみると、
よさそうですね。このヘッダはApp.vueに設置されているので、すべての画面に表示されるようになります。
いったんまとめ
いやあ、Vue.js よいですね。AngularJSやってた身からすると、作者がおなじだからか今のところほぼおなじといえる使い勝手です。逆にいうと数年前のAngularJSと違いがまだ体感できてない状態です、、orz。
いまのところドットインストールのVue.js入門とほぼおなじ仕組みになっちゃったので、ひきつづきFirebaseやVuexなどとの連携を整理してみようと思います。
ソースコード
上記はマスタブランチで、この時点でのソースはこちらです。
https://github.com/masatomix/todo-examples/tree/for_qiita_init_001
下記手順でビルドできます。
$ git clone --branch for_qiita_init_001 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install
$ npm run dev
これで、http://localhost:8080/ にブラウザでアクセスできるとおもいます。
おつかれさまでした。。