簡単なTodo アプリを作りましょう
- オリジナルはdotinstall さんの vue.js のtodoアプリをアレンジしたものですので詳しくはそちらをみてください。
今回は、Vue.js を使って、Todoに必要な処理の練習をします。
Todo タスクを追加してみましょう。
lang
指定でts
,stylus
を指定していますが、typescript
, stylus css
用ですが、実際つかっているのは、javascript
, css
です。 stylus
にすると、混在できますのでstylus
を使っています。 typescript
の混在できますので、型つけたければつければいいかと思います。
Vue.js は、input周りは、簡単に処理できるのがいいですね。
src/MyApp.vue
<template lang="pug">
div.container
Todo
</template>
<script lang="ts">
import Vue from 'vue';
import Hello from './Hello.vue';
import Todo from './Todo.vue';
export default Vue.extend({
components: {
Todo,
}
})
</script>
<style lang="stylus">
body {
font-size: 16px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.container {
width: 300px;
margin: auto;
}
h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}
li {
line-height: 1.5;
}
input[type="text"] {
padding: 2px;
}
.done
color gray
text-decoration line-through
</style>
src/Todo.vue
<template lang="pug">
div
ul
li(v-for="todo in todos")
| {{ todo }}
form( @submit.prevent="addItem" )
input(type="text" v-model="newItem")
input(type="submit" value="Add")
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return({
newItem: '',
todos: [
"task 0",
"task 1",
"task 2",
]
})
},
methods: {
addItem() {
this.todos.push(this.newItem);
this.newItem = '';
}
}
})
</script>
- コンテナ(uname linux でしたね)
uname
cd src
npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug
delete / checkbox
削除もチェックボックスも実装が楽ですね。
id
追加しましたが、使っていません(笑) index指定で削除していますが、まずければ id
に変えようと思いましたが、問題なさそうなので、使っていません。
src/Todo.vue
<template lang="pug">
div
ul( v-if="todos.length" )
li(v-for="todo, index in todos")
input(type="checkbox" v-model="todo.isDone")
span( :class="{done: todo.isDone }" ) {{ todo.title }} : {{ todo.isDone }}
button( @click="deleteItem(index)" ) Delete
ul( v-else )
| Nothing todo
form( @submit.prevent="addItem" )
input(type="text" v-model="newItem")
input(type="submit" value="Add")
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return({
newItem: '',
todos: [
{ id: 0, title: 'Task 0', isDone: false },
{ id: 1, title: 'Task 1', isDone: false },
{ id: 2, title: 'Task 2', isDone: true },
]
})
},
methods: {
addItem() {
const newItem = {
id: new Date().getTime().toString(36),
title: this.newItem,
isDone: false,
}
this.todos.push(newItem);
this.newItem = '';
console.log(newItem);
},
deleteItem(index) {
this.todos.splice(index, 1)
}
}
})
</script>
複数削除
完了していないタスクは、computed で常に集計できるので、それを新しい配列にすれば、結果的に完了タスクを削除したことになりますね。
Todo.vue
<template lang="pug">
div
h1
| todo
span ( {{ remaining.length }} : {{ todos.length }} )
button( @click="doneItemsPurge" ) Purge
ul( v-if="todos.length" )
li(v-for="todo, index in todos")
input(type="checkbox" v-model="todo.isDone")
span( :class="{done: todo.isDone }" ) {{ todo.title }} : {{ todo.isDone }}
button( @click="deleteItem(index)" ) Delete
ul( v-else )
| Nothing todo
form( @submit.prevent="addItem" )
input(type="text" v-model="newItem")
input(type="submit" value="Add")
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return({
newItem: '',
todos: [
{ id: 0, title: 'Task 0', isDone: false },
{ id: 1, title: 'Task 1', isDone: false },
{ id: 2, title: 'Task 2', isDone: true },
]
})
},
methods: {
doneItemsPurge() {
this.todos = this.remaining
},
addItem() {
const newItem = {
id: new Date().getTime().toString(36),
title: this.newItem,
isDone: false,
}
this.todos.push(newItem);
this.newItem = '';
console.log(newItem);
},
deleteItem(index) {
this.todos.splice(index, 1)
}
},
computed: {
remaining() {
return this.todos.filter( todo => {
return !todo.isDone
})
}
}
})
</script>
データの永続化
初心者むけでは大体 localStorage
を使う例が多いので、 ここでは、Firebase
の DB FireStore
を使ってみようかと思います。
FireStore db
は、googole
のアカウントがあれば、利用できますので、firebase console
で検索してみてください。 ここでは、プロジェクトの作成やfireStore
の有効にする方法は説明しませんので、利用出来る前提で始めていきます。
fireStore の 読み書きルール はすべて true の テストモードで行っています。
- ファイル構成です。
├── Dockerfile
├── docker-compose.yml
├── firebase.js
└── src
├── Hello.vue
├── MyApp.vue
├── Todo.vue
├── dist
├── index.js
├── index.pug
├── package.json
└── yarn.lock
ルードディレクトリに firebase.js
を作っていますので、そこにfirebase
の config
をコピペしてください。
firebase.js
export const config = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: ""
};
MyApp.vue はほとんど変わっていませんが一応記載します。
src/MyApp.vue
<template lang="pug">
div.container
//- Hello
Todo
</template>
<script lang="ts">
import Vue from 'vue';
import Hello from './Hello.vue';
import Todo from './Todo.vue';
export default Vue.extend({
components: {
Todo,
Hello,
}
})
</script>
<style lang="stylus">
body {
font-size: 16px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.container {
width: 700px;
margin: auto;
}
h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}
li {
line-height: 1.5;
}
input[type="text"] {
padding: 2px;
}
.done
color gray
text-decoration line-through
</style>
Todo.vue は、fireStore
対応で大幅に変わっています。
リファクタリングもしていないので、見苦しい感じが残っていますが、実装の苦労の跡が見えていいのではないでしょうか(笑)
なるべく、fireStoreの機能を使って実装しようとしているので、データのやり取りは、fireStoreを毎回介して行っています。
マウントとアンマウントの時にfireStore
にデータを渡したほうが動作が軽く楽かもしれませんが
fireStore
の練習には、こちらのほうがいいかと思います.
ラグが気になりますが、追加、削除、完了、未完了、一括削除ができているのOK でしょう。(笑)
async / await に慣れていない人へ
DB を使うと処理が非同期になるため、async / await を使っていますが、asyce/await に慣れていない人に一言いうと、処理に時間がかかりそうだと思うものの前に,すべてawait
を 付けていけばいいです。
今回で言えば、データの読み書きで firestore
に対してのと命令を書いた場合にあたります。
付けていないと、ほしいデータが入る前に次の処理が走ってしまって、空処理になるのを防いでいます。
ですから、時間がかかると思ったら、await
を処理の前につけてください。
await
を使うとき、親要素に async
という呪文が必要なので、これを関数式に付けます。
await
をつけたけれど、親要素がない場合は、そのあたりの処理一体を、関数にまとめてしまえばいいです。
そうすれば、非同期処理はできているはずです。
src/Todo.vue
<template lang="pug">
div
h1
| todo
span ( {{ remaining.length }} : {{ todos.length }} )
button( @click="doneItemsPurge" ) Purge
form( @submit.prevent="addItem" )
input(type="text" v-model="newItem")
input(type="submit" value="Add")
ul( v-if="todos.length" )
li(v-for="todo, index in todos")
//- input(type="checkbox" v-model="todo.isDone")
input(type="checkbox" @click="checkboxChange(index)" :checked="todo.isDone")
span( :class="{done: todo.isDone }" ) {{ todo.title }} : {{ todo.isDone }} : {{ todo.date }}
button( @click="deleteItem(index)" ) Delete
ul( v-else )
| Nothing todo
</template>
<script lang="ts">
import firebase from 'firebase/app';
import 'firebase/firestore';
import { config } from '../firebase';
firebase.initializeApp(config);
const db = firebase.firestore()
const collection = db.collection('todos')
import Vue from 'vue'
export default Vue.extend({
data() {
return({
newItem: '',
todos: [],
itemsId: [],
storeItem: {},
state: [],
})
},
watch: {
state: { // 更新チェック されたら、実行
handler: async function(){
const queryItems = await collection.orderBy("created_at", "desc").get()
const qureyItemsData = queryItems.docs.map( item => item.data())
const qureyItemsId = queryItems.docs.map( item => item.id )
this.todos = qureyItemsData;
this.itemsId = qureyItemsId;
}, deep: true
},
todos: {
handler: async function(){
}, deep: true
}
},
methods: {
async checkboxChange(index) {
let isDoneState = Boolean;
const item = await collection.doc(this.itemsId[index])
const isDone = await item.get()
.then(function(doc) {
isDoneState = doc.data().isDone
})
.catch( err => {
console.log(err);
})
const changeItem = await item.update({
isDone: !isDoneState,
})
this.state = ["checkboxUpdate"]
},
async deleteItem(index) {
const item = await collection.doc(this.itemsId[index]).delete()
this.state = ["delete"]
},
addItem() {
this.storeItem = {
id: new Date().getTime(),
date: new Date().toLocaleString("ja"),
title: this.newItem,
isDone: false,
created_at: firebase.firestore.FieldValue.serverTimestamp(),
}
const item = collection.add(this.storeItem);
this.newItem = '';
this.state = ["addItem"] // 状態更新通知用
},
async doneItemsPurge() {
// this.todos = this.remaining
const itemsQuery = await collection.where("isDone", "==" , true)
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
const item = collection.doc(doc.id).delete();
console.log(item);
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
})
this.state = ["purge"] // 状態更新通知用
},
},
computed: {
remaining() {
return this.todos.filter( todo => {
return !todo.isDone
})
}
},
async mounted() {
const query = await collection.get()
const items = query.docs.map( item => item.data())
const itemsId = query.docs.map( item => item.id)
this.todos = items;
this.itemsId = itemsId;
this.state = items;
}
})
</script>
これで、Vue.js での練習は、終了です。
お疲れ様でした。
次回は, この環境で、React を使ってみたいと思います