Suzuki Blog Written by Yuki Suzuki

Todoアプリ作成手順【Vue.js+Firebase】

Vue.js

こんにちは。スズキです。

最近はフロントエンドエンジニアとしてweb系企業への転職を目標に、JavaScript(Vue.js)の独学をしています。

先日、勉強したVue.jsとFirebaseを使って、Todoアプリを作成しました。

こんな感じです。

SimpleTodo

今回はVue.jsの復習も兼ねて、このTodoアプリの作成手順をまとめます。

私と同じように、Vue.jsを独学している方には、参考になる部分があると思います。

この記事の内容

  • アプリの機能を洗い出す
  • デザインカンプ作成
  • 環境構築
  • コーディング

スポンサードサーチ

アプリの機能を洗い出す

洗い出し

まず、このアプリに最低限必要な機能は何かを考えてみました。

その結果、洗い出された機能が以下です。

  • サインアップ機能(メールアドレスとパスワードを使用)
  • サインイン機能
  • サインアウト機能
  • タスク追加機能
  • タスク削除機能

これらの機能は全て実装することができました。

参考サイト

デザインカンプ作成

デザインカンプ

次にデザインを作成しました。

デザインについてはまだ勉強できていないのですが、とりあえず参考になりそうなデザインを探してみました。

参考サイト

探してきたデザインを参考にしつつ、作成しました。

デザインツールは「Figma」を使用しました。

デザインカンプ

スポンサードサーチ

環境構築

環境構築

続いて、環境構築をしていきます。

Vue.jsではvue-cliを使って環境構築ができます。

vue-cliを使うにはNode.jsが必要なので、インストールしましょう。

私はこちらの記事を見ながら進めていきました。

参考サイト

コーディング

コーディング

いよいよコーディングを進めていきます。

上記の参考サイト通りに環境構築を行うと、「src」フォルダができると思います。

このフォルダ内で作業していきます。

まず、Vue.jsについて少し説明します。

「src」フォルダ内に「.vue」ファイルを作成していくのですが、このファイルにはHTML,CSS,JavaScriptをまとめて記述することができます。

このようなファイルを「単一ファイルコンポーネント」と呼びます。

一般的には単に「コンポーネント」と呼ばれることが多いそうです。

このコンポーネントを組み合わせてフロントエンドを開発していきます。

それではコードの解説をしていきます。

このアプリのコードはGitHubで公開しているので、詳しく見たい方は、覗いてみてください。

GitHub

App.vue

<template>
  <div>
    <router-view />
  </div>
</template>

<script>

</script>

<style>
body {
  background-color: #6DCB93;
}
</style>

まずは、App.vueから見ていきます。

<router-view />の部分で、vue-routerライブラリによるコンポーネントの表示を行っています。

vue-routerは、パスに応じて表示するコンポーネントを切り替えることができます。

そのパスは、このアプリの場合はrouter.jsに設定されています。

router.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Signup from '@/components/Signup'
import Signin from '@/components/Signin'
import Todo from '@/components/Todo'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: "/",
      name: "signup",
      component: Signup
    },
    {
      path: "/signin",
      name: "signin",
      component: Signin
    },
    // カスタム文字列にユーザーUIDを設定して、Todoに遷移する
    {
      path: '/todo/:id',
      name: 'todo',
      component: Todo,
    }
  ]
});

export default router

まず、importの部分でsrc/components内にあるコンポーネントを読み込んでいます。

そして、routes:[ ]内でそれぞれのパスを設定します。

path: ’/’と設定したコンポーネントが一番最初に表示されます。

今回の場合は、「Signup.vue」ですね。

ちなみに、pathにはカスタム文字列を設定することもできます。

このアプリではユーザーごとにTodo.vueのURLを変えたいので、カスタム文字列を設定することにしました。

Signup.vue

