Building a Slack Bot Clone using Firebase Realtime Database & Vue.js - Part 3

Friday, April 21, 2017

In this final part, we’ll cover the UI of the Slack Bot Clone application. We will be using Vue.js to build the UI and the data will be fetched from the firebase database.

Read Part One and Part Two of Building a Slack Bot clone using firebase and Vue.js before reading this.

To know about how to Implement the Authentication using Firebase and Vue.js view this post. The same authentication mechanism is used here to Authenticate the user.

If you are new to Vue.js, I recommend reading the Vue.js Basics.


1. UI Markup

We need to build the following UI sections for our application,

  • Authentication - This section contains the login and signup. To learn more about how Authentication is implemented, read how to Implement Authentication using Firebase and Vue.js

  • User Input Section - This is where the user input’s the command

  • Commands - In this section we’ll show the available commands along with the description of the command.

  • Messages - This section displays all the messages that was created and stored in Firebase.


All of the UI sections are placed under a div which will only be shown when the isAuthenticated data property is true. isAuthenticated property changes based on the Firebase Authentication status.

Since the Authentication section is covered in another post, we’ll move on to building the next UI section in our View i.e. the User Input Section.

1.1 User Input Section

This section contains the input elements with which the user can provide input to the bot. It also contains the Logout button for the user to exit the application.

<!-- 1.1 User Input -->
<div class="input-group">
  <span class="input-group-addon" id="basic-addon">></span>

  <input v-model="input" @keyup.enter="askBot" type="text" class="form-control" 
    placeholder="What do you want me to do ?" />

  <span class="input-group-btn">
    <button @click="signOut" class="btn btn-danger" type="button">Signout</button>
  </span>
</div>

The input element is attached to the Vue input data property using the v-model directive. Any changes in the input text field will be updated to the input data property.

The Logout button is attached with a on click event using the short form (@event) of the v-on directive. So every time the user clicks on the Logout button, the signOut method on the Vue instance will be invoked.

1.2 Commands

This section contains the UI Tooltip to show the commands format and description.

<!-- 1.2 Commands tips -->
<div class="hoverCommands">
  <div v-if="showHoverCommands" class="panel panel-default hover-panel">
    <div class="panel-heading">
      <h4 class="panel-title">Commands</h4>
    </div>
    <div class="panel-body">
      <ul id="containerBox" class="list-group">
        <li is="hover-command" v-for="command in hoverCommands" :command="command"></li>
      </ul>
    </div>
  </div>
</div>

The list of commands to show in the UI Tooltip is based on the hoverCommands Vue data property. The hoverCommands is populated with the matching list of available commands based on the user input.

showHoverCommands is a computed property which returns true if the hoverCommands is not empty and false if empty. Based on the showHoverCommands result, the Tooltip is shown and hidden.

When the Tooltip is shown, i.e. when there is available commands matching the user input in hoverCommands, we use the v-for directive to loop through the list of Command and display the hover-command Vue Component passing it the command data using the v-bind short form. The declaration of the hover-command Vue component will be shown later in the post.

1.3 Messages

The Messages UI contains all the messages stored in the Firebase Database.

<!-- 1.3 Messages -->
<ul class="list-group">
  <li is="message-item" v-for="message in messages" :msg-data="message"></li>
</ul>

Messages are stored in the messages data property. We loop through the messages data property and display the message-item Vue Component passing in the message data via msg-data prop.

We will look at the message-item Component in the next section.

We have so far created only the HTML part of the view, attaching the Vue data properties and components. We cannot run the view yet, since we have not defined the components and the Vue instances.

2. Vue Components

Vue Components are a way to compose reusable UI elements that are reactive and has it’s own scope of the state. It’s like creating our own HTML elements, but way more customizable e.g <link pointsTo="http://example.com" heading="This is similar to title property"></link> which is identical to the anchor HTML element tag.

Let’s create the hover-command and message-item Vue Components we discussed above.

2.1 hover-command Component

hover-command is the simplest of the Components we need to create.

/**
 * The Vue HoverCommand Component
 */
