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

Building a Google Keep clone with Vue and Firebase, Pt 1

Free Course

Build Your First Node.js Website

Node is a powerful tool to get JavaScript on the server. Use Node to build a great website.

In this tutorial, you will learn how to create a (minimal) Google Keep clone app with the Model-View-Viewmodel (MVVM) framework Vue and Firebase as a backend.

Vue introduces easy to use building blocks in the form of components.

Each component has its own ViewModel and can be composed of other components. What sets Vue apart from Angular is that Vue has a less steep learning curve. It focuses much more on providing an easy way to compose components and nothing more.

Firebase is a realtime NoSQL database in the cloud by Google. Firebase offers official libraries for Android, iOS, JavaScript, and even Angular. With Firebase you can build real-time apps very quickly!

For this tutorial, you will need some experience with ES6 aka ES2015, Node, and a little Vue.

You must have NodeJS installed on your device.

There are two tools that may be helpful during this tutorial:

  1. Vue.js Devtools: This plugin lets you explore the component tree and watch any values bound to the ViewModel inside the Chrome DevTools
  2. Vulcan by Firebase: Vulcan lets you explore your data inside your DevTools so you don't need to adjust data without switching tabs

At the end of this part our app will look like this:

Demo

You can find the source code at Github (tag part_1) and play with the demo here.

Setup

  1. Install Vue Command Line globally (Vue is at version 1.0.18 at the time of writing this)
  2. Create a project with the vue-cli (use the default options)
  3. Install packages
  4. Run dev server
npm install -g vue-cli
vue init webpack gkeep-vueifire
cd gkeep-vueifire
npm install
npm run dev

When you visit localhost:8080 you should see the following screen:
Vue initial setup

Project Structure

  • index.html: root HTML-file. Your JavaScript and CSS are automatically injected into this file
  • config.js: config file (you can change your port here, add proxies to proxy to our own API, etc.)
  • build: contains node files to run and build your app (‘npm run dev’ runs ‘node build/dev-server.js’)
  • static: public folder containing static assets needed for your project
  • test: for your unit, integration, and tests

The starting point of the app is ‘src/main.js’.

// src/main.js
import Vue from 'vue' // import Vue the ES6 way
import App from './App' // import App.vue component

/* eslint-disable no-new */
new Vue({ // new Vue instance
  el: 'body', // attach Vue to the body
  components: { App } // include App component globally
})

When you use the App component like this <app></app>, it will replace the app element with the template provided in App.vue. Also, the JavaScript and styles will be injected into the view automagically. Currently, the App component is referenced in 'index.html'.

With .vue files you can write HTML, CSS, and JS all in the same file. This makes it very easy to develop with a component-mindset.

Note that you can write ES6 code inside the script block without worrying about browser compatibility! Webpack will use Babel to compile ES6 code to ES5. You can also add support for any other loaders like TypeScript, CoffeeScript, Sass, Less, etc.

If you followed the default when creating your project, eslint will be enabled by default. This will force us to follow the standard code styleguide. Make sure to pay attention to use 2 spaces and no semi-colons!  

Hooking up Firebase

Log into Firebase and create a new app. You can create an app for free for development purposes.

You will need the app-URL in one of the next steps.

Firebase

Install firebase through npm. (At the time of writing this, Firebase was version 2.4.2, and I used the now legacy console)

npm install firebase@2.4.2 --save

Import Firebase at the top of main.js.

Next, pass the Firebase app link to a new Firebase instance.

import Vue from 'vue'
import App from './App'
import Firebase from 'firebase'

let firebase = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/')
// ...

Now you can start adding, modifying and deleting data with Firebase. Make a notes array and add one note object to test if it is working.

import Vue from 'vue'
import App from './App'
import Firebase from 'firebase'

let firebase = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/')

firebase.child('notes').set([
  {title: 'Hello world', content: 'Lorem ipsum'}
])