<template>
  <div class="signup">
    <h1 class="signup-title">SimpleTodoへようこそ!</h1>
    <p class="signup-text">アカウントを登録してSimpleTodoを利用しよう!</p>
    <div class="signup-item">
      <label for="mail">メールアドレス</label>
      <br>
      <input type="text" id="mail" v-model="username">
    </div>
    <div class="signup-item">
      <label for="pass">パスワード(6文字以上)</label>
      <br>
      <input type="password" id="pass" v-model="password">
    </div>
    <div class="signup-btn" @click="signUp">
      登録
    </div>
    <p>
      <router-link class="signup-link" to="/signin">サインインはこちらから</router-link>
    </p>
  </div>
</template>

<script>
import firebase from "@/firebase.js";
import { db } from "@/firebase.js";

export default {
  name: "signup",
  data() {
    return {
      username: "",
      password: ""
    };
  },
  methods: {
    signUp: function() {
      firebase.auth().createUserWithEmailAndPassword(this.username, this.password)
      .then(user => {
        // FirestoreのドキュメントにユーザーUID、emailフィールドにアドレスをセットする
        db.collection('users').doc(user.user.uid).set({
          email: this.username
        })
        // サインイン画面に遷移
        this.$router.push('/signin')
      })
      .catch(error => {
        alert(error.message)
      })
    }
  }
};
</script>

<style scoped>
.signup {
  text-align: center;
}
.signup-title {
  color: #fff;
}
.signup-text {
  color: #fff;
}
.signup-item label {
  color: #fff;
}
.signup-item input {
  width: 40%;
  min-width: 300px;
  border: none;
  padding: 0.5em;
  margin-bottom: 30px;
  margin-top: 5px;
}
.signup-btn {
  cursor: pointer;
  width: 150px;
  border: 2px solid #fff;
  margin: 0 auto;
  padding: 12px 0;
  color: #fff;
  transition: .4s;
}
.signup-btn:hover {
  background: #fff;
  color: #6DCB93;
}
.signup-link {
  color: #fff;
}
</style>

まず、input要素で使っているv-modelを見てみましょう。

v-modelとは、input要素やtextarea要素などに「双方向データバインディング」を作成するために使用します。

「双方向データバインディング」とは、HTMLからの入力、JavaScriptからの入力、どちらから行っても双方向で値の更新ができる仕組みのことです。

今回の場合、テキストエリアにアドレスとパスワードを入力することで、data()内のusernameとpasswordの値を更新できるようになります。

続いて@clickを見てみます。

これはv-on:clickのことです。

v-onを使うことで、イベント発火時にJavaScriptの処理を実行できます。

今回の場合、登録ボタンをクリックすることで、methods内のsignUpが実行されます。

次にrouter-linkです。

router-linkで画面の遷移ができます。

「to=」の部分に遷移先を指定します。

遷移先はrouter.jsで設定したpathから指定できます。

今回の場合、サインイン画面に遷移させたいので、”/signin”を指定しています。

最後にsignUpを見てみます。

ここでは、createUserWithEmailAndPasswordメソッドを使用して、Firebaseにユーザー登録をしています。

FirebaseとはGoogleが提供しているバックエンドサービスです。

これを利用することで、フロントエンドの開発に専念することができ、バックエンドを開発しなくてもwebサービスが作れます。

Firebase

ここでFirebaseについて少し説明します。

まずはFirebaseの設定をしていきましょう。

私はこちらを参考にしながら進めました。

参考サイト

まずFirebase内にはAuthenticationという機能があります。

先程のcreateUserWithEmailAndPasswordメソッドにemailとpasswordを渡すことで、Authenticationにユーザー登録ができます。

さらにFirebase内にはFirestoreというデータベースがあります。

ここにデータ(このアプリではタスクなど)を保存できます。

そして、Firestoreには2つの重要な概念があります。

collectionとdocumentです。

collectionとは特定の情報を集めてグループ化するものです。

documentには実際のデータが入っています。

