8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue.jsとFirebase/Firestoreを使った家族用ToDoリスト

8
Last updated at Posted at 2020-09-06

家族用ToDoリスト

Vue.jsの学習のために作成したので、その過程をまとめます。

学習用の成果物としてToDoリストは定番ですが、せっかくだから実生活で活用したいと思い、
家族、すなわち事前に登録した特定のユーザのみで閉鎖的に利用できるToDoリストを作りました。
(自分だけのToDo管理なら普通にKeepメモとか使いますしね)

まずは「1.ToDoリストの作成」でFirestoreを利用したToDoリストを作成し、
その後「2.認証機能の作成」で家族用に仕上げます。

※VuetifyとVue routerが導入済みであることを前提とします。
<参考> Vuetifyの使い方:Vueプロジェクトへのインストール手順
<参考> インストール | Vue Router

1.ToDoリストの作成

ありがたいことに、世の中ではいくらでもToDoリストの作り方が紹介されており
私のようなCOBOLしか読めない人間でも簡単にToDoリストを作ることができます。

ToDoの新規作成、読み取り、更新、削除機能を順に作っていきます。

1.1 ToDoの新規作成

まず最初に入力フォームやボタンなどの外見を作成し、次にFirestoreにToDoを保存する機能を作成します。

1.1-1) ToDo入力フォーム

ToDo名称を入力するためのフォームです。

ToDo入力フォーム抜粋
<v-text-field
 color="grey darken-1"
 label="ToDoを入力してください"
 v-model="newTodoName">
</v-text-field>

入力した値をあとでToDoタイトルとして使いたいので、変数newTodoNameにバインドしています。

1.1-2) 期限入力フォーム

期限がないと一生やらないので、各ToDoに期限を設定できるようにします。

VuetifyのDatePickerで実装します。(正直コピペです…)
このパーツだけで記述量がそこそこになってしまうのと、今後何かで再利用することもあろうと考えてコンポーネント化しています。

datePicker.vue
<template>
  <v-row>
    <v-col cols="5" sm="6" md="4">
      <v-menu
        v-model="menu"
        :close-on-content-click="false"
        :nudge-right="40"
        transition="scale-transition"
        offset-y
        min-width="290px"
      >
        <template v-slot:activator="{ on, attrs }">
          <v-text-field
            v-model="date"
            label="todo期限"
            prepend-inner-icon="event"
            readonly
            v-bind="attrs"
            v-on="on"
          ></v-text-field>
        </template>
        <v-date-picker
         v-model="date"
         @input="menu = false"
         @change="datePick">
        </v-date-picker>
      </v-menu>
    </v-col>
  </v-row>
</template>

<script>
  export default {
    name: 'datepicker',
    data () {
      return {
      date: new Date().toISOString().substr(0, 10),
      menu: false,
      }
    },
    created:function(){
      //date初期値(当日の日付)を親コンポーネントにわたすためにemit実行
      this.$emit('datePick',this.date)
    },
    methods:{
      datePick(){
        this.$emit('datePick',this.date)
      }
    }
  }
</script>

日付が選択された際に日付を親コンポーネントに渡すために、@change$emitを実行します。
加えて、日付は初期値でいい場合(当日をそのまま期限として設定したい場合)もあるので、createdフックでも実行しています。
今思えば、初期日付はブランクにして、必ず日付選択させるようにしてもよかったです。

なお、当コンポーネントは日付をYYYY/MM/DDの形式で渡しますが、今回のToDoリスト的には本来MM/DDで足ります。
しかし、再利用することも考えて汎用性のためにYYYYも含めた値を渡すようにしておき、親側でよしなに整形することとします。

todoリストのDatePicker部分抜粋
<template>
  <datepicker @datePick="dateSet"></datepicker>
</template>

<!-- ~中略~ -->