Vue.component('hover-command', {
  props: ['command'],
  template: `<li class="list-group-item">
    <p class="hover-list-item">
      <strong class="pull-left">{{command.cmd}}</strong>
      <span class="pull-right">{{command.desc}}</span>
    </p>
  </li>`
});

It receives the command data from the property passed to it and uses it to populate the data in the view.

2.2 message-item Component

The message-item Component has various options; we will look at it each at a time.

2.2.1 data and props

The props is the list of data that we want the component to receive from the parent that is including the message-item Component. In our case, we want to get the message data via the msgData prop.

Note: HTML attributes are case-insensitive, so when using non-string templates, camelCased prop names need to use their kebab-case (hyphen-delimited) equivalents. In our case the msg-data will be written as msgData when using in template string.

// 2.2.1
data: function () {
  if (this.msgData.m_type === 'cmd') {
    var command = slackbot.getCommand(this.msgData.cmd);

    if (command !== undefined) {
      return command.load(this.msgData);
    }
  }

  return {
    text: this.msgData.text,
    type: this.msgData.m_type,
    cmd: '',
    data: {}
  }
},

The data property must be a function since this is a Component. Here the function identifies the message type, if it’s a cmd it will get the command object using the slackbot.getCommand(cmd) method and pass in the message data to the load method of the Command object retrieved. If the type of command is msg the message is displayed in the UI.

2.2.2 computed properties
// 2.2.2
computed: {

  msgText: function () {
    return this.text.charAt(0).toUpperCase() + this.text.slice(1);
  },

  isBotMessage: function () {
    return this.type === 'cmd';
  }

}

The Component has two computed properties, msgText will upper case the first character of the message and isBotMessage is used to check if the command data is of type cmd. The isBotMessage is used to modify the class list of the element in the View for command message type.

2.2.3 methods

Here we have two methods, which are just some helpers for the View.

// 2.2.3
methods: {

  updateScroll: function () {
    var body = document.getElementsByTagName('body')[0];
    body.scrollTop = body.scrollHeight;
  },

  getReminderDateTime: function () {
    return new Date(this.data.createdAt).toDateString() + ' ' +
      new Date(this.data.createdAt).toTimeString();
  }

}

updateScroll is used to move the windows scroll when content added to the bottom. getReminderDateTime is used to display the date and time string for the reminder command type.

2.2.4 template

Our Template contains the code for the UI of our Component.

// 2.2.4
template: `<li class="list-group-item">
  <div class="">
    <div class="message-profile">
      <img width="36" height="36" class="img-circle" 
        src="assets/bot.png" />
    </div>
    <div class="message-text row" :class="{'bot-message-text': isBotMessage}">
      <div class="col-md-12">
        <p>{{ msgText }}</p>

        <div v-if="type === 'cmd' && cmd === '/yesno'">
          <img class="img-thumbnail" :src="data.image" maxwidth="300px" maxheight="300px" />
        </div>

        <div v-if="type === 'cmd' && cmd === '/remind'">
          <p>You created reminder at - <b>{{ getReminderDateTime() }}</b> for <b>"{{data.subject}}"</b></p>
        </div>

        <div v-if="type === 'cmd' && cmd === '/shorten'">
          <p>Here is the short url - <b><a :href="data.id" target="_blank">{{ data.id }}</a></b> for the given long url</b></p>
        </div>          
      </div>
    </div>
  </div>
</li>`

The template show’s different format of message based on the command type(in our case, yesno, remind and shorten commands). If it’s not a command, it just show’s the text of the message.

The Complete message-item looks as shown below,

/**
 * The Vue MessageItem Component
 */