例えば今回の場合、「users」というcollection(情報の集まり)に、「ユーザー情報」をdocument(実際のデータ)として保存しています。

db.collection(‘users’).doc(user.user.uid).setのところですね。

ちなみに、Authenticationにユーザー登録するとユーザーUIDが発行されるため、それを利用しています。(user.user.uid)

これらの処理が成功したら、pushによってサインイン画面に遷移しています。

こちらもrouter-linkと同じく、router.jsで設定したpathで遷移先を指定できます。

Signin.vue

<template>
  <div class="signin">
    <h1 class="signin-title">おかえりなさい!</h1>
    <p class="signin-text">サインインしてSimpleTodoを利用しよう!</p>
    <div class="signin-item">
      <label for="mail">メールアドレス</label>
      <br>
      <input type="text" id="mail" v-model="username">
    </div>
    <div class="signin-item">
      <label for="">パスワード</label>
      <br>
      <input type="password" id="pass" v-model="password">
    </div>
    <div class="signin-btn" @click="signIn">
      サインイン
    </div>
    <p>
      <router-link class="signin-link" to="/">未登録の方はこちらから登録してください!</router-link>
    </p>
  </div>
</template>

<script>
import firebase from "@/firebase.js";
import { db } from "@/firebase.js"

export default {
  name: "signin",
  data() {
    return {
      username: "",
      password: "",
    };
  },
  methods: {
    signIn: function() {
      firebase.auth().signInWithEmailAndPassword(this.username, this.password)
      .then(user => {
        // サインインに成功したユーザーのUIDをパラメータにして、Todo画面に遷移
        // パラメータについては、router.js参照
        this.$router.push({ name: 'todo', params: {id: user.user.uid }})
      })
      .catch(error => {
        alert(error.message)
      })
    }
  },
};
</script>

<style scoped>
.signin {
  text-align: center;
}
.signin-title {
  color: #fff;
}
.signin-text {
  color: #fff;
}
.signin-item label {
  color: #fff;
}
.signin-item input {
  width: 40%;
  min-width: 300px;
  border: none;
  padding: 0.5em;
  margin-bottom: 30px;
  margin-top: 5px;
}
.signin-btn {
  cursor: pointer;
  width: 150px;
  border: 2px solid #fff;
  margin: 0 auto;
  padding: 12px 0;
  color: #fff;
  transition: .4s;
}
.signin-btn:hover {
  background: #fff;
  color: #6DCB93;
}
.signin-link {
  color: #fff;
}
</style>

Signup.vueと同じく、v-modelや@clickを使っています。

サインインボタンをクリックすることで、signInが実行されます。

ここではsignInWithEmailAndPasswordメソッドを使っています。

このメソッドにemailとpasswordを渡すことで、サインイン処理ができます。

サインインに成功したら、pushによってTodo画面に遷移させます。

先程のSignup.vueのpushと違うのは、パラメータを使っていることですね。

ユーザーごとに違うURLにしたいので、ユーザーごとに発行されているユーザーUIDをパラメータにセットしています。

Todo.vue

<template>
    <div class="todo">
      <h1 class="todo-title">SimpleTodo</h1>
      <h2 class="todo-text">{{ useremail }}さんのやることリスト</h2>
      <!-- 入力エリア -->
      <Form />
      <!-- タスクエリア -->
      <!-- プロパティの受け渡し -->
      <Tasks :tasks="tasks" />
      <div class="todo-btn" @click="signOut">
        サインアウト
      </div>
    </div>
</template>

<script>
import Tasks from "@/components/Tasks"
import Form from "@/components/Form"
import firebase from "@/firebase.js"
import { db } from "@/firebase.js"

