Build a Realtime CRUD App with Vue & deepstream

Chris Nwamba

Vue is the JavaScript UI tool that parades itself as progressive because it is approachable so one can get started in just a day. On the other hand, it has every feature to cater for your front-end needs, making it versatile.

Realtime technologies are gradually taking a new shape; realtime servers now serve as an abstraction for handling realtime related tasks. deepstream is an open, free and blazingly fast realtime server that you can install on your machine.

This article demonstrates who we can build realtime apps using deepstream and Vue as our front end tool. The image below is a GIF of what we are up to:

Before we get dirty with codes, let's have a reason to do so.

Why Vue

Vue.js Homepage

Vue is so far, in my humble opinion, the simplest UI library out in the wild to get your hands dirty with. It is easy to get started and handles most of the tough concepts that come in mind when considering a UI library. These includes: data binding, server-side rendering, state management.

Vue acts on the complexity of the existing UI library and simplifies those complexities to make our lives less frustrating as software engineers. It also has been backed by one of our favorite backend tool, Laravel -- making integration straightforward. This of course does NOT mean you cannot integrate with any other backend platform.

Why deepstream

deepstream Homepage

deepstream is a standalone server -- faster than most of the our realtime solutions -- that allows you to provision realtime server using persisted state as Data Sync, pub/sub pattern as events or request/response pattern as RPCs.

With these varieties of options, you are assured that deepstream can integrate in whatever nature of app you are building. Including chats, realtime news updates, stock information, CRUD/CMS apps, data visualization, data monitoring, etc.

Setup Vue and deepstream

Installing both Vue and deepstream is quite a straightforward process; few commands on your CLI and you're good to go.

deepstream can be installed on Linux, Windows, and OSX; it can also be installed using Docker and npm. For this article, we will download deepstream for our OS. Unzip the file and run the following command on the unzipped directory to start the server:

./deepstream

vue-cli is a CLI tool that makes scaffolding a Vue project easy and fast. We need to install this CLI tool then we can use the tool to create a new project for our demo app:

npm install -g vue-cli

The CLI tool is installed globally so we can access it from anywhere in our machine. Vue project scaffolds come in different templates, we need something simple and the following command will do just that for us:

vue init webpack my-project

Just CRUD with Vue

Before we attempt a realtime app, let's create a platform for that first. A CRUD (Create, Read Update and Delete) app for managing books will be a good idea.

Creating

The App.vue file in the src folder is our main and only component which is enough for what we are trying to build. Open the file and create a simple form:

<!-- ./src/App.vue -->
<template>
  <div id="app">
    <h1> {{ title }} </h1>
    <h3>New Book</h3>
    <form v-on:submit.prevent="onSubmit">
      <div>
        <input name="title" type="text" placeholder="title" v-model="book.title" />
      </div>
      <div>
        <input name="year" type="text" placeholder="year" v-model="book.year" />
      </div>
      <div>
        <input name="author" type="text" placeholder="author" v-model="book.author" />
      </div>
      <div>
        <label for="read">Read?</label>
        <input type="checkbox" v-model="book.read" id="read" name="read" />
      </div>
      <button v-if="updating">Update</button>
      <button v-else>Add</button>
    </form>
  </div>
</template>

<script>

export default {
  name: 'app',
  data () {
    return {
      title: 'My Books Manager',
      updating: false,
      book: {
        title: '',
        year: '',
        author: '',
        read: false
      }
    }
  }
}
</script>

The file App.vue is known as Vue's Single File Component which is a great strategy from structuring Vue apps by allowing each component to be stored in a file while its template, style, and logic live in the file.

We have a basic form with Vue bindings. Each of the form control is bound to a property (using v-model) in the book object returned by Vue's data method. The method also returns a title which we use as the app's header and an updating flag which we use to toggle Add and Update buttons in the form. The toggle is achieved using Vue's v-if...v-else directives.

When the button(s) is clicked, an onSubmit method will be called because that is what the form's submit event binding is bound to. We need to create this method:

export default {
  name: 'app',
  data () {
    return {
      // . . .
      updateIndex: 0,
      books: [],
      book: {
        title: '',
        year: '',
        author: '',
        read: false
      }
    }
  },
  methods: {
      onSubmit() {
        if(this.updating) {
          this.onUpdate();
          return;
        }
        this.books.push(this.book);
        this.book = {
          title: '',
          year: '',
          author: '',
          read: false
        }
      }
  }
}