Vue.component('message-item', {
  props: ['msgData'],
  template: `<li class="list-group-item">
    <div class="">
      <div class="message-profile">
        <img width="36" height="36" class="img-circle" 
          src="assets/bot.png" />
      </div>
      <div class="message-text row" :class="{'bot-message-text': isBotMessage}">
        <div class="col-md-12">
          <p>{{ msgText }}</p>

          <div v-if="type === 'cmd' && cmd === '/yesno'">
            <img class="img-thumbnail" :src="data.image" maxwidth="300px" maxheight="300px" />
          </div>

          <div v-if="type === 'cmd' && cmd === '/remind'">
            <p>You created reminder at - <b>{{ getReminderDateTime() }}</b> for <b>"{{data.subject}}"</b></p>
          </div>

          <div v-if="type === 'cmd' && cmd === '/shorten'">
            <p>Here is the short url - <b><a :href="data.id" target="_blank">{{ data.id }}</a></b> for the given long url</b></p>
          </div>          
        </div>
      </div>
    </div>
  </li>`,

  data: function () {
    if (this.msgData.m_type === 'cmd') {
      var command = slackbot.getCommand(this.msgData.cmd);

      if (command !== undefined) {
        return command.load(this.msgData);
      }
    }

    return {
      text: this.msgData.text,
      type: this.msgData.m_type,
      cmd: '',
      data: {}
    }
  },

  computed: {

    msgText: function () {
      return this.text.charAt(0).toUpperCase() + this.text.slice(1);
    },

    isBotMessage: function () {
      return this.type === 'cmd';
    }

  },

  methods: {

    updateScroll: function () {
      var body = document.getElementsByTagName('body')[0];
      body.scrollTop = body.scrollHeight;
    },

    getReminderDateTime: function () {
      return new Date(this.data.createdAt).toDateString() + ' ' +
        new Date(this.data.createdAt).toTimeString();
    }

  },

  updated: function () {
    this.updateScroll();
  },

  mounted: function () {
    this.updateScroll();
  }

});

3. The Vue Instance

This is going to be our root Vue instance which uses the components and boot’s up the UI. It mount’s the UI Components in the #app div element. We specify that in the el property of the Vue instance.

3.1 data property

The data property is the source of truth for all our components and the Vue instance itself.

// 3.1 data property
data: {
  input: '',

  auth: {
    user: null,
    email: '',
    password: '',
    message: '',
    hasErrors: false
  },

  commands: [],
  hoverCommands: [],
  messages: [],
  attachEvents: false
}

The input data property is attached to the user input element. So anything the user enters in the input will be populated in the input data property.

The auth data property is an Object which contains the information needed to handle the application authentication. More information on this is covered in the Authentication using Firebase.

commands will be populated with the list of all available commands when the application loads. This will later be used to filter and get the matching commands based on the user input to show the available commands tooltip.

hoverCommands as discussed already in the HTML view, contains the list of matching commands based on the user input.

messages will be initially populated with the messages previously added in the Firebase Database for the user. Any new command messages created will be synced from the Firebase Database to the UI with the help of the Firebase Realtime Database events.

When the application loads initially, we need to attach and load data from the Firebase Realtime Database. To know if the data has been loaded and it’s not being loaded again and again, we use the attachEvents property to hold that state. If the attachEvents is true then the data has been loaded and events are attached and false is otherwise.

3.2 Watch properties

Here watch is used to listen to the user input while the user in typing and show the available commands Tooltip.

// 3.2 watch properties
watch: {
  /**
   * Watch the user input for changes
   *
   * @param string input
   */
  input: function (input) {
    if (input === '') {
      this.hoverCommands = [];
      return;
    }

    // Filter the commands based on the user input
    this.hoverCommands = _.filter(this.commands, function (cmdObj) {
      return _.startsWith(cmdObj.cmd, input);
    });
  }
}

This is done by populating the hoverCommands property by filtering through the commands data property array and returning all the commands matching the user input as the user is typing.

Note: The Lodash library is used here to filter through the commands data property array. Lodash is a JavaScript utility library.

3.2 Computed properties

Computed properties are used to react to data changes on the instance data properties.

// 3.3 computed properties
computed: {

  /**
   * Determines if the commands helper should be shown
   *
   * @return boolean
   */
  showHoverCommands: function () {
    return this.hoverCommands.length > 0;
  },

  /**
   * Determines if the user is authenticated
   *
   * @return boolean
   */
  isAuthenticated: function () {
    firebase.auth().onAuthStateChanged(function (user) {
      if (user) {
        this.auth.user = user;
        // Attach firebase db events
        this.attachFirebaseEvents();
        // Load the commands
        this.loadCommands();
      } else {
        this.auth.user = null;
      }
    }.bind(this));

    return (this.auth.user !== null);
  }

}