export default {
  name: 'todo',
  data() {
    return {
      tasks: [],
      useremail: firebase.auth().currentUser.email
    }
  },
  components: {
    Tasks,
    Form,
  },
  methods: {
    signOut: function() {
      firebase.auth().signOut().then(() => {
        this.$router.push('/signin')
      })
    },
    // リロードしたら警告
    handler (event) {
      event.returnValue = "Data you've inputted won't be synced"
    }
  },
  mounted() {
    // $routeでクエリパラメータの取得
    const userId = this.$route.params.id
    // usersコレクションのユーザーごとのtasksコレクションを取得
    // onSnapshotでデータの更新を監視
    db.collection('users').doc(userId).collection('tasks').orderBy('createdAt')
    .onSnapshot((snapshot) => {
      snapshot.docChanges().forEach((change) => {
        const doc = change.doc
        // 変更されたレコードの配列上のインデックス番号を特定する
        // 配列のfindIndexで、tasks配列のうち task.id プロパティがchange.doc.idと同じもののIndex番号を取得
        const index = this.tasks.findIndex(task => task.id === change.doc.id)
        // タスクが追加された時、tasks配列に追加
        if (change.type === 'added') {
          this.tasks.push({id: doc.id, ...doc.data()})
        }
        // タスクが削除された時、tasks配列を削除
        if (change.type === "removed") {
          this.tasks.splice(index, 1)
        }
      })
    })
  },
  // リロードしたら警告
  created () {
    window.addEventListener("beforeunload", this.handler)
  },
  // リロードしたら警告
  destroyed () {
    window.removeEventListener("beforeunload", this.handler)
  }
}
</script>

<style scoped>
.todo {
  text-align: center;
}
.todo-title {
  color: #fff;
}
.todo-text {
  color: #fff;
}
.todo-btn {
  cursor: pointer;
  width: 150px;
  border: 2px solid #fff;
  margin: 50px auto 50px;
  padding: 12px 0;
  color: #fff;
  transition: .4s;
}
.todo-btn:hover {
  background: #fff;
  color: #6DCB93;
}
/* ウィンドウ幅が最大479pxまでの場合(スマホの場合)に適用 */
@media screen and (max-width: 479px) {
  .todo-btn {
    width: 200px;
  }
}
</style>

まずは、サインアウトボタンをクリックした時に実行されるsignOutを見てみましょう。

ここではsignOut()メソッドを使ってサインアウト処理をさせています。

サインアウトに成功したら、pushメソッドでサインイン画面に遷移させます。

続いて<Form />と<Tasks :tasks="tasks" />についてです。

 

componentsフォルダの「Form.vue」と「Tasks.vue」を表示させています。

以下の手順で、コンポーネントを表示させることができます。

  1. コンポーネント作成
    例:componentsフォルダに「Form.vue」を作成
  2. コンポーネントの読み込み
    import 変数名 from ‘コンポーネントの格納場所’
    例:import Form from ‘@/components/Form.vue’
  3. コンポーネントの呼び出し
    export default {components: { 変数名 } }
    例:export default { components: { Form } }
  4. 表示させたい場所に記述
    <変数名 />
    例:<Form />

ちなみに、Tasksを表示する際、アトリビュートを設定することで、データの受け渡しを行っています。

「:tasks」が任意のアトリビュート名、「”tasks”」が受け渡すデータです。

受け取り先のTasks.vueでprops: [‘tasks’]と記述することでデータの受け渡しができます。

次にmounted()についてです。

mounted()とはコンポーネントが描画されたときに実行されるメソッドです。

 

この中で、まずはクエリパラメータを取得します。

つまりTodoに遷移する際に渡しているユーザーUIDのことですね。

続いて、取得したユーザーUIDを利用して、そのユーザーのtasksコレクションのデータを取得しています。

 

createdAtはタスクの登録日時で、orderByを使うことで登録順に並び替えています。

この取得したデータに対して、onSnapshot()メソッドを使用しています。

onSnapshot()ではイベントの監視をしています。

 

今回の場合はFirebaseのデータ更新を監視しています。

 