<script>
  methods:{
    dateSet(pickedDate){
      //datePickerコンポーネントから帰ってきた値を加工してappointedDateに格納する
      //YYYY-MM-DDで受け取るので、MM-DD形式に変換し、09などを9にするためNumberを噛ませる
      this.appointedDate = Number(pickedDate.substr(5,2)) + "/" + Number(pickedDate.substr(8,2))      
    },
</script>

前述のdatePickerの操作に応じてdateSetが動き、appointedDateにToDo期日を格納します。

1.1-3) ToDo追加ボタン

入力されたToDoを保存するためのボタンです。
image.png

ToDo追加ボタン抜粋
  <v-btn 
   small
   color="light-blue accent-4"
   dark
   @click="addTodo()">
   + 追加
  </v-btn>

このボタンを押したときに、後述するaddTodoメソッドでToDoの保存を実行します。

1.1-4) ToDoをFirestoreに書き込む(1/2:準備)

ここから本格的な機能を実装していきます。
まずはFirestoreを使うための準備です。

Firebaseのアカウント登録・プロジェクト作成をして云々といろいろやることがあるのですが、
はっきり言ってこのあたりの準備手順は他に素晴らしい解説記事がたくさんありますので、以下などをご参照いただければと思います。

公式ガイドや上記の記事などを参考に、firebase.jsを作成しmain.jsを編集します。

firabase.js
import Vue from 'vue'
import { firestorePlugin } from 'vuefire'
import firebase from 'firebase/app';
import 'firebase/firestore';

Vue.use(firestorePlugin)

const firebaseApp = firebase.initializeApp({
//"xxxxxxxxxxx"の部分は、Firebaseコンソールの
//「設定」→「マイアプリ」から確認できる値に置換する
  apiKey: "xxxxxxxxxxx",
  authDomain: "xxxxxxxxxxx",
  databaseURL: "xxxxxxxxxxx",
  projectId: "xxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxx",
  appId: "xxxxxxxxxxx",
  measurementId: "xxxxxxxxxxx"
});

export const db = firebaseApp.firestore();
main.js抜粋
import './plugins/firebase';

最後に、firebaseのモジュールを本体のtodo.vueでインポートして準備完了です。

todo.vue抜粋
import {db} from '@/plugins/firebase'

//~中略~

created: function() {
//todoコレクションへの参照  
this.todoListRef = db.collection("ToDoList")  

1.1-5) ToDoをFirestoreに書き込む(2/2:ToDo名称+期限の保存)

テキストフォームおよびDatePickerで入力した値をFirestoreに書き込みます。
ここで、書き込む際のメソッドはaddではなく**setを使います**。

<参考>Cloud Firestore にデータを追加する

addはドキュメントIDが無作為に採番されます。つまり降順に採番されていくわけではないので、ID順に表示すると新しいドキュメントが古いドキュメントより前に来たりします。

データ取得の際にソートしてもよいのですが、その場合も結局ソート用の項目を作らないといけないので
setを利用しドキュメントIDにミリ秒単位のタイムスタンプを設定しています。
家族しか使わないので、流石にミリ秒単位で重複することは考慮しなくてもよいでしょう。

