SOFTWAREONTWIKKELINGCUSTOM SOFTWAREFIREBASE
22/08/2018 • Dorien Jorissen

Snel schaalbare applicaties ontwikkelen met VueJS en Firebase (deel twee)

In het tweede deel van deze blogreeks deel ik mijn kennis over het creëren van een notitieapp. Het maken, bijwerken en verwijderen van notities en het toevoegen van verificatie komen aan bod. Het eerste deel van deze blogreeks waarin we onze Firebase-database en Vue-applicatie voor de weergave van notities hebben ingesteld, vind je hier.

Wat gaan we maken?

creating fast en scalable applications with VuaJS and firebase

We gaan verder waar we gebleven waren en zullen onze notitieapp voltooien.

Afhankelijk van je vaardigheidsniveau, zullen de volgende stappen je circa 20 tot 45 kosten om te voltooien.

Stap 1: Notities toevoegen

In onze vorige blog hebben we al geleerd hoe we notities kunnen toevoegen, maar dit hebben we wel handmatig gedaan in onze Firestore-database. We kunnen een functionaliteit creëren zodat we notities rechtstreeks via de interface van onze applicatie kunnen toevoegen. In Notes.vue:

<template>
  <div class="notes">
    <h1>
      Notes
      <el-button type="primary" size="medium" @click="addDialogVisible = true"><i class="el-icon-circle-plus"></i> Add note</el-button>
    </h1>
    <el-table
      :data="tableData"
      empty-text="Loading, or no records to be shown."
      border>
      <el-table-column type="expand">
        <template slot-scope="props">
          <p>{{ props.row.content }}</p>
        </template>
      </el-table-column>
      <el-table-column
        label="Note title">
        <template slot-scope="props">
          {{ props.row.title }}
        </template>
      </el-table-column>
      <el-table-column
        label="Date added / modified"
        prop="date">
      </el-table-column>
      <el-table-column
        fixed="right"
        label=""
        width="90">
        <template slot-scope="scope">
          <el-button type="info" size="small" icon="el-icon-edit" circle></el-button>
          <el-button type="danger" size="small" icon="el-icon-delete" circle style="margin-left: 5px;"></el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-dialog
      title="Add note"
      :visible.sync="addDialogVisible"
      width="30%">
      <el-form ref="addNoteRuleForm" :model="addNoteRuleForm" :rules="rules">
        <el-form-item label="Note title" prop="title">
          <el-input type="text" placeholder="Note title" v-model="addNoteRuleForm.title"></el-input>
        </el-form-item>
        <el-form-item label="Note content" prop="content">
          <el-input type="textarea" placeholder="Note content" v-model="addNoteRuleForm.content"></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="addDialogVisible = false">Cancel</el-button>
        <el-button type="primary" @click="addNoteForm('addNoteRuleForm'); addDialogVisible = false;">Confirm</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
import { db } from '@/main'
export default {
  name: 'Notes',
  data() {
    return {
      tableData: [],
      addDialogVisible: false,
      addNoteRuleForm: {
        title: '',
        content: ''
      },
      rules: {
        title: [
          { required: true, message: "Note title can't be empty", trigger: 'blur' }
        ],
        content: [
          { required: true, message: "Note content can't be empty", trigger: 'blur' }
        ]
      }
    }
  },
  created () {
    db.collection('notes').get().then(querySnapshot => {
      querySnapshot.forEach(doc => {
        const data = {
          'id': doc.id,
          'date': doc.data().date,
          'title': doc.data().title,
          'content': doc.data().content
        }
        this.tableData.push(data)
      })
    })
  },
  methods: {
    addNoteForm (formName) {
      var self = this;
      this.$refs[formName].validate((valid) => {
        if (valid) {
          console.log('Form valid');
        } else {
          console.log('error submit!!')
        }
      })
    }
  }
}
</script>

De code hierboven laat zien dat we een @click=”addDialogVisible = true”-handler aan onze 'Add note'-knop hebben toegevoegd en dat we die in onze 'data-hook' hebben geregistreerd als standaard 'false'. 

We hebben ook de dialoog zelf toegevoegd die op basis van de waarde ‘addDialogVisible’ wordt getoond. We hebben aan deze dialoog een ElementUI-formulier toegevoegd met titel- en contentvelden die bepaalde validatieregels bevatten die ook worden gespecificeerd in onze 'data-hook'.