この監視はデータの追加や削除などをアプリの画面側にも即座に反映させるために必要です。

 

collectionに変化があったときにsnapshotがコールバック関数に渡されます。

snapshotとはデータベースをコピーしたようなものです。

snapshotに対してdocChanges()を使うことで、データベースの変更点を取得できます。

 

そして、それに対してforEachでいろいろ処理しています。

Firebaseの更新といっても色々ありますが、今回は追加と削除について考えています。

change.typeが’added’のとき(追加されたとき)に配列tasksにpushしています。

change.typeが’removed’のとき(削除されたとき)に配列tasksからspliceしています。

 

ちなみにspliceの引数のindexは、配列tasksに対してfindIndexを使い、削除されたデータと同じものを返しているものを利用しています。

最後にhandlerについてです。

これはTodoの画面を閉じたり、リロードしようとした時に警告を出すために記述しています。

Form.vue

<template>
  <div class="input-container">
    <p class="form-text">タスクを追加!</p>
    <textarea v-model="text"></textarea>
    <div class="form-btn" @click="addTask">
      Add
    </div>
  </div>
</template>

<script>
import firebase from "@/firebase.js"
import { db } from "@/firebase.js"

export default {
  data() {
    return {
      text: null
    }
  },
  methods: {
    addTask() {
      // $routeでクエリパラメータの取得
      const userId = this.$route.params.id
      // userコレクションの各ユーザーのtasksコレクションにタスクを登録
      db.collection('users').doc(userId).collection('tasks').add({
        // タスクと登録日付と完了状態
        text: this.text,
        createdAt: new Date().getTime(),
        isDone: false
      }).then(() => {
        // 登録成功したら空欄に
        this.text = null
      })
    },
  }
}
</script>

<style scoped>
.input-container {
  padding: 10px;
  height: 100%;
  text-align: center;
}
.form-text {
  color: #fff;
}
textarea {
  width: 50%;
  height: 100%;
  border: none;
}
.form-btn {
  cursor: pointer;
  width: 150px;
  border: 2px solid #fff;
  margin: 20px auto 10px;
  padding: 12px 0;
  color: #fff;
  transition: .4s;
}
.form-btn:hover {
  background: #fff;
  color: #6DCB93;
}

/* ウィンドウ幅が最大479pxまでの場合(スマホの場合)に適用 */
@media screen and (max-width: 479px) {
  textarea {
    width: 100%;
  }
  .form-btn {
    width: 200px;
  }
}
</style>

ここでも@clickを使っており、クリックするとaddTaskが実行されます。

まずクエリパラメータを取得し、それを利用してtasksコレクションにタスクを登録します。

addメソッドを使うことで、Firestoreに登録できるのです。

今回の場合は、tasksコレクションの

  • Textフィールドに、入力したタスク(this.text)
  • createdAtフィールドに、現在日時(getTime)
  • isDoneフィールドに、false(タスクの完了状態を表現しています。最初は完了していないので、false)

を登録してます。

これらの登録が成功したらtextarea(this.text)をnullにして、クリアします。

Tasks.vue

<template>
  <div class="todos-container">
    <!-- v-forを使ってプロパティの受け渡し -->
    <Task v-for="task in tasks" :task="task" :key="task.id" />
  </div>
</template>

<script>
import Task from "@/components/Task"

export default {
  // プロパティの受け取り(Todo.vueからTasks.vueへ)
  props: ['tasks'],
  components: {
    Task,
  }
}
</script>

<style scoped>
.todos-container {
  padding: 16px;
  margin: 0 50px;
}
/* ウィンドウ幅が最大479pxまでの場合(スマホの場合)に適用 */
@media screen and (max-width: 479px) {
  .todos-container {
    margin: 0;
  }
}
</style>

Taskコンポーネントを表示させる際に、v-forを使ってプロパティの受け渡しをしています。