addTodoメソッド
addTodo(){
  //入力がなければ抜ける
  if(this.newTodoName === ""){ console.log("todo入力なし");return }

  //処理日取得(todo登録日用の日付はYY/MM形式に、タイムスタンプはミリ秒形式の文字列に変換)
  const today     = new Date()
  const entryDate = (today.getMonth()+1) + "/" + today.getDate()
  const timestamp = today.toISOString()

  //todolistにsetする
  this.todoListRef.doc(timestamp).set({
  title:this.newTodoName,// 入力されたnewTodoName
  entry_date:entryDate,//todo登録日
  appointed_date:this.appointedDate,//DatePickerで設定したToDo期日
  complete_sts:false // 初期値はfalse(未完了)
  })

このメソッドが動くとこんな感じにFirestoreに登録されます。

1.2 ToDoの取得・表示

Firebase上に保存されているToDo一覧を取得して表示します。
併せてcomplete_sts更新用のチェックボックスと、ToDo削除用の削除ボタンも作ります。

まずはFirestoreから表示するためのデータを取得します。

データ取得処理抜粋
created: function() {

//~中略~

  //todoコレクションへの参照  
  this.todoListRef = db.collection("ToDoList")  
  //onSnapshotメソッドでリスナーを登録
  this.todoListRef.onSnapshot(querySnapshot => { 

    //Firestoreのデータが更新されるたびに以下の処理が実行される
    const obj = {} //object型でデータを取得するため定義しておく
    querySnapshot.forEach(doc => { 
      obj[doc.id] = doc.data()   
      })
    this.todos = obj
    })

onSnapshotquerySnapshot(コレクション配下にあるドキュメントのデータ)を取得し、
forEachでワーク用変数objに放り込んでいき、最後にthis.todosにまとめて格納しています。

このとき、doc.idをkeyとして、doc.dataをkeyに対するデータとして格納しています。
さらにdoc.data自体もオブジェクト型なので、最終的にtodosは以下のような入れ子構造になります。

todosの中身
{
  "2020-09-02T15:58:03.939Z": {
    "title": "牛乳買う",
    "appointed_date": "9/2",
    "entry_date": "9/3",
    "complete_sts": true
  },
  "2020-09-06T02:34:55.908Z": {
    "title": "洗剤買う",
    "appointed_date": "9/6",
    "entry_date": "9/6"
    "complete_sts": false,
  }
}

onSnapshotは一回きりのデータ取得ではなく**ドキュメントのリッスン**を行うので、Firestoreのデータが更新されるたびに自動でこの格納処理が動きます。
 このおかげで、ToDoリスト取得処理を何度も実行せずとも、新規作成・更新・削除のたびに常に最新の状態に保たれます。

次に、このtodosをリスト形式で表示します。

ToDoリスト表示部分抜粋
<ul>
  <li
   v-for="(todo,key) in todos" :key="key">
    <input
     type="checkbox"
     v-model="todo.complete_sts"
     @click="updateTodo(todo,key)"/>
    <font
     :class="{done: todo.complete_sts}">  
     {{todo.title}}
    </font>
    <font 
      class="appointedDate" >
      {{todo.appointed_date}}
      まで
    </font>
    <font 
      class="entryDate" >
      {{todo.entry_date}}
    </font>
    <v-btn
     x-small
     color="grey darken"
     dark
     margin-left="100px"
     @click="deleteTodo(todo,key)"
     v-show="todo.complete_sts"
     class="delbtn">
     削除
    </v-btn>
  </li>   
</ul>
style抜粋
    .entryDate {
      color:gray;
      font-size:70%;
    }
    .appointedDate{
      color:#C62828;
      font-size:85%;
      margin-left: 15px;
    }
    .delbtn {
      margin-left: 15px;
    }
    .done {
      color:gray;
      text-decoration: line-through;
    }

<li>要素をv-forで繰り返し、以下の通り要素を1行に並べて表示しています。

  • [チェックボックス] [ToDoタイトル] [ToDo期限] [ToDo登録日] [削除ボタン]

チェックボックス、ToDoタイトル、削除ボタンはcomplete_stsにバインドしており、
complete_ststrue(完了)かfalse(未完了)かによって表示を切り分けます。

complete_ststrue(完了)のとき、チェックボックスはチェック状態になり、
ToDoタイトルはdoneクラスが適用されてグレーアウト・打ち消し線が入り、
削除ボタンは表示になります。

上の例だとFirestore側は以下のようになっており、「牛乳買う」が完了、「洗剤買う」が未完了という状態です。

また、チェックボックスと削除ボタンはそれぞれ@clickでメソッドを動かすようにしています。
これは後述する「ToDo完了時の状態更新(CRUDの"U")」「完了したToDoの削除(CRUDの"D")」で作成するメソッドです。

1.3 ToDo完了時の状態更新

先ほど登場したupdateTodoメソッドでは、対象ToDoのcomplete_stsを更新します。

updateTodo抜粋

updateTodo(todo,key){
  //checkbox押下時にcomplete_stsを反転させて更新する(完了⇔未完了)
  todo.complete_sts = !todo.complete_sts 
  this.todoListRef.doc(key).update({
    complete_sts: todo.complete_sts 
  })
},    

「2.2 ToDoの取得・表示」においてkeyは各ドキュメントのIDを設定しているため、
doc(key).updateとすることで対象のドキュメントを更新することができます。

1.4 完了したToDoの削除(CRUDの"D")

削除ボタン押下時に動くdeleteTodoメソッドです。

deleteTodo抜粋
deleteTodo(todo,key){
  //削除ボタン押下時にfirestoreの該当docを削除する
  this.todoListRef.doc(key).delete()
},

updateTodoと同様にkeyはドキュメントIDなので、
doc(key).deleteとすることで対象のドキュメントを削除することができます。

以上で通常のToDoリストとしての要件であるCRUD機能は揃ったので、
ようやく、今回の本丸である家族用ToDoリストとしての特別な要件を作っていきます。

2.認証機能の作成

このまま公開すると、家族以外の第三者が自由に閲覧・更新できてしまいます。
これをプライベートなサービスとするため、Firebaseで認証されたユーザのみが利用できるようにしていきます。

2.1 家族のみが閲覧・利用できる

2.1-1) Firabase Authenticationの設定

FirebaseのAuthentication設定画面で、家族のユーザ情報を登録しておきます。

「ユーザーを追加」ボタンを押下するとメールアドレスとパスワードを入力できるようになるので、適宜入力して追加します。
(Sign-in methodタブから「メール / パスワード」を有効にしておく必要があります)

最後に、認証ユーザでなければ読み書きの両方を拒否するようFirestoreのルールを設定しておきます。

2.1-2) ログイン機能の作成