firebase.child('notes').on('value', (snapshot) => {
  let notes = snapshot.val()
  console.log(notes)
  window.alert(notes[0].title)
})
// ...

When you look in the browser, you should get a popup saying ‘hello world’.

You just hooked up Firebase to your Vue-app. Next up is creating your first component!

Creating your first component

We will now create a component to present all notes. First, create a new folder ‘src/components/notes’ to put all note-related components in. Create an index.vue file inside the folder you just created and copy the following content to it.

src/components/notes/Index.vue

<template>
  <!-- add html here -->
</template>
<script>
// add js here
export default {}
</script>
<style>
/* add css here */
</style>

This is the barebones for a Vue component. Note that the template, script, and style can be omitted if you do not need them. The Index component will hold all notes.

First, add the data method to make Vue aware of the notes array.

src/components/notes/Index.vue

export default {
  data () {
    return {
      notes: []
    }
  }
}

Now remove the Firebase code from main.js and import Firebase in the component. Create a new Firebase-instance and listen to the child_added-event for your notes in the ready-method. The child_added-event will first iterate over all the existing notes in the array, and also immediately trigger when someone adds a note to the array. This means when somebody else adds a note, you will instantly be notified through the event.

Grab the data in the callback and add it onto the notes array with the unshift method. Instead of pushing an element onto the end of the array, unshift will add the element to the start. This makes sure that the most recent note is displayed first.

src/components/notes/Index.vue

import Firebase from 'firebase'
export default {
  data () {
    return {
      notes: []
    }
  },
  ready () {
    let firebase = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/')
    firebase.child('notes').on('child_added', (snapshot) => {
      let note = snapshot.val()
      this.notes.unshift(note)
    })
  }
}

Now add a simple template that iterates (v-for) over the notes and prints out the data in JSON-format using the json filter.

src/components/notes/Index.vue

<template>
  <ol>
    <li v-for="note in notes">
      <pre>
        {{note | json}}
      </pre>
    </li>
  </ol>
</template>

When we check the browser, there's no difference. That’s because we aren’t using the component yet.

Replace everything under div#app in App.vue with a notes element.

Also, import the Notes component and pass it to the components property of the Vue instance. (You can delete the Hello.vue file)

src/components/App.vue

<template>
  <div id="app">
    <notes></notes>
  </div>
</template>
<script>
  import Notes from './components/notes/Index'
  export default {
    components: {
      Notes
    }
  }
</script>
<style>
</style>

If you reload the browser now, the note that was added previously in main.js is outputted in JSON-format. If no note is appearing, try adding a note manually on the Firebase website.

Creating a second, more appealing component

Now that you can render all the notes, create a component for an individual note.

Create the file src/components/notes/Note.vue

By adding strings to the props property, you can define custom attributes/properties for the component.

Define an attribute note for the component. Now you can simply pass the note object through thenote attribute.

Note that we are using a pre-tag instead of a paragraph. The pre-tag is used for preformatted text. This tag will respect '\t\n' characters that come from the textarea. Though the sentences don't get broken automatically and overflow the width. With some CSS the pre-tag has same behaviour that other elements have.

src/components/notes/Note.vue

<template>
  <div class="note">
    <h1>{{note.title}}</h1>
    <pre>{{note.content}}</pre>
  </div>
</template>

<script>
  export default {
    props: ['note']
    // here you define the attributes/props of the component, will be available via this.note
    // when using component you can use the prop externally via '<note :note"{title: 'hi', content: 'lorem'}" ></note>'
  }
</script>

<style>
  .note{
    background: #fff;
    border-radius: 2px;
    box-shadow: 0 2px 5px #ccc;
    padding: 10px;
    width: 240px;
    margin: 16px;
    float: left;
  }
  .note h1{
    font-size: 1.1em;
    margin-bottom: 6px;
  }
  .note pre {
    font-size: 1.1em;
    margin-bottom: 10px;
    white-space: pre-wrap; 
    word-wrap: break-word;
    font-family: inherit;
  }
</style>

