Cloud Firestore helps us store data in the cloud. It supports offline mode so our app will work fine (write, read, listen to, and query data) whether device has internet connection or not, it automatically fetches changes from our database to Firebase Server. We can structure data in our ways to improve querying and fetching capabilities. This tutorial shows you a Vue.js app that can do Firebase Firestore CRUD Operations.
Related Post: Vue.js CRUD example – a simple Note App
Vue.js Firestore example Overview
Goal
Our Vue App can help us write new Notes, then it displays a list of Notes and each Note page (containing title and content) can be modified easily. This App also supports offline mode – we can create/update/delete Note without internet connection.
And, of course, this App interacts with Firebase Firestore as backend infrastructure:
Demo
Project Structure
We have 3 components:
– App.vue
holds all of the other components.
– NotesList.vue
contains all of notes in a List with + Note button.
– Note.vue
display a single Note in the List that allows us to create new Note or edit current Note.
Technologies
– Vue CLI 3.0.1
– Vue 2.5.17
– Firebase SDK for Javascript 5.4.2
Practice
Setup Vue Project
Install vue-cli
For use Vue CLI anywhere, run command:
npm install -g vue-cli
Init Project
Point cmd to the folder you want to save Project folder, run command:
npm create vue-note-app
You will see 2 options, choose default:
Install Firebase SDK
Run command: npm install firebase
Once the process is done, you can see firebase in package.json:
"dependencies": { "firebase": "^5.4.2", "vue": "^2.5.17" },
Setup Firebase Project
Create Project
Go to Firebase Console, login with your Google Account, then click on Add Project.
Enter Project name, select Location:
Then press CREATE PROJECT.
Config Rules for Firebase Cloud Firestore
On the left tab, click on Database.
Choose Cloud Firestore. Click on Create Database, a window is shown, choose Start in test mode:
This action is equivalent to set Database Rules:
service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write; } } }
Child Components
To understand child Components’ code, please read description in the tutorial:
Vue.js CRUD example – a simple Note App
Note Component
components/Note.vue
<template> <div class="note"> <div v-if="note"> <h3>Note</h3> <div class="form-group"> <input class="form-control" type="text" v-model="note.title" placeholder="Title" /> </div> <div class="form-group"> <textarea class="form-control" v-model="note.content" placeholder="Content"></textarea> </div> <button class="btn btn-danger" @click="removeNote()">Remove</button> <button class="btn btn-success" @click="saveNote()">Save</button> </div> <div v-else> <h5>Please create new Note...</h5> </div> </div> </template> <script> export default { name: "Note", props: ["note"], methods: { saveNote() { this.$emit("app-saveNote"); }, removeNote() { this.$emit("app-removeNote"); } } }; </script> <style> .note { margin: 20px; } </style>
NotesList Component
components/NotesList.vue
<template> <div class="list"> <h3>List</h3> <ul class="list-group"> <li class="list-group-item" v-for="(note, index) in notes" :key="note.index" :class="{ 'active': index === activeNote}" @click="changeNote(index)" > <div>{{ note.title }}</div> </li> </ul> <button @click="addNote()" class="btn btn-info">+ Note</button> </div> </template> <script> export default { name: "NoteList", props: ["notes", "activeNote"], methods: { changeNote(index) { this.$emit("app-changeNote", index); }, addNote() { this.$emit("app-addNote"); } } }; </script> <style> .list { margin: 20px; } </style>
App Component
In this Component, we have to do these things:
– import Firebase, initialize Firestore Database, then make reference to the Database Collection.
– import and interact 2 components above (NotesList
and Note
).
– use Firebase SDK function to do CRUD operations and listen to the Firestore events (create, delete, update).
Import Firebase Firestore
Go to Firebase Console, choose the Project that we have created before -> Project Overview.
Click on Web App icon:
A Popup will be shown:
Copy the code and paste in App.vue
, don’t forget to import, initialize Firebase and create Reference to the Notes Collection:
import firebase from "firebase/app"; import "firebase/firestore"; var config = { apiKey: "AIzaSyD7Lw-BWAP1jJUv_Kf3BmZWAxShItPXkug", authDomain: "ozenero-vue-firebase.firebaseapp.com", databaseURL: "https://ozenero-vue-firebase.firebaseio.com", projectId: "ozenero-vue-firebase", storageBucket: "ozenero-vue-firebase.appspot.com", messagingSenderId: "682969700542" }; firebase.initializeApp(config); const database = firebase.firestore(); database.settings({ timestampsInSnapshots: true }); database.enablePersistence(); const noteCollection = database.collection("notes");
Import Child Components
We define the data()
function that returns array of Notes (notes[]
) and a number for current Note’s index in the array (index
).
<template> <div id="app"> <NotesList @app-addNote="addNote" @app-changeNote="changeNote" :notes="notes" :activeNote="index" /> <Note @app-saveNote="saveNote" @app-removeNote="removeNote" :note="notes[index]" /> </div> </template> export default { name: "app", components: { NotesList, Note }, data: () => ({ notes: [], index: 0 }), ... }
Please remember that we have used $emit
in 2 child Components before:
+ Note Component: $emit("app-removeNote")
& $emit("app-saveNote")
+ NotesList Component: $emit("app-changeNote", index)
& $emit("app-addNote")
Now we have 4 event handlers with @app-
prefix that point to corresponding methods
inside App component:
export default { ... methods: { addNote() { // ... }, saveNote() { // ... }, changeNote(index) { // ... }, removeNote() { // ... } } };
Do Firebase Firestore CRUD Operations
– Create Note using collectionRef.add({data})
:
noteCollection.add(note);
– Update Note using nodeRef.update({data})
:
noteCollection.doc(note.id).update({ title: note.title, content: note.content });
– Delete Note using nodeRef.delete()
:
noteCollection.doc(id).delete();
– Read list of Notes using collectionRef.get().then(function(snapshot))
:
noteCollection.get().then(snapshot => { snapshot.forEach(doc => { this.notes.push({ id: doc.id, title: doc.data().title, content: doc.data().content }); }); });
Listen to the data changes
To listen for changes, use onSnapshot()
methods of firebase.firestore.CollectionReference
to observe events:
var unsubscribe; unsubscribe = noteCollection.onSnapshot(snapshot => { snapshot.docChanges().forEach(change => { if (change.type === "added") { console.log("note was added: ", { ...change.doc.data(), id: change.doc.id }); } if (change.type === "modified") { console.log("note was updated: ", { ...change.doc.data(), id: change.doc.id }); } if (change.type === "removed") { console.log("note was removed: ", { ...change.doc.data(), id: change.doc.id }); } }); }); // use the unsubscribe function on onSnapshot() to stop listening to updates unsubscribe();
Full code
App.vue
<template> <div id="app"> <div style="color: blue;"> <h1>ozenero</h1> <h3>Firestore Note App</h3> </div> <div class="row"> <div class="col-sm-6"> <NotesList @app-addNote="addNote" @app-changeNote="changeNote" :notes="notes" :activeNote="index" /> </div> <div class="col-sm-6"> <Note @app-saveNote="saveNote" @app-removeNote="removeNote" :note="notes[index]" /> </div> </div> </div> </template> <script> import NotesList from "./components/NotesList.vue"; import Note from "./components/Note.vue"; import firebase from "firebase/app"; import "firebase/firestore"; var config = { apiKey: "xxx", authDomain: "ozenero-vue-firebase.firebaseapp.com", databaseURL: "https://ozenero-vue-firebase.firebaseio.com", projectId: "ozenero-vue-firebase", storageBucket: "ozenero-vue-firebase.appspot.com", messagingSenderId: "xxx" }; firebase.initializeApp(config); const database = firebase.firestore(); database.settings({ timestampsInSnapshots: true }); database.enablePersistence(); const noteCollection = database.collection("notes"); var unsubscribe; export default { name: "app", components: { NotesList, Note }, data: () => ({ notes: [], index: 0 }), methods: { addNote() { this.notes.push({ title: "", content: "" }); this.index = this.notes.length - 1; }, changeNote(index) { this.index = index; }, saveNote() { const note = this.notes[this.index]; if (note.id) { this.updateNote(note); } else { this.createNote(note); } }, updateNote(note) { noteCollection.doc(note.id).update({ title: note.title, content: note.content }); }, createNote(note) { noteCollection.add(note); }, removeNote() { const id = this.notes[this.index].id; noteCollection.doc(id).delete(); } }, created() { noteCollection.get().then(snapshot => { snapshot.forEach(doc => { this.notes.push({ id: doc.id, title: doc.data().title, content: doc.data().content }); }); }); /* eslint-disable no-console */ unsubscribe = noteCollection.onSnapshot(snapshot => { snapshot.docChanges().forEach(change => { if (change.type === "added") { const note = { ...change.doc.data(), id: change.doc.id }; console.log("note was added: ", note); this.notes[this.notes.length - 1] = note; } if (change.type === "modified") { const updatedNote = this.notes.find( note => note.id === change.doc.id ); updatedNote.title = change.doc.data().title; updatedNote.content = change.doc.data().content; console.log("note was updated: ", updatedNote); } if (change.type === "removed") { const deletedNote = this.notes.find( note => note.id === change.doc.id ); console.log("note was removed: ", deletedNote); const index = this.notes.indexOf(deletedNote); this.notes.splice(index, 1); this.index = this.index === 0 ? 0 : index - 1; } }); }); /* eslint-enable no-console */ }, destroyed() { unsubscribe(); } }; </script> <style> #app { text-align: center; max-width: 700px; } </style>
Run
– Run Vue.js App with command: npm run serve
.
– Open browser with url: http://localhost:8080/
.