神刀安全网

Building a Google Keep Clone with Vue and Firebase, Pt 3

In theprevious part we added the missing Update and Delete functionality, as well as refactoring our code to be more DRY. In this final part we are going to integrate the Vue-router to route between our authentication-page and our notes-page. As we add the ability to sign up and sign in, we are also going to introduce some Firebase security rules. With the security rules in place we’ll make sure that everyone’s notes are separate and private .We’ll also add a way to search through the notes. The application will look like this at the end of this part.

Building a Google Keep Clone with Vue and Firebase, Pt 3

But before all that, let’s create something to give more feedback to the user.

Alerts component

This component will accept an array of alerts through the alerts-property and visualize them at the top of the screen. Every alert consists out of a type (success, error, …) which will be visualized by a different color, and the actual message .

Create the Alerts-component at src/components/Alerts.vue . Tell the component to accept a alerts-property and iterate over the alerts in the template. By binding the type to the class, you can dynamically set the class. Depending on the class you can change the background color of the alert.

src/components/Alerts.vue

<template>   <div class="alerts">     <div v-for="alert in alerts" v-bind:class="alert.type" transition="expand">       <p>         {{alert.message}}       </p>     </div>   </div> </template> <script>   export default {     props: ['alerts']   } </script> <style>   .alerts{     position: fixed;     top: 0;     left: 0;     right: 0;     z-index: 1;   }   .alerts div{     background: #bbb;     overflow: hidden;   }   .alerts div.error{     background: #e03c3c;     color: #fff;   }   .alerts div.success{     background: #41b883;     color: #fff;   }   .alerts p{     width: 480px;     margin: 0 auto;     padding: 4px;     text-align: center;   }   .expand-transition{     max-height: 200px; /* height 0 -> auto is not animatable, so use max-height 0 -> large height */     transition: max-height 1s ease;   }   .expand-enter, .expand-leave{     max-height: 0;   } </style>

Add an expand transition that will animate the alerts as they are created or removed. The animation is very simple. You can’t hardcode the height since you don’t know how tall the alerts will be beforehand, so this should be set to auto (the initial value). You also can’t animate between auto and 0 to create an expanding/shrinking animation. So instead you can animate the max-height from a value that is certain to always be bigger than the height, and animate it to 0.

Now in App-component, add an empty alerts-array to the data. Listen to the alert-event and push them onto the alerts-array. By default set a timeout of 1500ms to remove the alert. Import the Alerts-component and put it at the top of the template. Don’t forget to bind the alerts in the viewmodel to the alerts-attribute.

src/App.vue

<template>   <div>     <alerts :alerts="alerts"></alerts>     ...   </div> </template> <script>   ...   import Alerts from './components/Alerts'   export default {     components: {       ...       Alerts     },     data () {       return {         selectedNote: null,         alerts: []       }     },     events: {       'alert': function (alert) {         this.alerts.push(alert)         setTimeout(() => {           this.alerts.$remove(alert)         }, alert.duration || 1500)       },       ...     }   } </script>

Now you have created a very reusable way to give feedback about operations to the user. There are actually quite a few instances where we should do this. Currently, you are throwing the exceptions instead of handling them in the callbacks of create, update, and delete. Replace those throws by firing an alert-event up the parent-chain. Optionally, you can also let the user know if the operation was successful, though they already get direct feedback by seeing the notes being created, updated, or deleted.

src/components/Create.vue

... export default {   ...   methods: {     createNote () {       if (this.title.trim() || this.content.trim()) {         noteRepository.create({title: this.title, content: this.content}, (err) => {           if (err) return this.$dispatch('alert', {type: 'error', message: 'Failed to create note'})           this.title = ''           this.content = ''           this.$dispatch('alert', {type: 'success', message: 'Note was successfully created'})         })       }     }   } }

src/components/Note.vue

... export default {   ...   methods: {     remove () {       noteRepository.remove(this.note, (err) => {         if (err) return this.$dispatch('alert', {type: 'error', message: 'Failed to remove note'})       })     }   } }

src/components/UpdateModal.vue

... export default {   ...   methods: {     remove () {       noteRepository.remove(this.note, (err) => {         if (err) return this.$dispatch('alert', {type: 'error', message: 'Failed to remove note'})         this.dismissModal()       })     },     update () {       noteRepository.update(this.note, (err) => {         if (err) return this.$dispatch('alert', {type: 'error', message: 'Failed to update note'})         this.dismissModal()         this.$dispatch('alert', {type: 'success', message: 'Note was successfully updated'})       })     },     ...   } }

Now to test out errors, you can simply revoke all write access in the Firebase rules. Go to the (legacy) console, click ‘Security and Rules’ and change the rules to the following.

{   "rules": {     ".read": true,     ".write": false   } }

When you test out create, edit, and delete, the error-alerts should appear. These action might cause strange behavior because they are being performed optimistically . This means that when you perform an operation, it will locally be executed without waiting for Firebase to check if the operation was successful in the database. If the operation was unsuccessful, it will undo the action. When you delete a note, Firebase will call ‘child_removed’, but when it fails in the database it will call ‘child_added’ again to undo the local operation. Revert the rules to the original values when you’re done testing out the alerts.

Now you have a simple reusable way to give feedback to the user. This will come in handy in the login/signup form.

Vue-router

Until now, there wasn’t a real need for routing, but to introduce the login/signup form which is a completely different "page," you’ll need a way to route between different views/pages. The vue-router library makes it very easy to route between views, and even nested views. (My favourite aspect of the vue-router is how easy it is to integrate authentication with routes)

First, install the vue-router library.

npm install vue-router --save

Second, setup the router in main.js and add one route for the NotesPage that you’ll need to make in a second. (when the list of routes gets long, it might be a better option to create a routes.js file that you load in main.js)

src/main.js

import Vue from 'vue' import VueRouter from 'vue-router' import App from './App' import NotesPage from './components/pages/Notes'  Vue.use(VueRouter)  let app = Vue.extend({   components: { App } })  let router = new VueRouter()  router.map({   '/notes': {     name: 'notes',     component: NotesPage   } })  router.alias({   '/': '/notes' })  router.start(app, 'body')

The vue-router library will use the <router-view> element to inject the correct view in the app. Add the <router-view> element to src/App.vue and move all the Notes related code to a new component under src/components/pages/Notes.vue . Your files should look like this.

src/App.vue

<template>   <div>     <alerts :alerts="alerts"></alerts>     <router-view></router-view>   </div> </template> <script>   import Alerts from './components/Alerts'   export default {     components: {       Alerts     },     data () {       return {         alerts: []       }     },     events: {       'alert': function (alert) {         this.alerts.push(alert)         setTimeout(() => {           this.alerts.$remove(alert)         }, alert.duration || 1500)       }     }   } </script> <style>   *{     padding: 0;     margin: 0;     box-sizing: border-box;   }   html{     font-family: sans-serif;   }   body{     padding: 80px 16px;     background: #eee;   }   .clearfix:after {     content: "";     display: table;     clear: both;   } </style>

src/components/pages/Notes.vue

<template>   <div>     <create-note-form></create-note-form>     <notes></notes>     <update-modal :note.sync="selectedNote"></update-modal>   </div> </template> <script>   import Notes from 'src/components/notes/Index'   import CreateNoteForm from 'src/components/notes/Create'   import UpdateModal from 'src/components/notes/UpdateModal'   export default {     components: {       Notes,       CreateNoteForm,       UpdateModal     },     data () {       return {         selectedNote: null       }     },     events: {       'note.selected': function (note) {         this.selectedNote = note       }     }   } </script>

That should be all you need to do to get the router working. When you browse to ‘#!/ or ‘#!/notes’ everything should be working as before. Though the router isn’t really useful for only one page. Let’s make the page for authorization!

Auth page

The authorization page will offer a form for the user to sign up or sign in. The user will be able to sign up with email + password or via any of the other third-party providers (Facebook, Google, etc.). The form will look like this (the yellow background is because of Chrome’s autofill).

Building a Google Keep Clone with Vue and Firebase, Pt 3

For now, simply create a form that asks for an email and password without implementing any of the logic.

src/components/pages/Auth.vue

<template>   <form class="auth-form" v-on:submit.prevent="wantsToSignUp ? signUpWithPassword() : signInWithPassword()">     <h1>{{wantsToSignUp ? 'Sign up' : 'Sign in'}}</h1>     <div>       <label for="email">Email</label>       <input type="email" name="email" id="email" placeholder="Email" required v-model="email">     </div>     <div>       <label for="password">Password</label>       <input type="password" name="password" id="password" required v-model="password">     </div>     <div v-show="wantsToSignUp">       <label for="confirm-password">Confirm Password</label>       <input type="password" name="confirm-password" id="confirm-password" v-model="confirmPassword">     </div>     <div v-show="!wantsToSignUp" class="clearfix btn-group">       <button type="submit">Sign in</button>       <button type="button" v-on:click="wantsToSignUp = true">Sign up</button>     </div>     <div v-show="wantsToSignUp">       <button type="submit" class="signup-submit">Sign up</button>     </div>     <hr>     <div class="social-providers">       <a href="#"><i class="fa fa-facebook-square" aria-hidden="true"></i></a>       <a href="#"><i class="fa fa-twitter-square" aria-hidden="true"></i></a>       <a href="#"><i class="fa fa-google-plus-square" aria-hidden="true"></i></a>       <a href="#"><i class="fa fa-github-square" aria-hidden="true"></i></a>     </div>   </form> </template> <script>   export default {     data () {       return {         email: '',         password: '',         confirmPassword: '',         wantsToSignUp: false       }     }   } </script> <style>   .auth-form{     width: 480px;     max-width: 100%;     margin: 25vh auto 15px;     background: #fff;     padding: 15px;     border-radius: 2px;     box-shadow: 0 1px 5px #ccc;   }   .auth-form h1{     font-weight: 300;   }   .auth-form > div {     margin-top: 15px;   }   .auth-form input {     height: 32px;     border: none;     border-bottom: 2px solid #bbb;   }   .auth-form input:focus{     border-bottom-color: #555;   }   .auth-form label, .auth-form input{     display: block;     width: 100%;   }   .auth-form button {     font-size: 18px;     background: #fff;     border: 1px solid #41b883;     padding: 4px 6px;     margin: 0;     border-radius: 3px;   }   .auth-form .btn-group button{     border-radius: 3px 0 0 3px;     width: 50%;     float: left;   }   .auth-form .btn-group button + button{     border-radius: 0 3px 3px 0;     border-left: none;   }   .auth-form .signup-submit{     width: 100%;   }   .auth-form hr{     margin-top: 20px;   }   .auth-form .social-providers{     text-align: center;   }   .auth-form .social-providers a{     color: #41b883;     font-size: 36px;     padding: 4px;   } </style>

The confirm-password field and register submit button is only shown when ‘wantsToSignUp’ is true. So when the user clicks the register button, it will set it to true and a confirm password field shows up and the register button takes up the full width of the card.

Now that you have a second page, you can bind it to a new route in main.js .

src/main.js

import Vue from 'vue' import VueRouter from 'vue-router' import App from './App' import NotesPage from './components/pages/Notes' import AuthPage from './components/pages/Auth'  Vue.use(VueRouter)  let app = Vue.extend({   components: { App } })  let router = new VueRouter()  router.map({   '/notes': {     name: 'notes',     component: NotesPage   },   '/auth': {     name: 'auth',     component: AuthPage   } })  router.alias({   '/': '/notes' })  router.start(app, 'body')

Now you should be able to navigate between the #!/notes and #!/auth . Because the Notes-component (Index.vue) isn’t always loaded immediately anymore, you’ll have to update our code a little. In previous parts we assumed the NoteRepository would be constructed and used immediately. Due to the changes, the existing notes will not trigger the ‘added’-event in Index.vue. Therefore you need to call attachFirebaseListeners in the ready-method of the Notes-component and remove it from the NoteRepository component. It should look like this.

src/data/NoteRepository.js

class NoteRepository extends EventEmitter {   constructor () {     super()     // firebase reference to the notes     this.ref = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/notes')    } }

src/components/notes/Index.vue

... export default {   ...   ready () {     this.masonry = new Masonry(this.$els.notes, {       itemSelector: '.note',       columnWidth: 240,       gutter: 16,       fitWidth: true     })     noteRepository.on('added', (note) => {       this.notes.unshift(note) // add the note to the beginning of the array     })     noteRepository.on('changed', ({key, title, content}) => {       let note = noteRepository.find(this.notes, key) // get specific note from the notes in our VM by key       note.title = title       note.content = content     })     noteRepository.on('removed', ({key}) => {       let note = noteRepository.find(this.notes, key) // get specific note from the notes in our VM by key       this.notes.$remove(note) // remove note from notes array     })     noteRepository.attachFirebaseListeners()   } }

Now everything should be working fine, even after coming from a different route. Next up is creating an Authentication module and wiring it up to the authentication page.

Authentication

Just like with the NotesRepository, it’s a good idea to keep your Firebase code contained in a seperate module instead of directly interacting with Firebase inside your components. Create a module for Authentication under src/data/Auth.js . This module will be responsible for:

  • offering callbacks to listen in to changes to the authenticated user
  • offering information about the authenticated user
  • logging in and registering with
    • Email + Password
    • Social provider like Facebook, Twitter, etc.
  • logging users out

Fortunately, Firebase makes it really easy by handling all the hard work for you.

src/data/Auth.js

import Firebase from 'firebase'  export default {   ref: new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/'),   // calls callback when user signs in or out   onAuth (authCallback) {     this.ref.onAuth(authCallback)   },   // get's authenticated user   getAuth () {     return this.ref.getAuth()   },   signInWithPassword (credentials) {     return this.ref.authWithPassword(credentials)   },   signUpWithPassword (credentials) {     return this.ref.createUser(credentials) // this will create a Firebase user for authentication, this is separate from our own user objects   },   signInWithProvider (provider, callback) {     // provider => 'google', 'facebook', 'github', etc.     this.ref.authWithOAuthPopup(provider, (error, authData) => {       if (error) {         if (error.code === 'TRANSPORT_UNAVAILABLE') {           // fall-back to browser redirects, and pick up the session           // automatically when we come back to the origin page           this.ref.authWithOAuthRedirect(provider, (error) => {             if (callback) callback(error, authData)           })         }       } else if (authData) {         if (callback) callback(null, authData)       }     })   },   signOut () {     this.ref.unauth()   } }

Now wire it up to your Auth page. Bind the signInWithPassword() method to the signin-button and the signInWithPassword() method with the second register-button. In case of an error, you can simply pass on the error-message that Firebase provides to the alerts. If you need to internationalize your app, you can use the error-code to map to your own translated messages.

src/components/pages/Auth.vue

<template>   <form class="auth-form" v-on:submit.prevent="wantsToSignUp ? signUpWithPassword() : signInWithPassword()">     <h1>{{wantsToSignUp ? 'Sign up' : 'Sign in'}}</h1>     <div>       <label for="email">Email</label>       <input type="email" name="email" id="email" placeholder="Email" required v-model="email">     </div>     <div>       <label for="password">Password</label>       <input type="password" name="password" id="password" required v-model="password">     </div>     <div v-show="wantsToSignUp">       <label for="confirm-password">Confirm Password</label>       <input type="password" name="confirm-password" id="confirm-password" v-model="confirmPassword">     </div>     <div v-show="!wantsToSignUp" class="clearfix btn-group">       <button type="submit">Sign in</button>       <button type="button" v-on:click="wantsToSignUp = true">Sign up</button>     </div>     <div v-show="wantsToSignUp">       <button type="submit" class="signup-submit">Sign up</button>     </div>     <hr>     <div class="social-providers">       <a href="#" v-on:click.prevent="signInWithProvider('facebook')"><i class="fa fa-facebook-square" aria-hidden="true"></i></a>       <a href="#" v-on:click.prevent="signInWithProvider('twitter')"><i class="fa fa-twitter-square" aria-hidden="true"></i></a>       <a href="#" v-on:click.prevent="signInWithProvider('google')"><i class="fa fa-google-plus-square" aria-hidden="true"></i></a>       <a href="#" v-on:click.prevent="signInWithProvider('github')"><i class="fa fa-github-square" aria-hidden="true"></i></a>     </div>   </form> </template> <script> import Auth from 'src/data/Auth' export default {   data () {     return {       email: '',       password: '',       confirmPassword: '',       wantsToSignUp: false     }   },   methods: {     signUpWithPassword () {       if (this.password === this.confirmPassword) {         Auth.signUpWithPassword({           email: this.email,           password: this.password         })           .then((userData) => this.signInWithPassword())                                              // signIn           .then(() => this.$dispatch('alert', {type: 'success', message: 'Signed up successfully'}))  // let user know everything was successful           .catch((error) => this.$dispatch('alert', {type: 'error', message: error.message}))         // tell the user an error occurred       }     },     signInWithPassword () {       return Auth.signInWithPassword({         email: this.email,         password: this.password       })         .then((userData) => {           this.$dispatch('alert', {type: 'success', message: 'Signed in successfully'})           this.onSignedIn()           return userData         })         .catch((error) => this.$dispatch('alert', {type: 'error', message: error.message})) // tell the user an error occurred     },     signInWithProvider (provider) {       Auth.signInWithProvider(provider, (error, authData) => {         if (error) this.$dispatch('alert', {type: 'error', message: error.message})         this.onSignedIn()       })     },     onSignedIn () {       this.$router.go({name: 'notes'})     }   } } </script>

To take advantage of the Email + Password authentication, you need to enable it in the Firebase console. Go to the ‘Email & Password’ section under ‘Login & Auth’, and there enable the checkbox. Now you should be able to register and login. When you register, you will see them populate in the Firebase console. You can also reset a user’s password or even delete the user from the Firebase console.

Building a Google Keep Clone with Vue and Firebase, Pt 3

When users are not authenticated, they should not be able to access the notes-page, but currently that’s still possible. Let’s take care of that. Set the auth: true to the notes-route in src/main.js . To enforce authentication, you can attach a callback that gets called before a route changes. Check if you’re dealing with an authenticated route, if so, check if the user is authenticated. Redirect the user to the auth-page if he is not authenticated. Otherwise you can just call next to complete the routing change.

src/main.js

... import Auth from './data/Auth' ... router.map({   '/notes': {     name: 'notes',     component: NotesPage,     auth: true // this route requires the user to be signed in   },   '/auth': {     name: 'auth',     component: AuthPage   } })  router.alias({   '/': '/notes' })  router.beforeEach((transition) => {   if (transition.to.auth && !Auth.getAuth()) {     transition.redirect('/auth')   } else {     transition.next()   } }) ...

When you browse to the notes-page without being authenticated, you will be redirected to the authentication-page. Now you can register and login. After you are logged in, you are automatically redirected to the notes-page!

Third party providers

For every provider icon, you can just pass the provider name to signInWithProvider() . Though, to get these providers to work, you need to perform some extra actions in the Firebase console too. I’ll go over how to do it with Twitter, but it’s pretty similar for other providers. Visit the Auth docs to learn more about how to implement the other providers.

Go to the Twitter section under ‘Login & Auth’ in the Firebase console and enable the checkbox. There is a link provided on the side that will give you more information on how to get an api key and secret.

Building a Google Keep Clone with Vue and Firebase, Pt 3

Go the Twitter Apps website and click the create application button. Give the application a name, description, and fill in your Firebase hosting link as website. The most important part is setting the callback url to the following url: https://auth.firebase.com/v2/<YOUR-FIREBASE-APP>/auth/twitter/callback .

Once you created the app in Twitter, navigate to the ‘Keys and access tokens’. There you will find your api key and secret.

Building a Google Keep Clone with Vue and Firebase, Pt 3

After copying over the key and secret to the Firebase console, your users will be able to sign in using Twitter! Visit the Auth docs to learn more about how to implement the other providers.

Now the users can sign in and sign up with email and password, or sign in with Twitter! Awesome job! But how does the user logout, and see what account he logged in with? Let’s build that header you saw earlier.

HeaderBar

The header-bar will be responsible for 3 things:

  1. Providing some information about the authenticated user (name, email, picture)
  2. Giving the user a way to sign out of the app
  3. A generic search bar where other parts of the app can listen to. The notes-component will react to the search by filtering its’ notes.

Create a new component under src/components/HeaderBar.vue .

The reason that I’m calling this component HeaderBar instead of Header , is because <header> is an existing HTML5 element and we want to prevent collision between the two.

In the ready-method, check if the user is signed in and listen to any authentication changes.

Get the user info and for each provider, grab the provider specific information, and show it to the user on the right of the header. Luckily, Firebase is very consistent in the information it provides for each 3rd party provider. Rest assured, there is always a displayName and profileImageURL property.

Only the Password-provider doesn’t have a displayName . In that case you can grab the email address instead. If the user uses Password authentication, then his Gravatar image will be used, but if he doesn’t have a Gravatar, Firebase provides a default image.

Next to the users info, you can use another icon from FontAwesome to create a SignOut-button. Bind a method to this button and use Auth.signOut() method. Redirect the user back to the authentication page after that.

Use an input for the searchbar and center it. Bind it to searchQuery and listen to any changes. When the query changes, fire an event upwards with the query. To prevent the event from being fired too often, you can use the debounce attribute. This is a Vue feature that will prevent the binding from updating on every keyup, but instead will wait to see if there’s another change coming within the timespan you provide.

src/components/HeaderBar.vue

<template>   <header v-if="user">     <input placeholder="Search" v-model="searchQuery" debounce="500">     <div>       <span>{{user.userTitle}}</span>       <img :src="user.imageUrl" alt="{{user.userTitle}}"/>       <a href="#" v-on:click.prevent="signOut">         <i class="fa fa-sign-out" aria-hidden="true"></i>       </a>     </div>   </header> </template> <script>   import Auth from 'src/data/Auth'   export default {     data () {       return {         user: null,         searchQuery: ''       }     },     watch: {       'searchQuery': function () {         this.$dispatch('search', this.searchQuery)       }     },     methods: {       processUser (authed) {         if (authed === null) {           this.user = null           return         }         this.user = {           userTitle: authed[authed.provider].displayName || authed[authed.provider].email || '', // if there's no displayName, take the email, if there's no email, use an empty string           imageUrl: authed[authed.provider].profileImageURL         }       },       signOut () {         Auth.signOut()         this.$router.go('auth')       }     },     ready () {       Auth.onAuth(this.processUser) // processUser everytime auth state changes (signs in or out)       this.processUser(Auth.getAuth()) // processUser in case user is already signed in     }   } </script> <style>   header{     position: fixed;     left: 0;     top: 0;     right: 0;     z-index: 1;     height: 50px;     background: #333;     padding: 10px;     box-shadow: 0 2px 5px rgba(0,0,0,.4);   }   header input {     display: block;     width: 480px;     margin: 0 auto;     height: 30px;     border: none;     padding: 0 16px;     border-radius: 2px;   }   header span {     padding: 15px;     color: #fff;     position: absolute;     right: 95px;     top: 1px;   }   header img {     width: 35px;     height: 35px;     border-radius: 20px;     position: absolute;     right: 60px;     top: 8px;   }   header a {     position: absolute;     display: block;     color: #fff;     right: 15px;     top: 10px;     font-size: 25px;     cursor: pointer;     transition: color .2s;   }   header a:focus, header a:hover {     color: #41b883;   }   @media screen and (max-width: 1200px) {     header span{       display: none;     }   }   @media screen and (max-width: 720px) {     header input{       width: calc(100% - 64px);       margin: 0 0 0 16px;     }     header span, header img {       display: none;     }   } </style>

Now include the HeaderBar into src/App.vue and you should have a nice header when you are logged in. Also listen to the ‘search’-event and propagate it downwards so the Notes-component can listen to it too.

src/App.vue

<template>   <div>     <header-bar></header-bar>     <alerts :alerts="alerts"></alerts>     <router-view></router-view>   </div> </template> <script>   import Alerts from './components/Alerts'   import HeaderBar from './components/HeaderBar'   export default {     components: {       Alerts,       HeaderBar     },     data () {       return {         alerts: []       }     },     events: {       'alert': function (alert) {         this.alerts.push(alert)         setTimeout(() => {           this.alerts.$remove(alert)         }, alert.duration || 1500)       },       'search': function (searchText) {         this.$broadcast('search', searchText) // send the event downwards to children       }     }   } </script>

When the user receives an alert, it will show up on top of the header. Let’s change the position of the alerts underneath the header.

src/components/Alerts.vue

.alerts{   position: fixed;   top: 50px;   left: 0;   right: 0;   z-index: 1; }

Now the alerts show up nicely underneath the header.

Building a Google Keep Clone with Vue and Firebase, Pt 3

Lastly, listen to the ‘search’-event in src/components/notes/Index.vue and bind it to a new data property searchQuery . For filtering you can take advantage of an amazing Vue feature (one of my favourite) called computed properties. Instead of normal property, with computed properties, you can write functions and still use them as properties inside the Vue instance. If you are using any variables from outside the function, Vue will detect any changes and recalculate your computed property automatically!

Create a computed property filteredNotes() and use the filter-function to filter through the notes. If the searchQuery is found in either the title or content, than that note should be part of the filteredNotes. If the query is just a simple string, all notes should be part of the filteredNotes. Now you can watch the filteredNotes and iterate over the filteredNotes in the template.

src/components/notes/Index.vue

<template>   <div class="notes" v-el:notes>     <note       v-for="note in filteredNotes"       :note="note"       v-on:click="selectNote(note)"       >     </note>   </div> </template> <script>   import Masonry from 'masonry-layout'   import Note from './Note'   import noteRepository from '../../data/NoteRepository'   export default {     components: {       Note     },     data () {       return {         notes: [],         searchQuery: ''       }     },     methods: {       selectNote ({key, title, content}) {         // notify listeners that user selected a note         // pass in a copy of the note to prevent edits on the original note in the array         this.$dispatch('note.selected', {key, title, content})       }     },     computed: {       filteredNotes () {         return this.notes.filter((note) => {           if (this.searchQuery) return (note.title.indexOf(this.searchQuery) !== -1 || note.content.indexOf(this.searchQuery) !== -1) // returns truthy if query is found in title or content           return true         })       }     },     watch: {       'filteredNotes': { // watch the notes array for changes         handler () {           this.$nextTick(() => {             this.masonry.reloadItems()             this.masonry.layout()           })         },         deep: true // we also want to watch changed inside individual notes       }     },     events: {       'search': function (searchQuery) {         this.searchQuery = searchQuery       }     },     ...   } </script>

If the user types into the searchbar, the notes will be filtered! Sweet!Currently, every user still shares one set of notes, so let’s head over the final part and secure those notes!

Securing the notes

At the moment, every user can see everyone’s notes, but users should only see their own notes. So instead of sharing one big set of notes, update the NotesRepository to have a set of notes for every user. Every user will have it’s own user object inside the users object, and inside the user object you can put the set of notes.

Inside src/data/NoteRepository create a getter for the uid and a getter for the new notesRef that will dynamically return a reference to the currently authenticated user’s notes. Next update the code to use the notesRef instead.

src/data/NoteRepository.js

import Firebase from 'firebase' import EventEmitter from 'events'  // extend EventEmitter so user of NoteRepository can react to our own defined events (ex: noteRepository.on('added')) class NoteRepository extends EventEmitter {   get uid () {     return this.ref.getAuth().uid   }   get notesRef () {     return this.ref.child(`users/${this.uid}/notes`)   }   constructor () {     super()     // firebase reference to the notes     this.ref = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com') // will have same result as new Firebase('https://resplendent-heat-896.firebaseio.com/').child('notes')   }   // creates a note   create ({title = '', content = ''}, onComplete) {     this.notesRef.push({title, content}, onComplete)   }   // updates a note   update ({key, title = '', content = ''}, onComplete) {     this.notesRef.child(key).update({title, content}, onComplete) // key is used to find the child, a new note object is made without the key, to prevent key being inserted in Firebase     // new Firebase(`https://<YOUR-FIREBASE-APP>.firebaseio.com/notes/${key}`).update(...)   }   // removes a note   remove ({key}, onComplete) {     this.notesRef.child(key).remove(onComplete)   }   // attach listeners to Firebase   attachFirebaseListeners () {     this.notesRef.on('child_added', this.onAdded, this)     this.notesRef.on('child_removed', this.onRemoved, this)     this.notesRef.on('child_changed', this.onChanged, this)   }   // dettach listeners from Firebase   detachFirebaseListeners () {     this.notesRef.off('child_added', this.onAdded, this)     this.notesRef.off('child_removed', this.onRemoved, this)     this.notesRef.off('child_changed', this.onChanged, this)   }   onAdded (snapshot) {     // process data     let note = this.snapshotToNote(snapshot)     // propagate event outwards with note     this.emit('added', note)   }   onRemoved (oldSnapshot) {     let note = this.snapshotToNote(oldSnapshot)     this.emit('removed', note)   }   onChanged (snapshot) {     let note = this.snapshotToNote(snapshot)     this.emit('changed', note)   }   // processes the snapshots to consistent note with key   snapshotToNote (snapshot) {     // we will need the key often, so we always want to have the key included in the note     let key = snapshot.key()     let note = snapshot.val()     note.key = key     return note   }   // Finds the index of the note inside the array by looking for its key   findIndex (notes, key) {     return notes.findIndex(note => note.key === key)   }   // Finds the note inside the array by looking for its key   find (notes, key) {     return notes.find(note => note.key === key)   } } export default new NoteRepository() // this instance will be shared across imports

Now this works great, but other users are still able to access the data of other users. Introduce a security rule that only the authenticated user can read and write from his own user object. Go to the ‘Security & Rules’ section in the Firebase console and write the following rules.

{   "rules": {     "users": {       "$uid": {         ".read": "$uid === auth.uid",         ".write": "$uid === auth.uid"       }     }   } }

By prefixing the uid with a dollar sign, you can use ‘$uid’ as a variable which will be set to the key of every user object. Then in the read, write rules, check if the uid of the accessing user matches the $uid, if so allow the operation. And that’s all you need to do to secure the notes!

Wrapping up

In this last part, we secured the app and provided multiple ways for user to authenticate to use the app. During the course of this series, we familiarized ourselves with the many features Vue and Firebase gives us. Firebase really did all the heavy lifting and provided us with a very easy to use api to authenticate the users!

I hope the series was helpful and that you’ll give both Vue and Firebase a try for your next project. Don’t hesitate to let me know about it!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Building a Google Keep Clone with Vue and Firebase, Pt 3

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址