Here we use the showHoverCommands to trigger the Available Commands Tooltip and isAuthenticated to reflect on the changes to the Firebase Authentication State using the firebase auth().onAuthStateChanged() method.

Everytime the user is authenticated, we trigger the attachFirebaseEvents method, which Loads and Attaches events for loading the messages and adding new messages. We will look in detail about this method in the next section.

3.4 Methods

Below we will look at the various Vue instance methods used throughout the application. Among those methods are the login, signUp and signOut each of which are discussed in the Implementing Firebase Authentication post. So we will skip those methods and look into the remaining methods.

3.4.1 attachFirebaseEvents method

Once the application is loaded i.e. when the user is logged in, we need to Load all the previous messages of the user and attach events so that any changes to the database is reflected in the UI by loading them and updating it in the Vue instance data properties.

// 3.4.1
/**
 * Load the user's messages
 */
attachFirebaseEvents: function () {
  if (this.attachEvents === true) {
    return;
  }

  var vm = this,
    messagesRef = firebase.database().ref('chats/' + vm.auth.user.uid + '/messages'),
    messages = [];

  messagesRef.once('value', function (snapshot) {
    if (snapshot.val() === null) {
      vm.messages.push({
        text: 'Hi, what can i do for you ?',
        m_type: 'msg'
      });
    }
  });

  messagesRef.on('child_added', function (snap) {
    console.log('message child added');
    vm.messages.push(snap.val());      
  });

  messagesRef.on('child_removed', function (snap) {
    console.log('child has been removed');
    var index = _.indexOf(vm.messages, snap.val());
    vm.messages.splice(index, 1);
  });

  messagesRef.on('child_changed', function (snap) {
    console.log('child has been changed');
  });

  this.attachEvents = true;
}

attachFirebaseEvents is called after the user is Authenticated. It first checks if the events is already attached and the messages are loaded by checking the attachEvents property. If already attached, it returns without doing anything.

If the events are not attached and the messages are not loaded, it loads the messages using the firebase value event and attaches the child_added, child_removed and child_changed events, each which would update the messages data property based on the event. Finally, it sets the attachEvents to true.

3.4.2 loadCommands method

Loading the available commands from the database every time the user types is a lengthy process, so we need to load all the available commands when the application boot’s and store it in the commands data property. That is what the loadCommands method does.

// 3.4.2 loadCommands method
/**
 * Load the available commands based on the user input
 */
loadCommands: function () {
  firebase.database().ref('/commands').once('value', function (snap) {
    this.commands = snap.val();
  }.bind(this));
}

3.4.3 askBot method

This is the main method that links the bot and the UI. This is triggered when the user presses the enter button on the user input element.

// 3.4.3 askBot method
/**
 * Handles user command inputs
 */
askBot: function (event) {
  var vm = this,
    bot = null;
  
  // Check if the input is a command
  if (/\/[a-z]+\s/.test(vm.input)) {
    response = slackbot.execCommand(vm.input);
  } else {
    response = fireUtil.addMessage({
      text: vm.input,
      m_type: 'msg',
      timestamp: Date.now(),
      callback: null
    });
  }

  if (response === null) {
    vm.messages.push({
      text: 'I am sorry, i do not recognized that command.'
    });
  }

  // Clear the input
  vm.input = '';
}

Once the method is invoked, it checks the input with Regular Expression to see if it matches the format of a command. If it does, then it is sent to the slackbot.execCommand method, which process and creates the message data and stores it in the database.

If the input is not a command, then it is simply added to the database. Sometimes, the command format provided may be wrong, in such cases the Slack Bot returns a null response. If the response is null, we insert a temporary message, saying the command is not recognized.

You can view the full source code of the app.js in the github repository.

This is how the Slack Bot works and we could add more supported commands by adding it in the bot discussed in the previous parts and modifying the view template to show command messages based on the newly added command.