先ほど設定したメールアドレスとパスワードを入力し、認証を行う画面を作成します。

ログインフォーム抜粋
<v-text-field
 v-model="email"
 label="E-mail"
 required>
</v-text-field>

<v-text-field
 v-model="password"
 label="Password"
 type="password"
 required>
</v-text-field>

<v-btn
 color="light-blue darken-3"
 dark
 large
 @click="signIn">
  sign-in
</v-btn>
ログイン処理抜粋
signIn: function () {
  console.log("signin")
  firebase.auth().signInWithEmailAndPassword(this.email, this.password).catch(function(error) {
  //エラー時はポップアップを出す
        alert(error.message)
  });
//認証状態(userがnullでない)のときにtodoに遷移する
firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    location.href="#/todo";
  }
})
}

入力されたメールアドレスとパスワードはそれぞれemailpasswordにバインドし、
signInWithEmailAndPasswordの引数として渡すことでログイン処理を行います。
認証に成功した場合、ToDoリストの画面に切り替えています。

また、認証済みの状態でログイン画面を表示したとき(一度閉じて開き直したときなど)は
自動でToDoリストを表示したいので、createdフックでリダイレクトさせます。

ログイン画面リダイレクト処理抜粋
created: function(){
  //初期表示時にログイン済みであればtodoにリダイレクト
    firebase.auth().onAuthStateChanged(function(user) {
    if (user) {
      location.href="#/todo";
   }})
  },

これの逆をToDoリストにも組み込みます。
非認証状態でToDoリスト画面を表示しようとした際に、ログイン画面にリダイレクトさせます。