Als we nu de knop indrukken om de notitie toe te voegen, worden de verwijzingen van onze formuliernaam doorgegeven en wordt er een Vue Object teruggestuurd met de velden die gevalideerd moeten worden.

We kunnen vervolgens onze database-query toevoegen. Wijzig de methode ‘addNoteForm’ als volgt:

    addNoteForm (formName) {
      var self = this;
      this.$refs[formName].validate((valid) => {
        if (valid) {
          var today = new Date().toLocaleString('en-GB');
          db.collection('notes').add({
            'title': this.addNoteRuleForm.title,
            'content': this.addNoteRuleForm.content,
            'date': today
          }).then(function (docRef) {
            self.$message({
              type: 'success',
              message: 'Note successfully added'
            });
          }).catch(function (error) {
            console.error('Error adding document: ', error)
          })
        } else {
          console.log('error submit!!')
        }
      })
    }

Dien een geldig formulier in om het uit te testen en er wordt een succesbericht getoond. Er is echter nog steeds een probleem met het tonen van de nieuwe notitie die is toegevoegd in onze tabel. De notitie verschijnt als de pagina wordt vernieuwd.

Dit komt omdat we de database opdracht hebben gegeven om de notities op te halen die moeten worden uitgevoerd via de 'created' hook. Deze wordt geactiveerd als we de pagina openen, maar niet als we onze database hebben bijgewerkt. We kunnen dat flexibeler instellen door de get-query in plaats daarvan te verplaatsen naar een methode:

  created () {
    this.getNotes()
  },
  methods: {
    getNotes () {
      db.collection('notes').orderBy('date').get().then(querySnapshot => {
        this.tableData = [];
        querySnapshot.forEach(doc => {
          const data = {
            'id': doc.id,
            'date': doc.data().date,
            'title': doc.data().title,
            'content': doc.data().content
          }
          this.tableData.push(data);
        })
      })
    },
    addNoteForm (formName) {
      var self = this;
      this.$refs[formName].validate((valid) => {
        if (valid) {
          var today = new Date().toLocaleString('en-GB');
          db.collection('notes').add({
            'title': this.addNoteRuleForm.title,
            'content': this.addNoteRuleForm.content,
            'date': today
          }).then(function (docRef) {
            self.getNotes();
            self.$message({
              type: 'success',
              message: 'Note successfully added'
            });
          }).catch(function (error) {
            console.error('Error adding document: ', error)
          })
        } else {
          console.log('error submit!!')
        }
      })
    }
  }

Als je goed kijkt dan zie je dat we ook een orderBy(‘date’) hebben toegevoegd aan de query. Als we nu een andere notitie toevoegen, wordt onze tabel naadloos bijgewerkt met de laatste notitie onderaan de rij. Pas orderBy(‘date’) aan naar orderBy(‘date’, ‘desc’) om dit om te keren.

Stap 2: Notities verwijderen

Het verwijderen van een item is vrij eenvoudig, maar moet vanuit de gebruiker bezien ook weer niet te eenvoudig zijn. Ik heb daarom besloten om het verwijderen af te handelen na een ElementUI-bevestigingsdialoogvenster.

Voeg eerst een @click-handler toe aan het verwijderingspictogram waarnaar we de ID sturen van de notitie die we willen verwijderen:

<template slot-scope="props">
  <el-button type="info" size="small" icon="el-icon-edit" circle></el-button>
  <el-button type="danger" size="small" icon="el-icon-delete" circle style="margin-left: 5px;" @click="deleteNote(props.row.id)"></el-button>
</template>

Let op: ik heb ook de slot-scope=”scope” in de template-tag aangepast naar slot-scope=”props” om die af te stemmen op de andere props die aan de tabel worden doorgegeven.

We kunnen vervolgens de deleteNote-methode registreren:

    deleteNote (noteId) {
      var self = this;
      this.$confirm('This will permanently delete the note. Continue?', 'Warning', {
        confirmButtonText: 'OK',
        cancelButtonText: 'Cancel',
        type: 'warning'
      }).then(() => {
        db.collection('notes').doc(noteId).delete().then(function() {
          self.getNotes();
          self.$message({
            type: 'success',
            message: 'Delete completed'
          });
        }).catch(function(error) {
          console.error("Error removing note: ", error);
        });
      }).catch(() => {
        console.log("Delete canceled");
      });
    }