配列Tasksに登録されている分だけ表示させたいので使っています。

Task.vue

<template>
  <div class="todo-container">
    <div class="task-container">
      <input type="checkbox" v-model="task.isDone">
      <div v-bind:class="{done: task.isDone}">{{ task.text }}</div>
    </div>
    <div class="task-btn-container">
      <!-- task.idはfirebaseに登録されているドキュメントIDのこと -->
      <div class="task-btn" @click="delTask(task.id)">
        Delete
      </div>
    </div>
  </div>
</template>

<script>
import firebase from "@/firebase.js";
import { db } from "@/firebase.js"

export default {
  // プロパティの受け取り(Tasks.vueからTask.vueへ)
  props: ['task'],
  methods: {
    // task.idを引数に取る
    delTask: function(id) {
      const userId = this.$route.params.id
      // ここにタスクコレクション内の消したいドキュメント(task.id)を指定
      const taskId = db.collection('users').doc(userId).collection('tasks').doc(id)
      // 上記で指定したドキュメントを削除
      taskId.delete().then(function() {})
    }
  }
}
</script>

<style scoped>
.todo-container {
  display: flex;
  padding: 8px;
  justify-content: space-between;
}

.task-container {
  word-wrap: break-word;
  width: 80%;
  text-align: left;
  display: flex;
}

.done{
	text-decoration: line-through;
	color: gray;
}

.task-btn-container {
  width: 20%;
}

.task-btn {
  cursor: pointer;
  width: 80px;
  border: 2px solid #fff;
  margin: 0 auto;
  padding: 12px 0;
  color: #fff;
  transition: .4s;
}

.task-btn:hover {
  background: #fff;
  color: #6DCB93;
}

/* ウィンドウ幅が最大479pxまでの場合(スマホの場合)に適用 */
@media screen and (max-width: 479px) {
  .todo-container {
    display: block;
  }
  .task-container {
    width: 100%;
    margin: 0 auto;
  }
  .task-btn-container {
    width: 100%;
  }
  .task-btn {
    width: 200px;
    margin-top: 20px;
  }
}
</style>

まずcheckboxにv-modelを使ってバインディングしています。

task.isDoneとは先程Form.vueで確認した、falseで登録しているものですね。

そして次のdiv要素にv-bindを使っています。

プロパティは同じくtask.isDoneです。

これでチェックボックスの切り替えによって、div要素の「doneクラス」を切り替えられるようになりました。

繰り返しですが、これでタスクの完了状態を表現しています。

最後にdelTaskを確認します。

ここではタスクの削除を行っています。

引数にtask.idを指定することで、特定のタスクだけを削除できます。

削除するには、delete()メソッドを使うだけでOKです。

スポンサードサーチ

まとめ

まとめ

いかがでしたか。

長くなってしまいましたが、コードの解説をしてきました。

私と同じようにVue.jsの勉強を始めたばかりの方には、参考になった部分もあるのではないでしょうか。

今回初めてVue.jsを使ってアプリを作ってみました。

色々と学んだことや反省点があるのですが、特に次につなげたいことが2つあります。

  • ゲストログインボタンを実装したい
  • Vue.jsのインプットが足りない

まず、ゲストログインボタンについてです。

アプリが完成してから思ったのですが、こういったポートフォリオをユーザー登録してまで使ってくれる人はあまりいないと思います。

そんな人にも使ってもらうために、登録なしでも使える機能を実装したいと思います。

そして、まだまだVue.jsのインプットが必要だなと感じました。

今回復習をしてみて、さらに知識が深まりましたが、正直理解できていない部分もあります。

2つ目のアプリ作成の前にJavaScriptとVue.jsのインプットを改めて頑張りたいなと思います。

今回は以上です。

コーディングを学ぶならデイトラ

Web制作コース

動画でプログラミングを学ぶなら

Udemy

オンラインスクールでプログラミングを学ぶなら

Skill Hacks