todoリスト画面リダイレクト処理抜粋
created: function() {
  //認証状態でない場合、サインイン画面にリダイレクト
  firebase.auth().onAuthStateChanged(function(user) {
  if (!user) {
    location.href="#/signin";
  }})

### 2.1-3) ログアウト機能の作成

あまり使うことはないですが、ログアウトもできるようにしておきます。

ログアウトボタン抜粋
<v-btn
 @click="signOut"
 color="grey darken-1"
 dark
 dense>
 sign-out
</v-btn>
ログアウト処理抜粋
signOut(){
    firebase.auth().signOut()
    this.$router.push('signin')
  } 

ログアウトは非常に簡単で、signOut()を実行するだけです。
ログアウト後、signin画面に戻るようにしています。

2.2 ToDoを誰が新規作成したかがわかる

これまでの機能だけだと、家族の「誰が」登録したToDoなのかがわかりません。
よって、以下のようにToDoの登録者が誰であるかわかるようにします。

本来、ユーザのプロフィール情報から名前(displayName)を取得してaddTodoで一緒に登録すればよいのでとても簡単なのですが
今回の場合、以下2つの問題があります。

  • メールアドレスをFirebaseコンソールから登録しているので、ユーザプロフィールにdisplayNameが存在しない
  • displayNameの代わりにユーザのemailを名前のように使ってもよいが、イケてない

1点目は、たとえばユーザ認証をGoogleアカウントなどで実施していた場合、Googleのユーザ名がdisplayNameとしてそのまま使えるのですが、
今回はメールアドレスとパスワードのみをもったユーザであるため、デフォルトではdisplayNameには何も設定されていません。

コンソールなどからどうにか編集できないか調べたのですが見当たらず、以下の力技で解決することにしました。
もし良いやり方をご存じの方がいらっしゃれば教えて下さい……。

2.2-1) displayName登録フォームを作る

手作業で登録する方法はわかりませんでしたが、displayNameを更新するメソッドがあることはわかったため、displayName登録フォームを作成しました。

displayName登録フォーム抜粋
<v-text-field
 color="grey darken-1"
 label="新しいユーザ名を入力してください"
 v-model="userName"
 >
</v-text-field> 

{{msg}}<br> 

<v-btn
 @click="updateName"
 color="blue darken-3"
 dark>
  submit
</v-btn>
updateName抜粋
updateName: function () {
  var user = firebase.auth().currentUser;

  //thisの参照が切れるのでselfに退避
  const self = this

  //入力されたuserNameで、ログイン中ユーザのdisplayNameを更新
  user.updateProfile({ displayName:self.userName }).then(function() {
  self.msg = "登録成功"
  }).catch(function(error) {
  console.log(error)
  self.msg = "登録失敗"
  });
  this.userName=""
}

ログインした状態で任意の名前を入力してsubmitボタンを押すと、そのログイン中ユーザのdisplayNameが登録(更新)されます。
これを全ユーザ分行ってdisplayNameを登録します。

初回の登録作業が終わればこのフォームは不要なので、あとで消してしまってOKです。

2.2-2) ToDo登録者名を書き込み・読み取り・表示する

最後に、`addTodo`メソッドにおいてログインユーザの`displayName`を`entry_user`という項目名でFirestoreに書き込み、 またtemplate部でもFirestoreから取得した`entry_user`を表示させるようにします。
addTodoメソッド(2)
addTodo(){
  //入力がなければ抜ける
  if(this.newTodoName === ""){ console.log("todo入力なし");return }

  //ログイン中のユーザ情報を取得
  const user = firebase.auth().currentUser;

  //ログイン状態であれば処理を行う
  if (user != null) {

    //currentUserのdisplayNameを設定
    const userName  = user.displayName; 

    //処理日取得(todo登録日用の日付はYY/MM形式に、タイムスタンプはミリ秒形式の文字列に変換)
    const today     = new Date()
    const entryDate = (today.getMonth()+1) + "/" + today.getDate()
    const timestamp = today.toISOString()
        
    //todolistにsetする
    this.todoListRef.doc(timestamp).set({
    title:this.newTodoName,// 入力されたnewTodoName
    entry_user:userName,//ログイン中ユーザのdisplayName
    entry_date:entryDate,//todo登録日
    appointed_date:this.appointedDate,//DatePickerで設定したToDo期日
    complete_sts:false // 初期値はfalse(未完了)
    })
  }
ToDoリスト表示部分抜粋
<ul>
  <li
   v-for="(todo,key) in todos" :key="key">

  <!--中略-->
    <font 
      class="entryUserName" >
      {{todo.entry_user}}
    </font> 
  <!--中略-->

  </li>   
</ul>
style抜粋(2)
    .entryUserName {
      color:gray;
      font-size:70%;
      margin-left: 10px;

3.参考文献

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?