Et voilà! Het verwijderen van notities is voltooid.

Stap 3: Notities bewerken

Voeg net zoals bij verwijderen, een bewerkingshandler toe aan de bewerkingsknop samen met een waarde ‘editDialogVisible’ zoals we hebben toegevoegd voor de 'addDialog'.

<template slot-scope="props">
  <el-button type="info" size="small" icon="el-icon-edit" circle @click="editNote(props.row.id); editDialogVisible = true"></el-button>
  <el-button type="danger" size="small" icon="el-icon-delete" circle style="margin-left: 5px;" @click="deleteNote(props.row.id)"></el-button>
</template>

We moeten vervolgens een dialoog creëren waar het bewerken plaatsvindt:

    <el-dialog
      title="Edit note"
      :visible.sync="editDialogVisible"
      width="30%">
      <el-form ref="editNoteRuleForm" :model="editNoteRuleForm" :rules="rules">
        <el-form-item label="Note title" prop="title">
          <el-input type="text" placeholder="Note title" v-model="editNoteRuleForm.title" value="editNoteRuleForm.title"></el-input>
        </el-form-item>
        <el-form-item label="Note content" prop="content">
          <el-input type="textarea" placeholder="Note content" v-model="editNoteRuleForm.content" value="editNoteRuleForm.content"></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="editDialogVisible = false">Cancel</el-button>
        <el-button type="primary" @click="saveEditedNote('editNoteRuleForm', editNoteRuleForm); editDialogVisible = false;">Confirm</el-button>
      </span>
    </el-dialog>