Now back in src/components/notes/Index.vue, you need to change the template to use the Note component. In the script you also need to import the Note component and pass it to the components object.

src/components/notes/Index.vue

<template>
  <div class="notes">
      <note
        v-for="note in notes"
        :note="note"
        >
      </note>
  </div>
</template>

<script>
  import Firebase from 'firebase'
  import Note from './Note'
  export default {
    components: {
      Note
    },
    …
  }
</script>

<style>
  .notes{
    padding: 0 100px;
  }
</style>

When you prefix an attribute with a colon like :note the content of the attribute will be interpreted as JavaScript. This way you can pass the note object to the Note component.

Now add some style to the App component at src/App.vue.

src/components/App.vue

<template>
  <div>
    <notes></notes>
  </div>
</template>

<script>
  import Notes from './components/notes/Index'
  export default {
    components: {
      Notes
    }
  }
</script>

<style>
  *{
    padding: 0;
    margin: 0;
    box-sizing: border-box;
  }
  html{
    font-family: sans-serif;
  }
  body{
    background: #eee;
    padding: 0 16px;
  }
</style>

Awesome! You just nested two components into each other passing data from the parent component to the child components.

Next up is creating a form to make new notes.

Creating a form for adding notes

By now it probably doesn’t come as a surprise that you again are going to create a component for this (component-ception!).

Create a new file: src/components/notes/Create.vue

Inside the new vue-file, create a form in the template and return an object with two properties (title and content) in the 'data'-method. Bind the data to the input and textarea using the ‘v-model’-directive.

Create a method called ‘createNote’ in the methods object and bind it to the submit-event of the form. Inside the method check if either the title or the content has been filled out. If so, create a new note and push it to the array through Firebase. Also pass a callback to reset the form when the note has been pushed to Firebase successfully.

Using the v-on:submit.prevent="createNote()" you can trigger the ‘createNote’-method when the form is being submitted. The ‘.prevent’ is optional and will prevent the default submit behavior just like event.preventDefault().

src/components/notes/Create.vue

<template>
  <form class="create-note" v-on:submit.prevent="createNote()">
    <input name="title" v-model="title" placeholder="Title"/>
    <textarea name="content" v-model="content" placeholder="Text goes here..." rows="3">
    </textarea>
    <button type="submit">+</button>
  </form>
</template>

<script>
  import Firebase from 'firebase'
  let firebase = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/')

  export default {
    data () {
      return {
        title: '',
        content: ''
      }
    },
    methods: {
      createNote () {
        if (this.title.trim() || this.content.trim()) {
          firebase.child('notes').push({title: this.title, content: this.content}, (err) => {
            if (err) {
              throw err
            }
            this.title = ''
            this.content = ''
          })
        }
      }
    }
  }
</script>

<style>
  form.create-note{
    position: relative;
    width: 480px;
    margin: 15px auto;
    background: #fff;
    padding: 15px;
    border-radius: 2px;
    box-shadow: 0 1px 5px #ccc;
  }
  form.create-note input, form.create-note textarea{
    width: 100%;
    border: none;
    padding: 4px;
    outline: none;
    font-size: 1.2em;
  }
  form.create-note button{
    position: absolute;
    right: 18px;
    bottom: -18px;
    background: #41b883;
    color: #fff;
    border: none;
    border-radius: 50%;
    width: 36px;
    height: 36px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
    cursor: pointer;
    outline: none;
  }
</style>

Now don't forget to import and use the component in App.vue.

<template>
  <div>
    <create-note-form></create-note-form>
    <notes></notes>
  </div>
</template>

<script>
  import Notes from './components/notes/Index'
  import CreateNoteForm from './components/notes/Create'
  export default {
    components: {
      Notes,
      CreateNoteForm
    }
  }
</script>
...

Now you are able to add notes, and they will automatically be inserted through the child_added-event.

The app should look something like this now. The notes get laid out next to each other because of float: left, though this will not always look as good. That's why in the next section you will implement the Masonry-library which will take care of the layout. The app layout for now

