はじめに
Nuxt.jsの学習でTodoアプリを作成したので、理解した内容で少し機能の追加をしてみました。
0から作成する手順をまとめたいと思います。初学者のため、コード等お見苦しい箇所が多くあるかもしれません。お許しください
作成物
機能
以下が主な機能でnewが追加した部分になります。
- タスクが登録できる
- タスクに備考が登録できる new
- タスクに日付が登録できる new
- タスクを完了にできる
- タスクを削除できる
- UIフレームワークでVuetifyを使用する new
- 登録の際にダイアログを表示する new
- TodoとDoneは別々の表示領域を用意する new
前提
- Googleアカウントを所持していること
- Nuxtの開発環境が用意してあること
※開発環境の用意についてはこちらにまとめてみました!
Firebaseの設定
データの格納場所としてFirebaseの CloudFirestore
を使用します。
Cloud Firestore
サイトへアクセス
以下のURLへアクセスします。
https://firebase.google.com/?hl=ja
使ってみるを選択するとGoogleアカウントの認証画面が開くので認証情報を入力します。
プロプロジェクトの作成
Firebaseブロジェクトの「プロジェクトの追加」を選択します。
プロジェクト名を入力して「続行」を選択します。
Googleアナリティクスを有効にして「続行」を選択します。
アナリティクスを日本にして利用規約等を読み「プロジェクトを作成」を選択します。
Database(CloudFirestore)の作成
「開発」 -> 「Database」からCloudFirestoreを開きます。
「データベースを作成」を選択します。
セキュリティ保護ルールの作成にて「テストモードで開始」を選択します。ロックモードでは認証ありのモードです。テストモードは認証がありませんので検証や学習の際に利用します。
テストモードの場合、プロジェクトIDのみで利用できてしまうので、外部にプロジェクトIDを出さないよう注意しましょう。検証が終わったらdatabaseは削除します
ロケーション(物理サーバーの配置場所)の設定では「asia-northeast1 (東京)」を選択して完了を選択します。
これでFirebase側の準備はOKです!
プロジェクトの作成
npx create-nuxt-app nuxt-todo
作成時以下の内容だけ指定して、他は初期値にします。
// 利用するパッケージマネージャー
? Choose the package manager Npm
- Npm
// 利用するUIフレームワーク
? Choose UI framework None
- Vuetify
// 利用するコード検証ツール(コードのチェックツール)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
- ESLint
- Prettier
// 利用するレンダリングモード
? Choose rendering mode Universal (SSR)
- SPA
Firebase関連のパッケージをインストール
firebaseを利用するための基本パッケージ(firebase)をインストールします。
npm install --save firebase@6.2.4
CloudfireStoreを簡単に利用するためのパッケージ(vuexfire)をインストールします。
npm install --save vuexfire@3.0.1
環境変数の設定
.envから環境変数を読み込むためのパッケージ(dotenv)をインストールします。
npm install --save @nuxtjs/dotenv@1.3.0
.envを作成
FIREBASE_PROJECT_ID = 'project-id'
プロジェクトIDはコンソール画面の歯車マークから「プロジェクトの設定」を選択し、開いた「Setting」の画面に表示されています。
.env を使用するための設定を追加
/*
** Nuxt.js modules
*/
modules: ['@nuxtjs/dotenv'],
Gitで管理する予定の場合は .gitignore
に.envが含まれていることを確認します。(Gitの反映対象外とする)
# dotenv environment variables file
.env
Firebaseとの連携
pluginsディレクトリの下に連携用のファイルを作成します。ファイル名は自由です。
import firebase from 'firebase/app'
import 'firebase/firestore'
// .envからプロジェクトIDを取得して定数に設定
const config = {
projectId: process.env.FIREBASE_PROJECT_ID
}
// firebaseの初期化処理
if (!firebase.apps.length) {
firebase.initializeApp(config)
}
export default firebase
ストアの作成
storeディレクトリの下にindex.jsファイルを作成します。
import { vuexfireMutations } from 'vuexfire'
export const mutations = {
...vuexfireMutations
}
storeディレクトリの下にtask.jsファイルを作成します。
import { firestoreAction } from 'vuexfire'
import firebase from '~/plugins/firebase'
const db = firebase.firestore()
const taskRef = db.collection('task')
export const state = () => ({
tasks: []
})
export const actions = {
// 初期化
init: firestoreAction(({ bindFirestoreRef }) => {
bindFirestoreRef('tasks', taskRef)
}),
// 追加
add: firestoreAction((context, { title, detail, date }) => {
if (title.trim()) {
taskRef.add({
title,
detail,
date,
status: false
})
}
}),
// 削除
remove: firestoreAction((context, id) => {
taskRef.doc(id).delete()
}),
// ステータス更新
toggle: firestoreAction((context, todo) => {
taskRef.doc(todo.id).update({
status: !todo.status
})
})
}
// 日付の昇順でソート
export const getters = {
orderdTodos: (state) => {
return _.orderBy(state.tasks, 'date', 'asc')
}
}
一覧データを日付の昇順で表示するためにlodashというライブラリを使用しました。
使用するためにはnuxt.config.jsに以下の記載をします。
import webpack from 'webpack'
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {},
plugins: [
new webpack.ProvidePlugin({
_: 'lodash'
})
]
}
コンポーネントの作成
コンポーネントは全部で3つ作成しました。
もっと細かく分けたかったのですが、ざっくりと分けることにしました。
タスクコンポーネント
まずはダイアログの部分です。
Vuetifyにはダイアログのコンポーネントがあるのでそれを利用しました。
datepickerのコンポーネントも色々表示のさせ方が豊富で便利ですね
登録を押した時にaddメソッドからtask.jsのaddが実行されてDBに登録されます。
<template>
<v-dialog v-model="dialog" persistent max-width="600px">
<template v-slot:activator="{ on }">
<v-btn color="#5963F8" dark class="font-weight-bold" v-on="on"
><v-icon small class="mr-2">mdi-plus-circle-outline </v-icon
>新規タスクを追加</v-btn
>
</template>
<v-card>
<v-card-title>
<span class="headline">新規タスクを追加</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12">
<v-text-field v-model="title" label="Title"></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea v-model="detail" label="Detail"></v-textarea>
</v-col>
<v-col cols="12">
<v-dialog
ref="dialog"
v-model="modal"
:return-value.sync="date"
persistent
width="290px"
>
<template v-slot:activator="{ on }">
<v-text-field
v-model="date"
label="日時"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="date" scrollable>
<div class="flex-grow-1"></div>
<v-btn text color="primary" @click="modal = false"
>Cancel</v-btn
>
<v-btn text color="primary" @click="$refs.dialog.save(date)"
>OK</v-btn
>
</v-date-picker>
</v-dialog>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<div class="flex-grow-1"></div>
<v-btn color="primary" text @click="dialog = false">キャンセル</v-btn>
<v-btn color="primary" text @click="add">登録</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
data() {
return {
title: '',
detail: '',
dialog: false,
date: new Date().toISOString().substr(0, 10),
menu: false,
modal: false
}
},
methods: {
add() {
this.$store.dispatch('task/add', {
title: this.title,
detail: this.detail,
date: this.date
})
this.dialog = false
}
}
}
</script>
リストコンポーネント
次に一覧を表示するタスクの一覧の部分です。
TodoとDoneで利用するのでtitileとtasklistは親から値をもらうようにしています。
チェックボックスを押した時にtoggle、削除アイコンを押した時にremoveを実行します。
<template>
<v-row>
<v-col cols="12" md="12">
<v-list color="#f4f5fc">
<v-subheader class="font-weight-bold">{{ title }}</v-subheader>
<v-col v-for="task in tasklist" :key="task.id" cols="12" class="pt-0">
<v-card>
<v-card-title class="headline pb-0">
<v-checkbox
:checked="task.status"
color="primary"
class="ma-0"
:label="task.title"
@change="toggle(task)"
></v-checkbox>
</v-card-title>
<v-card-text class="pb-0">{{ task.detail }}</v-card-text>
<v-card-actions class="pt-0">
<v-col cols="10" md="10" class="pl-0">
<v-btn text>
<v-chip color="grey lighten-3" label>
<v-avatar left>
<v-icon small color="primary">mdi-calendar</v-icon>
</v-avatar>
{{ task.date }}
</v-chip></v-btn
>
</v-col>
<v-col cols="2" md="2">
<v-btn icon color="grey" text dark @click="remove(task.id)">
<v-icon>mdi-close-circle-outline</v-icon>
</v-btn>
</v-col>
</v-card-actions>
</v-card>
</v-col>
</v-list>
</v-col>
</v-row>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
},
tasklist: {
type: Array,
default: null
}
},
methods: {
remove(id) {
this.$store.dispatch('task/remove', id)
},
toggle(task) {
this.$store.dispatch('task/toggle', task)
}
}
}
</script>
<style>
.theme--light.v-label {
color: #000;
}
.v-input--selection-controls:not(.v-input--hide-details) .v-input__slot {
margin-bottom: 0px;
}
.v-application--is-ltr .v-list-item__avatar:first-child {
margin-right: 0;
}
</style>
ボードコンポーネント
最後にpagesディレクトリにboard.vueを作成します。
上記で作成した、タスクコンポーネント、リストコンポーネントをimportします。
また新規登録用の処理とcreatedのタイミングでfirebaseの初期化処理を実行します。
<template>
<v-container class="todo">
<v-form ref="form">
<v-row>
<v-col cols="12" md="12">
<task-detail></task-detail>
</v-col>
</v-row>
</v-form>
<task-list title="Todo" :tasklist="todolist"></task-list>
<task-list title="Done" :tasklist="donelist"></task-list>
</v-container>
</template>
<script>
import TaskList from '../components/TaskList.vue'
import TaskDetail from '../components/TaskDetail.vue'
export default {
components: {
TaskList,
TaskDetail
},
computed: {
todolist() {
return this.$store.getters['task/orderdTodos'].filter(function(el) {
return el.status === false
}, this)
},
donelist() {
return this.$store.getters['task/orderdTodos'].filter(function(el) {
return el.status === true
}, this)
}
},
created() {
this.$store.dispatch('task/init')
}
}
</script>
<style scoped>
.status.done {
text-decoration: line-through;
}
</style>
UIの調整
外観の部分を調整するためレイアウトを少しだけ修正しました。
<template>
<v-app dark>
<v-app-bar color="#5963F8" fixed app dark>
<v-toolbar-title>{{ title }}</v-toolbar-title>
</v-app-bar>
<v-content>
<v-container>
<nuxt />
</v-container>
</v-content>
</v-app>
</template>
<script>
export default {
data() {
return {
title: 'TodoList'
}
}
}
</script>
<style>
.theme--light.v-application {
background: #fff;
}
.v-toolbar__title {
font-family: 'Damion', cursive;
font-size: 2.5rem;
width: 100%;
text-align: center;
}
</style>
おわりに
Vueの学習課題としてよく見かけるTodoアプリの作成を、自分が本当に理解できたのか確認するために今回の作業を行いました。編集できるようにしたり、ドラッグ&ドロップできるようにしたりなど意外にやれることは多くて面白い課題だなと感じました。どこかでTrelloやTodoListなどのアプリを参考にして機能を追加していけたらいいなと思っています。
ここまでお読みいただきありがとうございました!!