Onze data-hook (we hebben editDialogVisible en editNoteRuleForm toegevoegd) zou moeten zijn bijgewerkt naar:


  data() {
    return {
      tableData: [],
      addDialogVisible: false,
      addNoteRuleForm: {
        title: '',
        content: ''
      },
      editDialogVisible: false,
      editNoteRuleForm: {
        id: '',
        title: '',
        content: ''
      },
      rules: {
        title: [
          { required: true, message: "Note title can't be empty", trigger: 'blur' }
        ],
        content: [
          { required: true, message: "Note content can't be empty", trigger: 'blur' }
        ]
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  data() {
    return {
      tableData: [],
      addDialogVisible: false,
      addNoteRuleForm: {
        title: '',
        content: ''
      },
      editDialogVisible: false,
      editNoteRuleForm: {
        id: '',
        title: '',
        content: ''
      },
      rules: {
        title: [
          { required: true, message: "Note title can't be empty", trigger: 'blur' }
        ],
        content: [
          { required: true, message: "Note content can't be empty", trigger: 'blur' }
        ]
      }
    }
  }

We moeten daarnaast een editNote-methode toevoegen die de gegevens verzamelt op basis van de notitie-ID en de waarde van de editDialog-velden moet worden gevuld:

    editNote (noteId) {
      var self = this;
      this.editNoteRuleForm.title = '';
      this.editNoteRuleForm.content = '';
      db.collection('notes').doc(noteId).get().then(function(doc) {
        if (doc.exists) {
          self.editNoteRuleForm.id = doc.id;
          self.editNoteRuleForm.title = doc.data().title;
          self.editNoteRuleForm.content = doc.data().content;
        } else {
          console.log("No such document!");
        }
      }).catch(function(error) {
        console.log("Error getting document:", error);
      });
    }

Het ophalen van de notitie in onze bewerkingsdialoog is nu afgehandeld, maar we hebben de saveEditedNote-methode nog nodig:

    saveEditedNote (note, noteObj) {
      var self = this;
      var today = new Date().toLocaleString('en-GB');
      this.$refs[note].validate((valid) => {
        if (valid) {
          db.collection('notes').doc(noteObj.id).set({
            'title': noteObj.title,
            'content': noteObj.content,
            'date': today
          }).then(function() {
            self.getNotes();
            self.$message({
              type: 'success',
              message: 'Note successfully edited'
            });
          })
          .catch(function(error) {
            console.error("Error writing document: ", error);
          });
        }
      })
    }

We hebben hier ons databaserecord ingesteld volgens de nieuwe waarde die door onze gevalideerde formuliervelden zijn gespecificeerd, en de bijgewerkte datum is voltooid. Na het voltooien wordt het bericht 'Note successfully edited' ('Notitie bijgewerkt') getoond.

Dat was het! Het is ons gelukt om een eenvoudig CRUD-applicatie te maken op basis van VueJS en Firebase!

gif van persoon die klapt

Bonusstap: Verificatie

Ward vroeg me in onze vorige blog naar verificatiefunctionaliteit. Ik zal daarom een eenvoudige verificatiestap aan onze app toevoegen op basis van de verificatie-API die Firebase biedt.

Ga naar je Firebase-console en navigeer naar de link 'Authentication' in de zijbalk onder 'Develop'. Schakel de simpele e-mail-/wachtwoordverificatie in.

firebase overview

We kunnen nu de aanmeld- en registratiepagina's gaan bouwen. Dit zijn soortgelijke formulieren zoals we al eerder hebben behandeld. Het enige verschil is dat we in dit geval de methodes firebase.auth().createUserWithEmailAndPassword en firebase.auth().signInWithEmailAndPassword gebruiken.

Login.vue:

<template>
  <div class="login">
 
    <el-container>
      <el-main>
        <el-row :gutter="20">
          <el-col :span="12">
 
            <h1>Login to your account</h1>
            <el-form ref="form" :model="form">
              <el-form-item label="E-mail address">
                <el-input type="email" placeholder="E-mail" v-model="form.email"></el-input>
              </el-form-item>
              <el-form-item label="Password">
                <el-input type="password" placeholder="Password" v-model="form.password"></el-input>
              </el-form-item>
              <el-form-item>
                <el-button type="primary" v-on:click="login">Login</el-button>
              </el-form-item>
              <el-form-item>
                No account? <router-link to="/signup">Create one</router-link>
              </el-form-item>
            </el-form>
 
          </el-col>
        </el-row>
      </el-main>
    </el-container>
 
  </div>
</template>
 
<script>
import firebase from 'firebase'
 
export default {
  name: 'login',
  data: function () {
    return {
      form: {
        email: '',
        password: ''
      }
    }
  },
  methods: {
    login: function () {
      firebase.auth().signInWithEmailAndPassword(this.form.email, this.form.password).then(
        (user) => {
          this.$router.replace('/home')
          this.$notify({
            title: 'Wonderful!',
            message: 'You are now logged in.',
            type: 'success'
          })
        },
        (err) => {
          this.$message.error({
            message: 'Oops. ' + err.message
          })
        }
      )
    }
  }
}
</script>

SignUp.vue:

<template>
  <div class="sign-up">
 
    <el-container>
      <el-main>
        <el-row :gutter="20">
          <el-col :span="12">
 
            <h1>Sign up for a FireNotes account</h1>
            <el-form ref="form" :model="form">
              <el-form-item label="E-mail address">
                <el-input type="email" placeholder="E-mail" v-model="form.email"></el-input>
              </el-form-item>
              <el-form-item label="Password">
                <el-input type="password" placeholder="Password" v-model="form.password"></el-input>
              </el-form-item>
              <el-form-item>
                <el-button type="primary" v-on:click="signUp">Sign up</el-button>
              </el-form-item>
              <el-form-item>
                Already have an account? <router-link to="/login">Login</router-link>
              </el-form-item>
            </el-form>
 
          </el-col>
        </el-row>
      </el-main>
    </el-container>
 
  </div>
</template>
 
<script>
import firebase from 'firebase'
 
export default {
  name: 'SignUp',
  data: function () {
    return {
      form: {
        email: '',
        password: ''
      }
    }
  },
  methods: {
    signUp: function () {
      firebase.auth().createUserWithEmailAndPassword(this.form.email, this.form.password).then(
        (user) => {
          this.$router.replace('/home')
          this.$notify({
            title: 'Wonderful!',
            message: 'Your account has been created.',
            type: 'success'
          })
        },
        (err) => {
          this.$message.error({
            message: 'Oops. ' + err.message
          })
        }
      )
    }
  }
}
</script>

We kunnen deze weergaves nog steeds niet zien, omdat we onze routes nog moeten bijwerken. Ga naar main.js en vervang het door het volgende (vergeet je API-sleutel en project-ID niet):

import Vue from 'vue'
 
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
 
import firebase from 'firebase'
import 'firebase/firestore'
firebase.initializeApp({
  apiKey: 'yourApiKey',
  projectId: 'yourProjectId'
})
export const db = firebase.firestore()
const settings = { timestampsInSnapshots: true }
db.settings(settings)
 
import VueRouter from 'vue-router'
Vue.use(VueRouter)
 
import App from '@/App'
import HelloWorld from '@/components/HelloWorld'
import Notes from '@/components/Notes'
import Login from '@/components/Login'
import SignUp from '@/components/SignUp'
 
const routes = [
  {
    path: '*',
    redirect: '/login'
  },
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/signup',
    name: 'SignUp',
    component: SignUp
  },
  {
    path: '/home',
    name: 'HelloWorld',
    component: HelloWorld,
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/notes',
    name: 'Notes',
    component: Notes,
    meta: {
      requiresAuth: true
    }
  }
]
 
const router = new VueRouter({
  routes
})
 
router.beforeEach((to, from, next) => {
  const currentUser = firebase.auth().currentUser
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  if (requiresAuth && !currentUser) next('/login')
  else if (!requiresAuth && currentUser) next('/home')
  else next()
})
 
/* eslint-disable no-new */
firebase.auth().onAuthStateChanged(function (user) {
  new Vue({
    el: '#app',
    router: router,
    render: h => h(App),
    components: { App }
  })
})

We hebben hier enkele routes voor inloggen en registratie toegevoegd en we hebben meta: { requiresAuth: true; } toegevoegd aan de pagina's waarvoor beperkte toegang geldt. We controleren met Firebase of de gebruiker is geverifieerd in de functie router.beforeEach voordat registratie in onze app plaatsvindt. De controle is gereed als de verificatiestatus verandert.

Het functioneert nu vrij goed, maar we willen de navigatie in de zijbalk verbergen als de gebruiker niet is ingelogd. Dus we hebben de 'created' hook toegevoegd en de v-if bij <el-aside>. We hebben ook een uitlog-link toegevoegd aan onze navigatie. Ons App.Vue-bestand ziet er nu zo uit:

<template>
  <div id="app">
    <el-container>
      <el-header>
        <img class="logo" src="./assets/logo.png" />
      </el-header>
      <el-container>
        <el-aside width="300px" v-if="user">
          <el-menu
            default-active="home"
            :router="true"
            class="el-menu-vertical-demo">
            <el-menu-item index="home">
              <i class="el-icon-menu"></i>
              <span>Home</span>
            </el-menu-item>
            <el-menu-item index="notes">
              <i class="el-icon-document"></i>
              <span>Notes</span>
            </el-menu-item>
            <el-menu-item index="/logout" v-on:click="logout">
              <i class="el-icon-setting"></i>
              <span>Logout</span>
            </el-menu-item>
          </el-menu>
        </el-aside>
        <el-main>
          <router-view/>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
 
<script>
import firebase from 'firebase'
export default {
  name: 'App',
  created () {
    this.user = firebase.auth().currentUser || false;
  },
  methods: {
    logout: function () {
      firebase.auth().signOut().then(() => {
        this.$router.replace('/login')
      })
    }
  }
}
</script>
 
<style>
  html, body { margin: 0; }
  body {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    color: #2c3e50;
  }
  .el-header { border-bottom: 1px solid #e6e6e6; display: flex; align-items: center; width: 100%; }
  .el-header button { float: right; }
  .el-menu-item { border-bottom: 1px solid #e6e6e6; }
  .logo { max-width: 50%; max-height: 50%; margin-right: auto; }
</style>

Het eindresultaat is een notitieapplicatie bestaande uit één enkele pagina. Je kunt een account aanmaken en je kunt je aanmelden om notities te lezen, bij te werken en verwijderen.

Gebruik deze code of kennis als basis en pas naar wens aan of breid uit om je eigen VueJS-/Firebase-projecten te creëren. Laat me weten hoe het ging!

Vragen of opmerkingen?

Lukt het niet om het aan de gang te krijgen? Meer informatie nodig over bepaalde delen van de code? Laat het ons gerust weten.

Dorien Jorissen