Letting Masonry handle the layout

Masonry is a great library for building dynamic grids that will layout dynamically depending on the width of the grid items.

Install 'masonry-layout' via npm.

npm install masonry-layout --save

Now import it in the Notes component.

Add the v-el:notes attribute to div.notes so you can reference it in the Vue instance via this.$els.notes.

At the time Masonry is instantiated there are no notes, so you need to tell Masonry that there are new items in the callback of 'child_added'-event and also tell it to lay out the notes again.

This won't work directly in the callback of child_added-event though because at that point the new note will not be rendered yet by Vue. Do it on the nextTick-event, which is similar to nextTick from NodeJS. There you can be certain that the new note is rendered and Masonry will correctly lay out the new item. To make sure all the notes are nicely centered, replace the padding with margin: 0 auto; and add the fitWidth: true option when initializing Masonry.

src/components/Index.vue

<template>
  <div class="notes" v-el:notes>
      <note
        v-for="note in notes"
        :note="note"
        >
      </note>
  </div>
</template>

<script>
  import Firebase from 'firebase'
  import Masonry from 'masonry-layout'
  import Note from './Note'
  export default {
    components: {
      Note
    },
    data () {
      return {
        notes: []
      }
    },
    ready () {
      let masonry = new Masonry(this.$els.notes, {
        itemSelector: '.note',
        columnWidth: 240,
        gutter: 16,
        fitWidth: true
      })
      let firebase = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/')
      firebase.child('notes').on('child_added', (snapshot) => {
        let note = snapshot.val()
        this.notes.unshift(note)
        this.$nextTick(() => { // the new note hasn't been rendered yet, but in the nextTick, it will be rendered
          masonry.reloadItems()
          masonry.layout()
        })
      })
    }
  }
</script>

<style>
  .notes{
    margin: 0 auto;
  }
</style>

In the Note component we can remove float: left; and change the margin margin: 8px 0;.

src/components/notes/Note.vue

<style>
  .note{
    background: #fff;
    border-radius: 2px;
    box-shadow: 0 2px 5px #ccc;
    padding: 10px;
    margin: 8px 0;
    width: 240px;
  }
  ...
</style>

Now Masonry lays out the notes nicely and the app looks like this: Notes layed out by Masonry

Build & deploy

  1. Build the project for production (this will compile everything and put your app in the dist folder)
  2. Install Firebase command line tools
  3. Initialize the app (make sure to enter 'dist' as your public folder and select the correct Firebase app)
  4. If you haven't logged in before, log into Firebase
  5. Deploy your app
  6. View your app online
npm run build
npm install -g firebase-tools
firebase init
firebase deploy
firebase open

Wrapping up

We just created a mimimal notes app that looks like Google Keep with VueJS and Firebase.

  • We created a Notes component that wraps around the individual notes
  • We created a component for visualizing individual notes
  • We created another component that handles the creation of a note
  • We integrated Masonry with the Notes component to handle layout of our notes.
  • We built our app and deployed it to Firebase

Unfortunately there is still a lot missing. So far the app only covers the CR of a CRUD-application. It's still missing UPDATE and DELETE.

In the next part, I will show you how to implement UPDATE and DELETE functionality. I will also show you how to abstract our Firebase logic to an independent data layer to keep our code DRY. Currently every client using the app is sharing this list of notes. In one of the next parts, I will also introduce authentication and give each user its own list of notes.

Let me know if you have any issues and I will try to get back to you. This is my first tutorial ever, so don't hesitate to give feedback. I hope this tutorial was helpful.

Niels Swimberghe

I am a Belgian developer specialized in building apps. I studied New Media and Communication Technology and graduated with a focus on programming. I love building delightful digital experiences with the newest technologies available. Building something that helps people and makes users happy is what drives me.

I am currently looking for opportunities on the US east coast (DC, NY, ...), feel free to get in touch with me to discuss options.