onSubmit will check if we are updating or not. If we are updating, it would delegate to another method, onUpdate, to handle updating. Otherwise, it would push the new values to the books array.

Reading

The books array can be iterated and its values printed using a table. The iteration is achievable using the v-for Vue directive as show:

<template>
  <div id="app">
    <h1> {{ title }} </h1>
    <h3>New Book</h3>
    <!-- For markup truncated -->
    <h3>All Books</h3>
    <table>
      <tr>
        <th>Title</th>
        <th>Year</th>
        <th>Author</th>
        <th>Read</th>
        <td>Update</td>
        <td>Delete</td>
      </tr>
      <tr v-for="(b, index) in books">
        <td>{{ b.title }}</td>
        <td>{{ b.year }}</td>
        <td>{{ b.author }}</td>
        <td v-if="b.read">✓</td>
        <td v-else> </td>
        <td v-on:click.prevent="onEdit(index)"><a>✎</a></td>
        <td v-on:click.prevent="onDelete(index)"><a>✗</a></td>
      </tr>
    </table>
  </div>
</template>

Something extra to the table's data rows -- two links to handle editing (not updating) and deleting a record. Each of them calls the onEdit and onDelete methods respectively. You can start using the form and see the results appear in the table:

Updating

Updating takes two stages -- selecting the record from the table that we need to update which will make it appear in the form and mutating the array to update the value.

The onEdit handler is responsible for the first stage:

data () {
    return {
      updating: false,
      updateIndex: 0,
      books: [],
      book: {
        title: '',
        year: '',
        author: '',
        read: false
      }
    }
  },
 methods: {
     // . . .
     onEdit(index) {
       this.updating = true;
       this.updateIndex = index;
       this.book = this.books[index];
     },
 }

onEdit first raises the updating flag, then sets updateIndex to the index being edited and the replaces the book model with the record found in the index being updated.

updateIndex is used to keep track of what is being updated when onUpdate is called:

onUpdate() {
   this.updating = false;
   this.books[this.updateIndex] = this.book;
   this.book = {
     title: '',
     year: '',
     author: '',
     read: false
   }
 },

onUpdate now resets the updating flag, mutates the array to update the book and then empties the book model.

Deleting

This is the simplest; we utilize the array splice method to remove an item from the array based on there index:

onDelete(index) {
 // Remove one item starting at
 // the specified index
 this.books.splice(index, 1)
}

Going Realtime with deepstream

We have just built a working app, but that was not the goal. We need to let all connected clients know when a record is added, updated and deleted by updating the table holding these records. This is where deepstream steps in to help out. At the moment, if we try such, other connected clients stays dumb:

deepstream Clients, Records & Lists

In the beginning of this post, we setup a deepstream server and left it running. In fact, the server is running on localhost at a given port, 6020. This server is just idle hoping for a client to connect and start exchanging data.

A deepstream client can come in any form, ranging from web, desktop, mobile and to even IoT. Our concern is Web for today, so we need to use deepstream's JS SDK to connect to the listening server. You can install the SDK by running:

npm install --save deepstream.io-client-js

Records in deepstream are like records in any other form of representing data. It is a single entity and store a given information item. The only difference is that deepstream records are live which means that data stored on records can be subscribed to by clients and the clients will be notified with payload at the slightest change.

Lists, on the other hand, help group records so they can be treated like a collection. Lists are as well live so they can be subscribed to for realtime changes and updates. We will utilize these deepstream features in our app to make the app realtime.

Authentication

Authentication just like in every other situation allows you to confirm that a user is actually who she claims to be. Your regular HTTP authentication differ a little from the how deepstream will handle authentication. However, the good thing is, it is easy to hook them together.

For every deepstream connecting client, an authentication is required. This does not necessarily mean that a credential must be provided; performing a login can just be anonymous and that applies to our case at the moment.

import * as ds from 'deepstream.io-client-js';

export default {
  name: 'app',
  data () {
    return {
      ds: ds('localhost:6020'),
      books$$: null
      // . . .
    }
  },
  created () {
    this.ds.login({}, () => {
      console.log('logged in');
    });
  },
  methods: {/* . . .*/}
}

First we import the installed client, then we create a member variable ds to hold a reference to the deepstream while passing in the server's URL.

The created function is a lifecycle method which is called by Vue when the component is ready. This makes created a good candidate to handle deepstreawm authentication. Authentication is performed by calling deepstream.login method which receives a credential and a callback.

The books$$ property will keep a reference to the deepstream list we are yet to create.

We will revisit the CRUD process and update them with respect to deepstream but before that, we need to make all the connected clients listen to value changes so they can update accordingly

List and Records Subscriptions

 created () {
    this.ds.login({}, () => {
      console.log('logged in');
    });

    this.books$$ = this.ds.record.getList('books');
    /*
    * Entry added
    */
    this.books$$.on('entry-added', (recordName, index) => {
       this.ds.record.getRecord(recordName).whenReady(record => {

        // The scond paramaeter,
        // a boolean, is a flag to specify whether 
        // the callback should be invoked immediatly
        // with the current value
         record.subscribe(data => {
              if(!data.id) {
                if(data.title) {
                  data.id = record.name;
                  this.books.push(data);
                }
              } else {

              this.books = this.books.map(b => {
                      if(data.id == b.id) {
                            b = data;
                        }
                        console.log(b)
                          return b;
                    });
              }
         }, true) 
       });
    });
    /*
     * Entry removed
     */

    this.books$$.on('entry-removed', (recordName, index) => {
       this.ds.record.getRecord(recordName).whenReady(record => {
         record.subscribe(data => {
             this.books.splice(this.books.indexOf(data, 1));
         }, true) 
       });
    });
  },
  • Lists and records API live on the record object. To create or retrieve an existing list, we use the getList method passing in, a name of our choice.
  • entry-added event listens to record addition to the created list. So when a record is added to the list, the event is triggered.
  • When a record is added, whenReady ensures the record is ready before we subscribe to it using the subscribe method.
  • subscribe takes a callback which checks if the data exists and updates it. If the record's data does not exist, it updates the record with the incoming data while setting the data's id to the record name (id). The books array is updated as data comes in.
  • entry-removed is the opposite of what we saw. It listens for when we remove a record so as to remove the record's data from the books array.

Creating

The onSubmit method will have to take a new shape. We won't be pushing data to books directly because that's already being done by the subscription. We just need to call a method that creates a record with data and adds the record to the list we created:

onSubmit() {
   const recordName = this.book.id || 'book/' + this.ds.getUid();

   this.ds.record.has(recordName, (err, has) => {
     if(has){
       this.onUpdate();
       return;
     } else {
        const bookRecord = this.ds.record.getRecord(recordName);
         bookRecord.set(this.book);

         this.books$$.addEntry(recordName)

         this.book = {
           title: '',
           year: '',
           author: '',
           read: false
         }
     }
   })
 },

We create/get a record using a UUID or the Id attached to data if data exists. The had method is used to check if the record exists or not. If it exists, we call the onUpdate method, else, we set the record to the new book and update the books$$ list with addEntry.

Updating

onUpdate() {
   const recordName = this.books[this.updateIndex].id;
   const bookRecord = this.ds.record.getRecord(recordName);
   bookRecord.set(this.book);
   this.book = {
     title: '',
     year: '',
     author: '',
     read: false
   }

   this.updating = false;
 }

onSubmit delegates updating to the onUpdate if the record name in question exists.

onUpdate retrieves the record name from the books array using the updateIndex. We get the record that the record name belongs to and the update the record with the book changes. No need to do anything on the list, it will behave accordingly.

Deleting

The onDelete method just calls removeEntry from the list to remove a record:

onDelete(index) {
  this.books$$.removeEntry(this.books[index].id);
 }

Conclusion

Building realtime apps keeps getting better with advent of more advanced solutions. You can as well perform HTTP auth or JWT auth as well as connect your database to deepstream when you need to persist data.

It's fascinating how we could build better UI apps using two simple to integrate platforms, Vue and deepstream. Vue is not just the only kid welcomed to the party. You can integrate deepstream with any other UI library out there using the same strategy of initialization and authentication.

Chris Nwamba

JavaScript Preacher. Building the web with the JS community.