Categories
JavaScript

Using JavaScript Objects Built into Browsers

JavaScript is commonly used in web browsers for rendering content. To help with this, almost all browsers have built in support for JavaScript, which includes a standard library of objects that can be used by web apps to do common functionality. The browser itself has a user interface where users type in a URL and then the website loads up and rendered for the user to see. Most websites have HTML, CSS, and JavaScript code. Loading the page can take some time on poorly coded websites. HTML defines the sections of a website, the CSS controls the static styling and layout, and the JavaScript contains the code for controlling the dynamic functionality.

When a user types in a URL, the HTML portion of the website first loads along with the CSS. The the JavaScript code loads line by line in the browser. All the code that is loaded has to be parsed and interpreted by the browser to render them properly on screen. Browsers parse HTML code into a tree model called the Document Object Model (DOM). It’s a full map of a web page, and you can access different parts of the DOM by using JavaScript. Then CSS is parsed which will load the styles defined by the CSS code. Finally, the JavaScript code runs which manipulates the DOM elements that was loaded previously depending on the JavaScript code written. Styling can also be applied with JavaScript dynamically. Loading a website quickly is crucial if you want to keep your visitors since they’re impatient. We can load code that don’t need to be loaded immediately in the background to avoid code holding up a page from loading.

The Browser Object Model

Almost all web browsers support JavaScript and all of those browser will have a set of built in objects which can be accessed by developers to manipulate the page. This is called the Browser Object Model or BOM for short, and it’s series of APIs that can be used by developers to write their web apps.

The Navigator Object

The navigator object is a JavaScript object built into browsers that has the basic properties of the browser, the kind of computer the browser is installed on, and some geolocation data. Pretty much all modern browsers have this, and you can access the following data from it:

  • appCodeName , has the code name of the browser
  • appName, has the name of the browser
  • appVersion , has the browser version
  • cookieEnabled , tells us whether the browser has cookies enabled
  • geolocation , has physical location data if geolocation is enabled
  • language , gets the language of the browser
  • onLine , tells us whether the browser is online
  • platform , gets the platform that the browser is running on.
  • product , gets the engine name of the browser
  • userAgent, has the user agent string that’s sent in HTTP requests to web servers
  • battery , tells us the charging status for the battery of the device if it exists
  • connection , tells us the connection status of the browser
  • hardwareConcurrency , has the number of logical processors of the device,
  • keyboard , tells us the keyword layout of the device
  • serviceWorker , tells us data about service workers, which lets web apps have offline functionality and installation / un-installation functionality for progressive web apps
  • storage , tells us the storage capabilities of the device the browser is running on

All the properties of the navigator object listed above are read only.

If we enter navigator in our browser console, we get the full list of properties of the navigator object.

<img class="do t u gu ak" src="https://miro.medium.com/max/2732/1*qJVRv6xdNNEToXeyfdcEHQ.png" width="1366" height="728" role="presentation"/>

The navigator object’s properties

To detect whether a browser support certain functionality, we should check if the built in browser object has the properties and methods that you want to use.

The Window Object

The window object has the data about the browser window, which is where web pages in the browser is shown. Each tab of the browser has its own instance of the window object.

The following properties are available in the window object in most browsers:

  • closed , boolean value indicating whether a window has been closed or not
  • defaultStatus , gets or sets the default text in the status bar of a window
  • document , the document object of the window, which lets us manipulate the DOM.
  • frameElement , Gets the element, such as <iframe> or <object> that the window is embedded in
  • frames , list all frames in the current window
  • history , has the browser history of the current window
  • innerHeight , inner height of the current window, a read only property
  • innerWidth , inner width of the current window, a read only property
  • length , number of frames in the window, a read only property
  • location , gets the location object, which lets us traverse to different URLs and manipulate the browser history.
  • name , lets us get or set the name of the window
  • navigator , has the navigator object we discussed above
  • opener , gets the window object that created the current window
  • outerHeight , the outer height of the window, including scrollbars and toolbars
  • pageXOffset , retrieves the number of pixels that’s scrolled horizontally in the window
  • pageYOffset, retrieves the number of pixels that’s scrolled vertically in the window
  • parent , object for the parent of the current window
  • screen , Screen object of the window
  • screenLeft , the horizontal distance in pixels from the left side of the main screen to the left side of the current window
  • screenTop, the vertical distance in pixels from the top of the window relative to the top of the screen
  • screenX, the horizontal coordinate relative to the screen
  • screenY, the vertical coordinate relative to the screen
  • self, the current window
  • top , the topmost browser window

We can do lots of things with the window object. Some of the most common include getting screen size, the scroll position, and navigation between pages.

Note that if we define a global variable, it automatically attaches itself as a property of the window object, so if we have a = 1 defined globally, then window.a is 1.

With the window.location object, we can navigate to a URL by setting window.location.href . So if we want to go to the Medium website we write:

window.location.href = 'http://www.medium.com';

The window.innerWidth and window.innerHeight provides us with the width and height of the current browser window.

We can go back to the previous page by writing:

window.history.go(-1)

And get the size of the browser history with:

window.history.length;

The window object also has a list of methods that we can use to display alert boxes, prompt users for data, move and resize the window, and many other things. A list of methods in the window object is below:

  • alert() , displays an alert box with a message and an OK button
  • atob() , decodes a base-64 encoded string
  • blur() , make the current window lose focus
  • clearInterval() , cancel the timer returned by setInterval()
  • clearTimeout() , cancel the timer returned by setTimeout()
  • close() , close the window instance created
  • confirm() , displays a dialog box with an optional message with OK and cancel buttons
  • createPopup() , creates a pop-up window
  • focus() , sets the current window into focus
  • moveBy() , moves the current window by a specified amount
  • moveTo() , move a window to a specified position
  • open() , opens a new window
  • print() , prints the contents of the current window
  • prompt() , displays a dialogue box prompting the user for input
  • resizeBy() , resizes the window by a specified number of pixels
  • resizeTo() , resizes a window to a specified height and width i pixels.
  • scrollBy() , scrolls the document by a specified amount of pixels
  • scrollTo() , scrolls the document to a specific set of pixel coordinates
  • setInterval() , repeatedly call a function at an interval specified in milliseconds
  • setTimeout() , calls function a specified amount of time in milliseconds
  • stop() , stops the current window from loading

Because of security reasons, the focus, open, move and resize methods can only be called on a window that is opened by your own code, because being able to open window that’s not generated by someone’s own code will be exploited by malicious programmers to do undesired things to other user’s computers.

For example, to use the moveBy method must be used as follows:

const newWindow = window.open('', 'My Window', 'width=250, height=250'); newWindow.document.write("<p>This is my window.</p>");  
newWindow.moveBy(250, 250);                                  
newWindow.focus();

These are the most common built in browser objects and properties and methods associated with it. They can be used for some manipulation, and functions like setInterval and setTimeout for executing things that depend on time.

Categories
JavaScript

Useful New Features in ES2016 and 2017

ES2015 introduced lots of features, like arrow functions, class syntax for object creation and inheritance, let and const, and many other new features. In the next few years, more great features will be introduced into the language and the standard library. ES2016 was a minor revision with new features, such as introducing the includes function to arrays and the exponential operator. ES2017 introduced more features like Object.values and Object.entries and string functions like padStart and padEnd, and async and await. These are handy features that bring even more convenience to JavaScript developers, making JavaScript app development even easier.


ES2016 Features

Array.includes

Array.includes checks if an item exists in an array. It takes a number or string, which the function can compare as the first argument and the index to search from as the second argument. The second argument can be positive or negative.

const array = [1,2,3];  
const includesTwo = array.includes(2); // returns true

We can also search the array starting with a negative index, then the function search beginning from the computed index, which is the length of the array plus the second argument. So if we write:

array.includes(2, -2);

Then the includes function will search starting with index 3 +(-2). This means that array.includes(2, -2); should return true. If the absolute number of what you pass in is bigger than the length, then the whole array is searched.

The includes function was also added to TypedArray types like Int8Array , or Uint8Array .

Exponential operator

The exponential operator is another new feature of ES2016. The exponential operator is denoted by **. It’s the new syntax for computing exponential values, which is the alternative to the Math.pow function. For example, to compute 2 to the power of 3, we can write,

2**3

which is 8. Note that in JavaScript, it’s impossible to write ambiguous expressions when we combine the exponential operator with unary operators like -, so something like -2**2 is invalid in JavaScript. However, -(2**2) is valid since it’s unambiguous in that we know that the value is -4 since the exponential is in parentheses and the negative sign is outside.

We can force the base of the exponentiation expression to be a negative number by writing:

(-2)**2

So this means the base has to be in parentheses.


ES2017 Features

Object.values

The Object.values lets us get an array with the values of an object in an array. The values are returned in the same order as it is provided by the for...in loop, except that Object.values do not return values that are in the prototype chain.

For example, we can write:

const obj = { a: 1, b: 2 };  
console.log(Object.values(obj)); // [1, 2]  
  
const obj = { 0: 'd', 1: 'e', 2: 'f' };  
console.log(Object.values(obj)); // ['d', 'e', 'f']  
  
const obj2 = { 100: 'd', 2: 'e', 7: 'f' };  
console.log(Object.values(obj2)); // ['e', 'f', 'd']  
  
console.log(Object.values('abc')); // ['a', 'b', 'c']

As we can see, Object.values works with objects and strings. If the keys are numbers, then they are returned in with the keys in increasing order, like in the obj2 example above. For strings, it returns an array of the individual characters of the string.

Object.entries()

The Object.entries function returns an array with each entry being the key-value pairs in their individual arrays. The entries are returned in the same order as it is provided by the for...in loop, except that Object.values do not return values that are in the prototype chain. As with any object, we can sort the array to get the entries in the order we want with the sort function.

For example, we can write:

const obj = { foo: 1, bar: 2 };  
console.log(Object.entries(obj)); // [ ['foo', 1], ['bar', 2] ]  
  
const obj = { 0: 'x', 1: 'y', 2: 'z' };  
console.log(Object.entries(obj)); // [ ['0', 'x'], ['1', 'y'], ['2', 'z'] ]  
  
const obj2 = { 100: 'x', 2: 'y', 7: 'z' };  
console.log(Object.entries(obj2)); // [ ['2', 'x'], ['7', 'y'], ['100', 'z'] ]  
  
console.log(Object.entries('abc')); // [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ]  
  
console.log(Object.entries(100)); // [ ]  
  
const obj = { a: 1, b: 2, c: 3};  
for (const [key, value] of Object.entries(obj)) {  
  console.log(`${key} ${value}`); // "a 1", "b 2", "c 3"  
}  
  
Object.entries(obj).forEach(([key, value]) => {  
  console.log(`${key} ${value}`); // "a 1", "b 2", "c 3"  
});

In the above examples, we can see that Object.entries return an array of key-value pairs with each entry as an array with the key being the first element and the value being the second element. If the keys are integers, then they are sorted in ascending numerical order. So Object.entries(obj2) returns [ ['2', 'x'], ['7', 'y'], ['100', 'z'] ]. It also works with strings with the individual characters return with the index of the characters as the key, so the key is the index of the string, which is the first element of each entry, and the individual character is the value of the string, which is returned as the second character. For objects that have no properties, like numbers and booleans, Object.entries returns an empty array.

The array returned by Object.entries can be converted to a Map object. It can be used like in the following example:

const obj = { a: 1, b: 2 };   
const map = new Map(Object.entries(obj));  
console.log(map); // Map { a: 1, b: 2 }

String.padStart()

The padStart() function adds a string the number of times before a string until it reaches the length that you specify. The function takes two parameters. The first is the target length of your string. If the target length is less than the length of your string, then the string is returned as-is. The second parameter is the string that you want to add the padding with. It’s an optional parameter and it defaults to ' ' is nothing is specified.

For example, we can write the following:

'def'.padStart(10);         // "       def"  
'def'.padStart(10, "123");  // "1231231def"  
'def'.padStart(6,"123465"); // "abcdef"  
'def'.padStart(8, "0");     // "00000def"  
'def'.padStart(1);          // "def"

Note that each string is filled up to the target length with the string in the second argument. The whole string in the second argument may not always be included. Only the part that lets the function fill the string up to the target length is included.

String.padEnd()

The padEnd() function adds a string the number of times after a string until it reaches the length that you specify. The function takes 2 parameters. The first is the target length of your string. If the target length is less than the length of your string, then the string is returned as is. The second parameter is the string that you want to add the padding with. It’s an optional parameter, and it defaults to ' ' is nothing is specified.

For example, we can write the following:

'def'.padEnd(10);         // "def       "  
'def'.padEnd(10, "123");  // "def1231231"  
'def'.padEnd(6,"123465"); // "defabc"  
'def'.padEnd(8, "0");     // "def00000"  
'def'.padEnd(1);          // "def"

Object.getOwnPropertyDescriptors()

The Object.getOwnPropertyDescriptors() function returns all the property descriptors of an object.

For example, we can use it like the following code:

const obj = {  
  a: 1  
};const descriptors = Object.getOwnPropertyDescriptors(obj);

The descriptors object should have:

{  
  a: {  
    configurable: true,  
    enumerable: true,  
    value: 1,  
    writable: true  
  }  
}

Async and Await

With async and await, we can shorten promise code. Before async and await, we have to use the then function, we make to put callback functions as an argument of all of our then functions. This makes the code long is we have lots of promises. Instead, we can use the async and await syntax to replace the then and its associated callbacks as follows. For example, we have the code to make requests to the back end with the Fetch API:

const APIURL = "http://localhost:3000";
const subscribe = async data => {  
  const response = await fetch(`${APIURL}/subscribers`, {  
    method: "POST",  
    mode: "cors",  
    cache: "no-cache",  
    headers: {  
      "Content-Type": "application/json"  
    },  
    body: JSON.stringify(data)  
  });  
  return response.json();  
};

window.onload = () => {  
  nameForm.method = "post";  
  nameForm.target = "_blank";  
  nameForm.action = "";  
  nameForm.addEventListener("submit", async e => {  
    e.preventDefault();  
    const firstName = document.getElementById("firstName").value;  
    const lastName = document.getElementById("lastName").value;  
    const email = document.getElementById("email").value;  
    let errors = [];  
    if (!firstName) {  
      errors.push("First name is required.");  
    } 

    if (!lastName) {  
      errors.push("Last name is required.");  
    } 

    if (!email) {  
      errors.push("Email is required.");  
    } 

    if (!/\[^@\]+@\[^\.\]+\..+/.test(email)) {  
      errors.push("Email is invalid.");  
    } 

    if (errors.length > 0) {  
      alert(errors.join(" "));  
      return;  
    }  
    try {  
      const response = await subscribe({  
        firstName,  
        lastName,  
        email  
      });  
      alert(`${response.firstName} ${response.lastName} has subscribed`);  
    } 
    catch (error) {  
      alert(error.toString());  
    }  
  });  
};

We replaced the then and callbacks with await. Then we can assign the resolved values of each promise as variables. Note that if we use await for our promise code then we have to put async in the function signature as we did in the above example. To catch errors, instead of chaining the catch function in the end, we use the catch clause to do it instead. Also, instead of chaining the finally function at the bottom to run code when a promise ends, we use the finally clause after the catch clause to do that instead.

In the code above, we got the resolved value of the promise assigned to a variable instead of getting the value in the callback of the then function, like in the const response = await subscribe({...}) line above.

async functions always return promises and cannot return anything else like any other function that uses promises. In the example above, we used the promised based Fetch API and async and await syntax, we showed that we can chain promises in a much shorter way than with the then function with callbacks passed in as an argument.

ES2016 and 2017 gave us convenient features that enhance the already great syntactic sugar and functions that were added to ES2015, which was a significant improvement over its predecessor. These two versions of JavaScript make JavaScript development even better with new Object, string, and Array methods and shorthand for chaining promises.

Categories
JavaScript Vue

How To Use Vuetify in Vue.js

There is great support for Material Design in Vue.js. One of the libraries available for Vue.js is Vuetify. It is easy to incorporate into your Vue.js app and the result is appealing to the users’ eyes.

In this piece, we will build an app that displays data from the New York Times API. You can register for an API key at https://developer.nytimes.com/. After that, we can start building the app.

To start building the app, we have to install the Vue CLI. We do this by running:

npm install -g @vue/cli

Node.js 8.9 or later is required for Vue CLI to run. I did not have success getting the Vue CLI to run with the Windows version of Node.js. Ubuntu had no problem running Vue CLI for me.

Then, we run:

vue create vuetify-nyt-app

To create the project folder and create the files. In the wizard, instead of using the default options, we choose ‘Manually select features’. We select Babel, Router, and Vuex from the list of options by pressing space on each. If they are green, it means they’re selected.

Now we need to install a few libraries. We need to install an HTTP client, a library for formatting dates, one for generating GET query strings from objects, and another one for form validation.

Also, we need to install the Vue Material library itself. We do this by running:

npm i axios moment querystring vee-validate

axios is our HTTP client, moment is for manipulating dates, querystring is for generating query strings from objects, and vee-validate is an add-on package for Vue.js to do validation.

Then, we have to add the boilerplate for vuetify. We do this by running vue add vuetify. This adds the library and the references to it in our app, in their appropriate locations in our code.

Now that we have all the libraries installed, we can start building our app.

First, we create some components. In the views folder, we create Home.vue and Search.vue. Those are the code files for our pages. Then, create a mixins folder and create a file, called nytMixin.js.

Mixins are code snippets that can be incorporated directly into Vue.js components and used as if they are directly in the component. Then, we add some filters.

Filters are Vue.js code that map from one thing to another. We create a filters folder and add capitalize.js and formatDate.js.

In the components folder, we create a file, called SearchResults.vue. The components folder contains Vue.js components that aren’t pages.

To make passing data between components easier and more organized, we use Vuex for state management. As we selected Vuex when we ran vue create, we should have a store.js in our project folder. If not, create it.

In store.js, we put:

import Vue from 'vue'  
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({  
  state: {  
    searchResults: []  
  },  
  mutations: {  
    setSearchResults(state, payload) {  
      state.searchResults = payload;  
    }  
  },  
  actions: {}  
})

The state object is where the state is stored. The mutations object is where we can manipulate our state.

When we call this.$store.commit(“setSearchResults”, searchResults) in our code, given that searchResults is defined, state.searchResults will be set to searchResults.

We can then get the result by using this.$store.state.searchResults.

We need to add some boilerplate code to our app. First, we add our filter. In capitalize.js, we put:

export const capitalize = (str) => {  
    if (typeof str == 'string') {  
        if (str == 'realestate') {  
            return 'Real Estate';  
        }  
        if (str == 'sundayreview') {  
            return 'Sunday Review';  
        }
        if (str == 'tmagazine') {  
            return 'T Magazine';  
        }  
        return `${str[0].toUpperCase()}${str.slice(1)}`;  
    }  
}

This allows us to map capitalize our New York Times section names, listed in the New York Times developer pages. Then, in formatDate.js, we put:

import * as moment from 'moment';
export const formatDate = (date) => {  
    if (date) {  
        return moment(date).format('YYYY-MM-DD hh:mm A');  
    }  
}

To format our dates into a human-readable format.

In main.js, we put:

import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
import { formatDate } from './filters/formatDate';  
import { capitalize } from './filters/capitalize';  
import VeeValidate from 'vee-validate';  
import Vuetify from 'vuetify/lib'  
import vuetify from './plugins/vuetify';  
import '@mdi/font/css/materialdesignicons.css'
Vue.config.productionTip = false;
Vue.use(VeeValidate);  
Vue.use(Vuetify);  
Vue.filter('formatDate', formatDate);  
Vue.filter('capitalize', capitalize);
new Vue({  
  router,  
  store,  
  vuetify,  
  render: h => h(App)  
}).$mount('#app')

Notice that, in the file above, we have to register the libraries we use with Vue.js by calling Vue.use on them, so they can be used in our app templates.

We call Vue.filter on our filter functions so we can use them in our templates by adding a pipe and the filter name to the right of our variable.

Then, in router.js, we put:

import Vue from 'vue'  
import Router from 'vue-router'  
import Home from './views/Home.vue';  
import Search from './views/Search.vue';
Vue.use(Router)
export default new Router({  
  mode: 'history',  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: '/',  
      name: 'home',  
      component: Home  
    },  
    {  
      path: '/search',  
      name: 'search',  
      component: Search  
    }  
  ]  
})

So that we can go to the pages when we enter the URLs listed.

mode: ‘history’ means that we won’t have a hash sign between the base URL and our routes.

If we deploy our app, we need to configure our web server so that all requests will be redirected to index.html so we won’t have errors when we reload the app.

For example, in Apache, we do:

<IfModule mod_rewrite.c>  
  RewriteEngine On  
  RewriteBase /  
  RewriteRule ^index\.html$ - [L]  
  RewriteCond %{REQUEST_FILENAME} !-f  
  RewriteCond %{REQUEST_FILENAME} !-d  
  RewriteRule . /index.html [L]  
</IfModule>

And, in NGINX, we put:

location / {  
  try_files $uri $uri/ /index.html;  
}

See your web server’s documentation for info on how to do the same thing in your web server.

Now, we write the code for our components. In SearchResult.vue, we put:

<template>  
  <v-container>  
    <v-card v-for="s in searchResults" :key="s.id" class="mx-auto">  
      <v-card-title>{{s.headline.main}}</v-card-title><v-list-item>  
        <v-list-item-content>Date: {{s.pub_date | formatDate}}</v-list-item-content>  
      </v-list-item>  
      <v-list-item>  
        <v-list-item-content>  
          <a :href="s.web_url">Link</a>  
        </v-list-item-content>  
      </v-list-item>  
      <v-list-item v-if="s.byline.original">  
        <v-list-item-content>{{s.byline.original}}</v-list-item-content>  
      </v-list-item>  
      <v-list-item>  
        <v-list-item-content>{{s.lead_paragraph}}</v-list-item-content>  
      </v-list-item>  
      <v-list-item>  
        <v-list-item-content>{{s.snippet}}</v-list-item-content>  
      </v-list-item>  
    </v-card>  
  </v-container>  
</template>

<script>  
export default {  
  computed: {  
    searchResults() {  
      return this.$store.state.searchResults;  
    }  
  }  
};  
</script>

<style scoped>  
.title {  
  margin: 0 15px !important;  
}

#search-results {  
  margin: 0 auto;  
  width: 95vw;  
}  
</style>

This is where get our search results from the Vuex store and display them.

We return this.$store.state.searchResults in a function in the computed property in our app so the search results will be automatically refreshed when the store’s searchResults state is updated.

md-card is a card widget for displaying data in a box. v-for is for looping the array entries and displaying everything. md-list is a list widget for displaying items in a list, neatly on the page. {{s.pub_date | formatDate}} is where our formatDate filter is applied.

Next, we write our mixin. We will add code for our HTTP calls in our mixin.

In nytMixin.js, we put:

const axios = require('axios');  
const querystring = require('querystring');  
const apiUrl = 'https://api.nytimes.com/svc';  
const apikey = 'your api key';
export const nytMixin = {  
    methods: {  
        getArticles(section) {  
            return axios.get(`${apiUrl}/topstories/v2/${section}.json?api-key=${apikey}`);  
        },
        searchArticles(data) {  
            let params = Object.assign({}, data);  
            params['api-key'] = apikey;  
            Object.keys(params).forEach(key => {  
                if (!params\[key]) {  
                    delete params[key];  
                }  
            })  
            const queryString = querystring.stringify(params);  
            return axios.get(`${apiUrl}/search/v2/articlesearch.json?${queryString}`);  
        }  
    }  
}

We return the promises for HTTP requests to get articles in each function. In the searchArticles function, we message the object that we pass in into a query string that we pass into our request.

Make sure you put your API key into your app into the apiKey constant and delete anything that is undefined, with:

Object.keys(params).forEach(key => {  
  if (!params[key]) {  
     delete params[key];  
  }  
})

In Home.vue, we put:

<template>  
  <div>  
    <div class="text-center" id="header">  
      <h1>{{selectedSection | capitalize}}</h1>  
      <v-spacer></v-spacer>  
      <v-menu offset-y>  
        <template v-slot:activator="{ on }">  
          <v-btn color="primary" dark v-on="on">Sections</v-btn>  
        </template>  
        <v-list>  
          <v-list-item v-for="(s, index) in sections" :key="index"@click="selectSection(s)">  
            <v-list-item-title>{{ s | capitalize}}</v-list-item-title>  
          </v-list-item>  
        </v-list>  
      </v-menu>  
      <v-spacer></v-spacer>  
      <v-spacer></v-spacer>  
    </div>  
    <v-spacer></v-spacer>  
    <v-card v-for="a in articles" :key="a.id" class="mx-auto">  
      <v-card-title>{{a.title}}</v-card-title>  
      <v-card-text>  
        <v-list-item>  
          <v-list-item-content>Date: {{a.published_date | formatDate}}</v-list-item-content>  
        </v-list-item>  
        <v-list-item>  
          <v-list-item-content>  
            <a :href="a.url">Link</a>  
          </v-list-item-content>  
        </v-list-item>  
        <v-list-item v-if="a.byline">  
          <v-list-item-content>{{a.byline}}</v-list-item-content>  
        </v-list-item>  
        <v-list-item>  
          <v-list-item-content>{{a.abstract}}</v-list-item-content>  
        </v-list-item>  
        <v-list-item>  
          <v-list-item-content>  
            <img  
              v-if="a.multimedia[a.multimedia.length - 1]"  
              :src="a.multimedia[a.multimedia.length - 1].url"  
              :alt="a.multimedia[a.multimedia.length - 1].caption"  
              class="image"  
            />  
          </v-list-item-content>  
        </v-list-item>  
      </v-card-text>  
    </v-card>  
  </div>  
</template>

<script>  
import { nytMixin } from "../mixins/nytMixin";

export default {  
  name: "home",  
  mixins: [nytMixin],  
  computed: {},
  data() {  
    return {  
      selectedSection: "home",  
      articles: [],  
      sections: `arts, automobiles, books, business, fashion, food, health,  
    home, insider, magazine, movies, national, nyregion, obituaries,  
    opinion, politics, realestate, science, sports, sundayreview,  
    technology, theater, tmagazine, travel, upshot, world\`  
        .replace(/ /g, "")  
        .split(",")  
    };  
  },
  beforeMount() {  
    this.getNewsArticles(this.selectedSection);  
  },
  methods: {  
    async getNewsArticles(section) {  
      const response = await this.getArticles(section);  
      this.articles = response.data.results;  
    },selectSection(section) {  
      this.selectedSection = section;  
      this.getNewsArticles(section);  
    }  
  }  
};  
</script>

<style scoped>  
.image {  
  width: 100%;  
}

.title {  
  color: rgba(0, 0, 0, 0.87) !important;  
  margin: 0 15px !important;  
}

.md-card {  
  width: 95vw;  
  margin: 0 auto;  
}

#header {  
  margin-bottom: 10px;  
}  
</style>

This page component is where we get the articles for the selected section, defaulting to the home section. We also have a menu to select the section we want to see, by adding:

<v-menu offset-y>  
   <template v-slot:activator="{ on }">  
     <v-btn color="primary" dark v-on="on">Sections</v-btn>  
   </template>  
   <v-list>  
      <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)">  
        <v-list-item-title>{{ s | capitalize}}</v-list-item-title>  
      </v-list-item>  
   </v-list>  
</v-menu>

Notice that we use the async and await keywords in our promises code, instead of using then.

It is much shorter and the functionality between then, await, and async is equivalent. However, it is not supported in Internet Explorer. In the beforeMount block, we run the this.getNewsArticles to get the articles as the page loads.

Note that the Vuetify library uses the slots of the features of Vue.js extensively. The elements with nesting, like the v-slot prop, are in:

<v-menu offset-y>  
  <template v-slot:activator="{ on }">  
     <v-btn color="primary" dark v-on="on">Sections</v-btn>  
  </template>  
  <v-list>  
    <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)">  
       <v-list-item-title>{{ s | capitalize}}</v-list-item-title>  
     </v-list-item>  
  </v-list>  
</v-menu>

See the Vue.js guide for details.

In Search.vue, we put:

<template>  
  <div>  
    <form>  
      <v-text-field  
        v-model="searchData.keyword"  
        v-validate="'required'"  
        :error-messages="errors.collect('keyword')"  
        label="Keyword"  
        data-vv-name="keyword"  
        required  
      ></v-text-field> <v-menu  
        ref="menu"  
        v-model="toggleBeginDate"  
        :close-on-content-click="false"  
        transition="scale-transition"  
        offset-y  
        full-width  
        min-width="290px"  
      >  
        <template v-slot:activator="{ on }">  
          <v-text-field  
            v-model="searchData.beginDate"  
            label="Begin Date"  
            prepend-icon="event"  
            readonly  
            v-on="on"  
          ></v-text-field>  
        </template>  
        <v-date-picker  
          v-model="searchData.beginDate"  
          no-title  
          scrollable  
          :max="new Date().toISOString()"  
        >  
          <v-spacer></v-spacer>  
          <v-btn text color="primary" @click="toggleBeginDate = false">Cancel</v-btn>  
          <v-btn  
            text  
            color="primary"  
           @click="$refs.menu.save(searchData.beginDate); toggleBeginDate = false"  
          >OK</v-btn>  
        </v-date-picker>  
      </v-menu> <v-menu  
        ref="menu"  
        v-model="toggleEndDate"  
        :close-on-content-click="false"  
        transition="scale-transition"  
        offset-y  
        full-width  
        min-width="290px"  
      >  
        <template v-slot:activator="{ on }">  
          <v-text-field  
            v-model="searchData.endDate"  
            label="End Date"  
            prepend-icon="event"  
            readonly  
            v-on="on"  
          ></v-text-field>  
        </template>  
        <v-date-picker  
          v-model="searchData.endDate"  
          no-title  
          scrollable  
          :max="new Date().toISOString()"  
        >  
          <v-spacer></v-spacer>  
          <v-btn text color="primary" @click="toggleEndDate = false">Cancel</v-btn>  
          <v-btn  
            text  
            color="primary"  
            @click="$refs.menu.save(searchData.endDate); toggleEndDate = false"  
          >OK</v-btn>  
        </v-date-picker>  
      </v-menu> 
      <v-select  
        v-model="searchData.sort"  
        :items="sortChoices"  
        label="Sort By"  
        data-vv-name="sort"  
        item-value="value"  
        item-text="name"  
      >  
        <template slot="selection" slot-scope="{ item }">{{ item.name }}</template>  
        <template slot="item" slot-scope="{ item }">{{ item.name }}</template>  
      </v-select>
      <v-btn class="mr-4" type="submit" @click="search">Search</v-btn>  
    </form>  
    <SearchResults />  
  </div>  
</template>

<script>  
import { nytMixin } from "../mixins/nytMixin";  
import SearchResults from "@/components/SearchResults.vue";  
import * as moment from "moment";  
import { capitalize } from "@/filters/capitalize";export default {  
  name: "search",  
  mixins: [nytMixin],  
  components: {  
    SearchResults  
  },  
  computed: {  
    isFormDirty() {  
      return Object.keys(this.fields).some(key => this.fields[key].dirty);  
    }  
  },  
  data: () => {  
    return {  
      searchData: {  
        sort: "newest"  
      },  
      disabledDates: date => {  
        return +date >= +new Date();  
      },  
      sortChoices: [  
        {  
          value: "newest",  
          name: "Newest"  
        },  
        {  
          value: "oldest",  
          name: "Oldest"  
        },  
        {  
          value: "relevance",  
          name: "Relevance"  
        }  
      ],  
      toggleBeginDate: false,  
      toggleEndDate: false  
    };  
  },  
  methods: {  
    async search(evt) {  
      evt.preventDefault();  
      if (!this.isFormDirty || this.errors.items.length > 0) {  
        return;  
      }  
      const data = {  
        q: this.searchData.keyword,  
        begin_date: moment(this.searchData.beginDate).format("YYYYMMDD"),  
        end_date: moment(this.searchData.endDate).format("YYYYMMDD"),  
        sort: this.searchData.sort  
      };  
      const response = await this.searchArticles(data);  
      this.$store.commit("setSearchResults", response.data.response.docs);  
    }  
  }  
};  
</script>

This is where we have a form to search for articles. We also have two date-pickers to label users to set the start and end dates. We only restrict the dates to today and earlier so that the search query makes sense.

In this block:

<v-text-field  
  v-model="searchData.keyword"  
  v-validate="'required'"  
  :error-messages="errors.collect('keyword')"  
  label="Keyword"  
  data-vv-name="keyword"  
  required  
></v-text-field>

We use vee-validate to check if the required search keyword field is filled in. If it’s not, it’ll display an error message and prevent the query from proceeding.

We also nested our SearchResults component into the Search page component, by including:

components: {  
  SearchResults  
}

Between the script tag and <SearchResults /> in the template.

Finally, we add our top bar and menu by putting the following in App.vue:

<template>  
  <v-app>  
    <v-navigation-drawer v-model="drawer" app>  
      <v-list nav dense>  
        <v-list-item-group v-model="group" active-class="deep-purple--text text--accent-4">  
          <v-list-item>  
            <v-list-item-title>New Yourk Times Vuetify App</v-list-item-title>  
          </v-list-item><v-list-item>  
            <v-list-item-title>  
              <router-link to="/">Home</router-link>  
            </v-list-item-title>  
          </v-list-item><v-list-item>  
            <v-list-item-title>  
              <router-link to="/search">Search</router-link>  
            </v-list-item-title>  
          </v-list-item>  
        </v-list-item-group>  
      </v-list>  
    </v-navigation-drawer><v-app-bar app>  
      <v-toolbar-title class="headline text-uppercase">  
        <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>  
        <span>New York Times Vuetify App</span>  
      </v-toolbar-title>  
      <v-spacer></v-spacer>  
    </v-app-bar><v-content>  
      <v-container fluid>  
        <router-view />  
      </v-container>  
    </v-content>  
  </v-app>  
</template>

<script>  
export default {  
  name: "app",  
  data: () => {  
    return {  
      showNavigation: false,  
      drawer: false,  
      group: null  
    };  
  }  
};  
</script>

<style>  
.center {  
  text-align: center;  
}

form {  
  width: 95vw;  
  margin: 0 auto;  
}

.md-toolbar.md-theme-default {  
  background: #009688 !important;  
  height: 60px;  
}

.md-title,  
.md-toolbar.md-theme-default .md-icon {  
  color: #fff !important;  
}  
</style>

If you want a top bar with a left navigation drawer, you have to follow the code structure above precisely.

Categories
JavaScript Vue

Form Validation with Vee-Validate

Vue.js is a great framework for building front end web apps. It uses a component-based architecture which makes organizing code easy. It allows you to use the latest features JavaScript has to offer which means writing code to build your apps is easy than ever. It has a lot of add-ons like routing and flux store that you can add when you scaffold your app.

However, one thing that is missing is form validation. This means that we have to find our own form validation library to do form validation or write the form validation code ourselves.

If we choose to use a library to do form validation, Vee-Validate is a great choice plugging directly into Vue.js form code to do form validation. Vee-Validate primary adds code to Vue.js component templates to enable form validation for Vue.js forms. It has form validation rules for many kinds of inputs. Therefore, it is a great choice for validating Vue.js forms.

In this story, we will build an address book app with Vue.js that uses Vee-Validate to validate our inputs. The form allows us to add and edit our contacts; also, we can get and delete contacts.

To build our app, first, we need to quickly set up a back end. To do this, we use a Node.js package called JSON Server to run our back end. Find the package’s documentation here.

Once this is running, it provides us with routes for us to save our contact entries from front end.

To install the package, run:

npm install -g json-server

We will run this later so we can save our contacts.

Now we can start building our app. To do this, install the Vue CLI by running:

npm install -g @vue/cli

Then create the app by running:

vue create vee-validate-address-book-app

vee-validate-address-book-app is our app name. When running the wizard, be sure you choose to include Vuex and Vue Router as we will use it later. Next, we have to install some libraries. We need an HTTP client, a Material Design library for making our app look good, and the Vee-Validate library.

To do this, run npm i axios vee-validate vue-material . Axios is our HTTP client for communicating to back end. Vue Material is our Material Design library.

Next, we create our components that we nest in our page components. To do this, create a components folder in our project folder and create a file within it called ContactForm.vue .

In this file, we put:

<template>
  <div class="contact-form">
    <div class="center">
      <h1>{{editing ? 'Edit': 'Add'}} Contact</h1>
    </div>
    <form novalidate class="md-layout" @submit="save">
      <md-field :class="{ 'md-invalid': errors.has('firstName') }">
        <label for="firstName">First Name</label>
        <md-input
          name="firstName"
          v-model="contact.firstName"
          v-validate="'required'"
          :disabled="sending"
        />
        <span class="md-error" v-if="errors.has('firstName')">First Name is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('lastName') }">
        <label for="lastName">Last Name</label>
        <md-input
          name="lastName"
          v-model="contact.lastName"
          :disabled="sending"
          v-validate="'required'"
        />
        <span class="md-error" v-if="errors.has('lastName')">Last Name is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('addressLineOne') }">
        <label for="addressLineOne">Address Line 1</label>
        <md-input
          name="addressLineOne"
          v-model="contact.addressLineOne"
          :disabled="sending"
          v-validate="'required'"
        />
        <span class="md-error" v-if="errors.has('addressLineOne')">Address line 1 is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('addressLineTwo') }">
        <label for="addressLineTwo">Address Line 2</label>
        <md-input name="addressLineTwo" v-model="contact.addressLineTwo" :disabled="sending" />
        <span class="md-error" v-if="errors.has('addressLineTwo')">Address line 2 is required</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('city') }">
        <label for="city">City</label>
        <md-input name="city" v-model="contact.city" :disabled="sending" v-validate="'required'" />
        <span class="md-error" v-if="errors.has('city')">City is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('country') }">
        <label for="country">Country</label>
        <md-select
          name="country"
          v-model="contact.country"
          md-dense
          :disabled="sending"
          v-validate.continues="'required'"
        >
          <md-option :value="c" :key="c" v-for="c in countries">{{c}}</md-option>
        </md-select>
        <span class="md-error" v-if="errors.firstByRule('country', 'required')">Country is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('postalCode') }">
        <label for="postalCode">Postal Code</label>
        <md-input
          name="postalCode"
          v-model="contact.postalCode"
          :disabled="sending"
          v-validate="{ required: true, regex: getPostalCodeRegex() }"
        />
        <span
          class="md-error"
          v-if="errors.firstByRule('postalCode', 'required')"
        >Postal Code is required.</span>
        <span
          class="md-error"
          v-if="errors.firstByRule('postalCode', 'regex')"
        >Postal Code is invalid.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('phone') }">
        <label for="phone">Phone</label>
        <md-input
          name="phone"
          v-model="contact.phone"
          :disabled="sending"
          v-validate="{ required: true, regex: getPhoneRegex() }"
        />
        <span class="md-error" v-if="errors.firstByRule('phone', 'required')">Phone is required.</span>
        <span class="md-error" v-if="errors.firstByRule('phone', 'regex')">Phone is invalid.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('gender') }">
        <label for="gender">Gender</label>
        <md-select
          name="gender"
          v-model="contact.gender"
          md-dense
          :disabled="sending"
          v-validate.continues="'required'"
        >
          <md-option value="male">Male</md-option>
          <md-option value="female">Female</md-option>
        </md-select>
        <span class="md-error" v-if="errors.firstByRule('gender', 'required')">Gender is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('age') }">
        <label for="age">Age</label>
        <md-input
          type="number"
          id="age"
          name="age"
          autocomplete="age"
          v-model="contact.age"
          :disabled="sending"
          v-validate="'required|between:0,200'"
        />
        <span class="md-error" v-if="errors.firstByRule('age', 'required')">Age is required.</span>
        <span class="md-error" v-if="errors.firstByRule('age', 'between')">Age must be 0 and 200.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('email') }">
        <label for="email">Email</label>
        <md-input
          type="email"
          name="email"
          autocomplete="email"
          v-model="contact.email"
          :disabled="sending"
          v-validate="'required|email'"
        />
        <span class="md-error" v-if="errors.firstByRule('email', 'required')">Email is required.</span>
        <span class="md-error" v-if="errors.firstByRule('email', 'email')">Email is invalid.</span>
      </md-field>
<md-progress-bar md-mode="indeterminate" v-if="sending" />
<md-button type="submit" class="md-raised">{{editing ? 'Edit':'Create'}} Contact</md-button>
    </form>
  </div>
</template>

<script>
import { COUNTRIES } from "@/helpers/exports";
import { contactMixin } from "@/mixins/contactMixin";
export default {
  name: "ContactForm",
  mixins: [contactMixin],
  props: {
    editing: Boolean,
    contactId: Number
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some(key => this.fields[key].dirty);
    },
    contacts() {
      return this.$store.state.contacts;
    }
  },
  data() {
    return {
      sending: false,
      contact: {},
      countries: COUNTRIES.map(c => c.name)
    };
  },
  beforeMount() {
    this.contact = this.contacts.find(c => c.id == this.contactId) || {};
  },
  methods: {
    async save(evt) {
      evt.preventDefault();
      try {
        const result = await this.$validator.validateAll();
        if (!result) {
          return;
        }
        if (this.editing) {
          await this.updateContact(this.contact, this.contactId);
          await this.getAllContacts();
          this.$emit("contactSaved");
        } else {
          await this.addContact(this.contact);
          await this.getAllContacts();
          this.$router.push("/");
        }
      } catch (ex) {
        console.log(ex);
      }
    },
    async getAllContacts() {
      try {
        const response = await this.getContacts();
        this.$store.commit("setContacts", response.data);
      } catch (ex) {
        console.log(ex);
      }
    },
    getPostalCodeRegex() {
      if (this.contact.country == "United States") {
        return /^[0-9]{5}(?:-[0-9]{4})?$/;
      } else if (this.contact.country == "Canada") {
        return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/;
      }
      return /./;
    },
    getPhoneRegex() {
      if (["United States", "Canada"].includes(this.contact.country)) {
        return /^[2-9]\d{2}[2-9]\d{2}\d{4}$/;
      }
      return /./;
    }
  }
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.contact-form {
  margin: 0 auto;
  width: 90%;
}
</style>

In the file above, we have the contact form for adding and updating contacts in our address book — where Vee-Validate is used the most. Notice that in most input controls within the form tag, we have the v-validate prop.

This is where we specify what kind of input the control accepts.

required means that the form field is required.

regex means we validate against a specified regular expression.

This allows for custom form validation where there are no built-in rules for Vee-Validate available, or when you need to validate the field differently, depending on the value of another field.

For example for a phone number, we have this function:

getPhoneRegex() {  
  if (["United States", "Canada"].includes(this.contact.country)){  
    return /^\[2-9\]\\d{2}\[2-9\]\\d{2}\\d{4}$/;  
  }  
  return /./;  
}

It allows us to validate the number to see if it matches the North American telephone format when we enter United States or Canada. Otherwise, we let people enter whatever they want.

Similarly, for postal code, we have:

getPostalCodeRegex() {  
  if (this.contact.country == "United States") {  
    return /^[0-9]{5}(?:-\[0-9\]{4})?$/;  
  } 
  else if (this.contact.country == "Canada") {  
    return /^[A-Za-z]\d\[A-Za-z]\[ -]?\d\[A-Za-z]\d$/;  
  }  
  return /./;  
}

This allows us to check for U.S. and Canadian postal codes.

To display errors, we can check if errors exist for a form field, then display them. For example, for a first name, we have:

<span class="md-error" v-if="errors.has('firstName')">First Name is required.</span>

errors.has(‘firstName’) checks if the first name field meets the specified validation criteria. Since we’re checking if it’s filled in, there is only one possible error, so we can just display the only error when errors.has(‘firstName’) returns true .

For something more complex like a phone number, we have:

<span class="md-error" v-if="errors.firstByRule('phone', 'required')">Phone is required.</span>
<span class="md-error" v-if="errors.firstByRule('phone', 'regex')">Phone is invalid.</span>

This allows us to check for each validation rule separately. For the phone number field, we have to check if it’s filled in and if what’s filled in has a valid format. The errors.firstByRule function allows us to do that.

errors.firstByRule(‘phone’, ‘required’) returns true if the field is not filled in and false otherwise.

errors.firstByRule(‘phone’, ‘regex’) returns true is the phone number format is filled in incorrectly and false otherwise.

Vee-Validate provides a this.field object to your component. So we can check if fields are dirty, meaning whether they have been manipulated or not, by adding:

Object.keys(this.fields).some(key => this.fields\[key\].dirty)

Each property is a form field and each property of the this.fields object has a dirty property, so we can check whether fields are manipulated or not.

In the save function of the methods object, we have:

async save(evt) {  
  evt.preventDefault();  
  try {  
    const result = await this.$validator.validateAll();  
    if (!result) {  
      return;  
    }  
    if (this.editing) {  
      await this.updateContact(this.contact, this.contactId);  
      await this.getAllContacts();  
      this.$emit("contactSaved");  
    } 
    else {  
      await this.addContact(this.contact);  
      await this.getAllContacts();  
      this.$router.push("/");  
    }  
  } 
  catch (ex) {  
    console.log(ex);  
  }  
},

We need evt.preventDefault() to stop the form from submitting the normal way, i.e. without calling the Ajax code below.

this.$validator.validateAll() validates the form.

this.$validator is an object provided by Vee-Validate.

It returns a promise, so we need the function to be async, and we need await before the function call.

If result is falsy, the form validation failed, so we run return to stop the rest of the function from executing. Finally, if form fields are all valid, we can submit. Since this form is used for both adding and editing contacts, we have to check which action we’re doing. If we edit, then we call await this.updateContact(this.contact, this.contactId); to update our contact. Otherwise, we add contact so we call await this.addContact(this.contact);

In either case, we call await this.getAllContacts(); to refresh our contacts and put them in the store. If we are adding, then we will redirect to the home page at the end by calling this.$router.push(“/”); . this.updateContact , this.addContact , and this.getAllContacts are all from our contactMixin which we will write shortly.

Next, we write some helper code.

Create a folder called helpers and make a file within it called export.js — put in the following:

export const COUNTRIES = [  
    { "name": "Afghanistan", "code": "AF" },  
    { "name": "Aland Islands", "code": "AX" },  
    { "name": "Albania", "code": "AL" },  
    { "name": "Algeria", "code": "DZ" },  
    { "name": "American Samoa", "code": "AS" },  
    { "name": "AndorrA", "code": "AD" },  
    { "name": "Angola", "code": "AO" },  
    { "name": "Anguilla", "code": "AI" },  
    { "name": "Antarctica", "code": "AQ" },  
    { "name": "Antigua and Barbuda", "code": "AG" },  
    { "name": "Argentina", "code": "AR" },  
    { "name": "Armenia", "code": "AM" },  
    { "name": "Aruba", "code": "AW" },  
    { "name": "Australia", "code": "AU" },  
    { "name": "Austria", "code": "AT" },  
    { "name": "Azerbaijan", "code": "AZ" },  
    { "name": "Bahamas", "code": "BS" },  
    { "name": "Bahrain", "code": "BH" },  
    { "name": "Bangladesh", "code": "BD" },  
    { "name": "Barbados", "code": "BB" },  
    { "name": "Belarus", "code": "BY" },  
    { "name": "Belgium", "code": "BE" },  
    { "name": "Belize", "code": "BZ" },  
    { "name": "Benin", "code": "BJ" },  
    { "name": "Bermuda", "code": "BM" },  
    { "name": "Bhutan", "code": "BT" },  
    { "name": "Bolivia", "code": "BO" },  
    { "name": "Bosnia and Herzegovina", "code": "BA" },  
    { "name": "Botswana", "code": "BW" },  
    { "name": "Bouvet Island", "code": "BV" },  
    { "name": "Brazil", "code": "BR" },  
    { "name": "British Indian Ocean Territory", "code": "IO" },  
    { "name": "Brunei Darussalam", "code": "BN" },  
    { "name": "Bulgaria", "code": "BG" },  
    { "name": "Burkina Faso", "code": "BF" },  
    { "name": "Burundi", "code": "BI" },  
    { "name": "Cambodia", "code": "KH" },  
    { "name": "Cameroon", "code": "CM" },  
    { "name": "Canada", "code": "CA" },  
    { "name": "Cape Verde", "code": "CV" },  
    { "name": "Cayman Islands", "code": "KY" },  
    { "name": "Central African Republic", "code": "CF" },  
    { "name": "Chad", "code": "TD" },  
    { "name": "Chile", "code": "CL" },  
    { "name": "China", "code": "CN" },  
    { "name": "Christmas Island", "code": "CX" },  
    { "name": "Cocos (Keeling) Islands", "code": "CC" },  
    { "name": "Colombia", "code": "CO" },  
    { "name": "Comoros", "code": "KM" },  
    { "name": "Congo", "code": "CG" },  
    { "name": "Congo, The Democratic Republic of the", "code": "CD" },  
    { "name": "Cook Islands", "code": "CK" },  
    { "name": "Costa Rica", "code": "CR" },  
    {  
        "name": "Cote D\\"Ivoire", "code": "CI"  
    },  
    { "name": "Croatia", "code": "HR" },  
    { "name": "Cuba", "code": "CU" },  
    { "name": "Cyprus", "code": "CY" },  
    { "name": "Czech Republic", "code": "CZ" },  
    { "name": "Denmark", "code": "DK" },  
    { "name": "Djibouti", "code": "DJ" },  
    { "name": "Dominica", "code": "DM" },  
    { "name": "Dominican Republic", "code": "DO" },  
    { "name": "Ecuador", "code": "EC" },  
    { "name": "Egypt", "code": "EG" },  
    { "name": "El Salvador", "code": "SV" },  
    { "name": "Equatorial Guinea", "code": "GQ" },  
    { "name": "Eritrea", "code": "ER" },  
    { "name": "Estonia", "code": "EE" },  
    { "name": "Ethiopia", "code": "ET" },  
    { "name": "Falkland Islands (Malvinas)", "code": "FK" },  
    { "name": "Faroe Islands", "code": "FO" },  
    { "name": "Fiji", "code": "FJ" },  
    { "name": "Finland", "code": "FI" },  
    { "name": "France", "code": "FR" },  
    { "name": "French Guiana", "code": "GF" },  
    { "name": "French Polynesia", "code": "PF" },  
    { "name": "French Southern Territories", "code": "TF" },  
    { "name": "Gabon", "code": "GA" },  
    { "name": "Gambia", "code": "GM" },  
    { "name": "Georgia", "code": "GE" },  
    { "name": "Germany", "code": "DE" },  
    { "name": "Ghana", "code": "GH" },  
    { "name": "Gibraltar", "code": "GI" },  
    { "name": "Greece", "code": "GR" },  
    { "name": "Greenland", "code": "GL" },  
    { "name": "Grenada", "code": "GD" },  
    { "name": "Guadeloupe", "code": "GP" },  
    { "name": "Guam", "code": "GU" },  
    { "name": "Guatemala", "code": "GT" },  
    { "name": "Guernsey", "code": "GG" },  
    { "name": "Guinea", "code": "GN" },  
    { "name": "Guinea-Bissau", "code": "GW" },  
    { "name": "Guyana", "code": "GY" },  
    { "name": "Haiti", "code": "HT" },  
    { "name": "Heard Island and Mcdonald Islands", "code": "HM" },  
    { "name": "Holy See (Vatican City State)", "code": "VA" },  
    { "name": "Honduras", "code": "HN" },  
    { "name": "Hong Kong", "code": "HK" },  
    { "name": "Hungary", "code": "HU" },  
    { "name": "Iceland", "code": "IS" },  
    { "name": "India", "code": "IN" },  
    { "name": "Indonesia", "code": "ID" },  
    { "name": "Iran, Islamic Republic Of", "code": "IR" },  
    { "name": "Iraq", "code": "IQ" },  
    { "name": "Ireland", "code": "IE" },  
    { "name": "Isle of Man", "code": "IM" },  
    { "name": "Israel", "code": "IL" },  
    { "name": "Italy", "code": "IT" },  
    { "name": "Jamaica", "code": "JM" },  
    { "name": "Japan", "code": "JP" },  
    { "name": "Jersey", "code": "JE" },  
    { "name": "Jordan", "code": "JO" },  
    { "name": "Kazakhstan", "code": "KZ" },  
    { "name": "Kenya", "code": "KE" },  
    { "name": "Kiribati", "code": "KI" },  
    {  
        "name": "Korea, Democratic People\"s Republic of", "code": "KP"  
    },  
    { "name": "Korea, Republic of", "code": "KR" },  
    { "name": "Kuwait", "code": "KW" },  
    { "name": "Kyrgyzstan", "code": "KG" },  
    {  
        "name": "Lao People\"s Democratic Republic", "code": "LA"  
    },  
    { "name": "Latvia", "code": "LV" },  
    { "name": "Lebanon", "code": "LB" },  
    { "name": "Lesotho", "code": "LS" },  
    { "name": "Liberia", "code": "LR" },  
    { "name": "Libyan Arab Jamahiriya", "code": "LY" },  
    { "name": "Liechtenstein", "code": "LI" },  
    { "name": "Lithuania", "code": "LT" },  
    { "name": "Luxembourg", "code": "LU" },  
    { "name": "Macao", "code": "MO" },  
    { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" },  
    { "name": "Madagascar", "code": "MG" },  
    { "name": "Malawi", "code": "MW" },  
    { "name": "Malaysia", "code": "MY" },  
    { "name": "Maldives", "code": "MV" },  
    { "name": "Mali", "code": "ML" },  
    { "name": "Malta", "code": "MT" },  
    { "name": "Marshall Islands", "code": "MH" },  
    { "name": "Martinique", "code": "MQ" },  
    { "name": "Mauritania", "code": "MR" },  
    { "name": "Mauritius", "code": "MU" },  
    { "name": "Mayotte", "code": "YT" },  
    { "name": "Mexico", "code": "MX" },  
    { "name": "Micronesia, Federated States of", "code": "FM" },  
    { "name": "Moldova, Republic of", "code": "MD" },  
    { "name": "Monaco", "code": "MC" },  
    { "name": "Mongolia", "code": "MN" },  
    { "name": "Montenegro", "code": "ME" },  
    { "name": "Montserrat", "code": "MS" },  
    { "name": "Morocco", "code": "MA" },  
    { "name": "Mozambique", "code": "MZ" },  
    { "name": "Myanmar", "code": "MM" },  
    { "name": "Namibia", "code": "NA" },  
    { "name": "Nauru", "code": "NR" },  
    { "name": "Nepal", "code": "NP" },  
    { "name": "Netherlands", "code": "NL" },  
    { "name": "Netherlands Antilles", "code": "AN" },  
    { "name": "New Caledonia", "code": "NC" },  
    { "name": "New Zealand", "code": "NZ" },  
    { "name": "Nicaragua", "code": "NI" },  
    { "name": "Niger", "code": "NE" },  
    { "name": "Nigeria", "code": "NG" },  
    { "name": "Niue", "code": "NU" },  
    { "name": "Norfolk Island", "code": "NF" },  
    { "name": "Northern Mariana Islands", "code": "MP" },  
    { "name": "Norway", "code": "NO" },  
    { "name": "Oman", "code": "OM" },  
    { "name": "Pakistan", "code": "PK" },  
    { "name": "Palau", "code": "PW" },  
    { "name": "Palestinian Territory, Occupied", "code": "PS" },  
    { "name": "Panama", "code": "PA" },  
    { "name": "Papua New Guinea", "code": "PG" },  
    { "name": "Paraguay", "code": "PY" },  
    { "name": "Peru", "code": "PE" },  
    { "name": "Philippines", "code": "PH" },  
    { "name": "Pitcairn", "code": "PN" },  
    { "name": "Poland", "code": "PL" },  
    { "name": "Portugal", "code": "PT" },  
    { "name": "Puerto Rico", "code": "PR" },  
    { "name": "Qatar", "code": "QA" },  
    { "name": "Reunion", "code": "RE" },  
    { "name": "Romania", "code": "RO" },  
    { "name": "Russian Federation", "code": "RU" },  
    { "name": "RWANDA", "code": "RW" },  
    { "name": "Saint Helena", "code": "SH" },  
    { "name": "Saint Kitts and Nevis", "code": "KN" },  
    { "name": "Saint Lucia", "code": "LC" },  
    { "name": "Saint Pierre and Miquelon", "code": "PM" },  
    { "name": "Saint Vincent and the Grenadines", "code": "VC" },  
    { "name": "Samoa", "code": "WS" },  
    { "name": "San Marino", "code": "SM" },  
    { "name": "Sao Tome and Principe", "code": "ST" },  
    { "name": "Saudi Arabia", "code": "SA" },  
    { "name": "Senegal", "code": "SN" },  
    { "name": "Serbia", "code": "RS" },  
    { "name": "Seychelles", "code": "SC" },  
    { "name": "Sierra Leone", "code": "SL" },  
    { "name": "Singapore", "code": "SG" },  
    { "name": "Slovakia", "code": "SK" },  
    { "name": "Slovenia", "code": "SI" },  
    { "name": "Solomon Islands", "code": "SB" },  
    { "name": "Somalia", "code": "SO" },  
    { "name": "South Africa", "code": "ZA" },  
    { "name": "South Georgia and the South Sandwich Islands", "code": "GS" },  
    { "name": "Spain", "code": "ES" },  
    { "name": "Sri Lanka", "code": "LK" },  
    { "name": "Sudan", "code": "SD" },  
    { "name": "Suriname", "code": "SR" },  
    { "name": "Svalbard and Jan Mayen", "code": "SJ" },  
    { "name": "Swaziland", "code": "SZ" },  
    { "name": "Sweden", "code": "SE" },  
    { "name": "Switzerland", "code": "CH" },  
    { "name": "Syrian Arab Republic", "code": "SY" },  
    { "name": "Taiwan, Province of China", "code": "TW" },  
    { "name": "Tajikistan", "code": "TJ" },  
    { "name": "Tanzania, United Republic of", "code": "TZ" },  
    { "name": "Thailand", "code": "TH" },  
    { "name": "Timor-Leste", "code": "TL" },  
    { "name": "Togo", "code": "TG" },  
    { "name": "Tokelau", "code": "TK" },  
    { "name": "Tonga", "code": "TO" },  
    { "name": "Trinidad and Tobago", "code": "TT" },  
    { "name": "Tunisia", "code": "TN" },  
    { "name": "Turkey", "code": "TR" },  
    { "name": "Turkmenistan", "code": "TM" },  
    { "name": "Turks and Caicos Islands", "code": "TC" },  
    { "name": "Tuvalu", "code": "TV" },  
    { "name": "Uganda", "code": "UG" },  
    { "name": "Ukraine", "code": "UA" },  
    { "name": "United Arab Emirates", "code": "AE" },  
    { "name": "United Kingdom", "code": "GB" },  
    { "name": "United States", "code": "US" },  
    { "name": "United States Minor Outlying Islands", "code": "UM" },  
    { "name": "Uruguay", "code": "UY" },  
    { "name": "Uzbekistan", "code": "UZ" },  
    { "name": "Vanuatu", "code": "VU" },  
    { "name": "Venezuela", "code": "VE" },  
    { "name": "Viet Nam", "code": "VN" },  
    { "name": "Virgin Islands, British", "code": "VG" },  
    { "name": "Virgin Islands, U.S.", "code": "VI" },  
    { "name": "Wallis and Futuna", "code": "WF" },  
    { "name": "Western Sahara", "code": "EH" },  
    { "name": "Yemen", "code": "YE" },  
    { "name": "Zambia", "code": "ZM" },  
    { "name": "Zimbabwe", "code": "ZW" }  
]

This provides the countries that we reference in ContactForm.vue .

Next, we add our mixin to manipulate our contacts by communicating with our back end. We make a folder call mixins and create a file called contactMixin.js within it.

In the file, we put:

const axios = require('axios');  
const apiUrl = 'http://localhost:3000';

export const contactMixin = {  
    methods: {  
        getContacts() {  
            return axios.get(`${apiUrl}/contacts`);  
        }, 

        addContact(data) {  
            return axios.post(`${apiUrl}/contacts`, data);  
        }, 

        updateContact(data, id) {  
            return axios.put(`${apiUrl}/contacts/${id}`, data);  
        }, 

        deleteContact(id) {  
            return axios.delete(`${apiUrl}/contacts/${id}`);  
        }  
    }  
}

This will let us include our functions in the methods object of the component object we include or mixin with by putting it in the mixins array of our component object.

Then we add our pages. To do this, create a views folder if it doesn’t already exists and add ContactFormPage.vue .

In there, put:

<template>  
  <div class="about">  
    <ContactForm :edit="false" />  
  </div>  
</template>

<script>  
// @ is an alias to /src  
import ContactForm from "@/components/ContactForm.vue";

export default {  
  name: "ContactFormPage",  
  components: {  
    ContactForm  
  }  
};  
</script>

This just displays the ContactForm component that we created. We set the :edit prop to false so that it’ll add our contact instead of editing.

Next, we add our home page to display a list of contacts. In the views folder, we add a file called Home.vue if it doesn’t already exist. In there we put:

<template>  
  <div class="home">  
    <div class="center">  
      <h1>Address Book Home</h1>  
    </div>  
    <md-table>  
      <md-table-row>  
        <md-table-head md-numeric>ID</md-table-head>  
        <md-table-head>First Name</md-table-head>  
        <md-table-head>Last Name</md-table-head>  
        <md-table-head>Address Line 1</md-table-head>  
        <md-table-head>Address Line 2</md-table-head>  
        <md-table-head>City</md-table-head>  
        <md-table-head>Country</md-table-head>  
        <md-table-head>Postal Code</md-table-head>  
        <md-table-head>Gender</md-table-head>  
        <md-table-head>Age</md-table-head>  
        <md-table-head>Email</md-table-head>  
        <md-table-head></md-table-head>  
        <md-table-head></md-table-head>  
      </md-table-row><md-table-row v-for="c in contacts" :key="c.id">  
        <md-table-cell md-numeric>{{c.id}}</md-table-cell>  
        <md-table-cell>{{c.firstName}}</md-table-cell>  
        <md-table-cell>{{c.lastName}}</md-table-cell>  
        <md-table-cell>{{c.addressLineOne}}</md-table-cell>  
        <md-table-cell>{{c.addressLineTwo}}</md-table-cell>  
        <md-table-cell>{{c.city}}</md-table-cell>  
        <md-table-cell>{{c.country}}</md-table-cell>  
        <md-table-cell>{{c.postalCode}}</md-table-cell>  
        <md-table-cell>{{c.gender}}</md-table-cell>  
        <md-table-cell md-numeric>{{c.age}}</md-table-cell>  
        <md-table-cell>{{c.email}}</md-table-cell>  
        <md-table-cell>  
          <md-button class="md-primary" @click="selectedContactId = c.id; showDialog = true">Edit</md-button>  
        </md-table-cell>  
        <md-table-cell>  
          <md-button class="md-accent" @click="removeContact(c.id)">Delete</md-button>  
        </md-table-cell>  
      </md-table-row>  
    </md-table><md-dialog :md-active.sync="showDialog">  
      <md-dialog-content>  
        <ContactForm  
          :editing="true"  
          :contactId="selectedContactId"  
          @contactSaved="selectedContactId = undefined; showDialog = false"  
        />  
      </md-dialog-content>  
    </md-dialog>  
  </div>  
</template>

<script>  
import { contactMixin } from "@/mixins/contactMixin";  
import ContactForm from "@/components/ContactForm.vue";

export default {  
  name: "HomePage",  
  mixins: [contactMixin],  
  components: {  
    ContactForm  
  },  
  props: {  
    editing: Boolean,  
    id: Number  
  },  
  computed: {  
    contacts() {  
      return this.$store.state.contacts;  
    }  
  },  
  data() {  
    return {  
      showDialog: false,  
      selectedContactId: undefined  
    };  
  }, 

  beforeMount() {  
    this.getAllContacts();  
  }, 

  methods: {  
    async getAllContacts() {  
      try {  
        const response = await this.getContacts();  
        this.$store.commit("setContacts", response.data);  
      } catch (ex) {  
        console.log(ex);  
      }  
    }, 

    async removeContact(id) {  
      try {  
        await this.deleteContact(id);  
        await this.getAllContacts();  
      } catch (ex) {  
        console.log(ex);  
      }  
    }  
  }  
};  
</script>

<style scoped>  
.md-dialog-container {  
  padding: 20px;  
}

.md-content.md-table.md-theme-default {  
  width: 95%;  
  margin: 0 auto;  
}  
</style>

We get our contacts during page load by call the this.getAllContacts function in the beforeMount function. Notice that we have this.getContacts function from our mixin. Mixins allows us to reuse code.

Code in our mixinx cannot have the same name as the functions in our methods objects in our components because mixin functions hooks straight into our methods, since we exported an object with methods field in our Mixin code.

In App.vue , we add our menu and top bar by putting the following:

<template>  
  <div id="app">  
    <md-toolbar class="md-accent">  
      <md-button class="md-icon-button" @click="showNavigation = true">  
        <md-icon>menu</md-icon>  
      </md-button>  
      <h3 class="md-title">Vee Validate Address Book App</h3>  
    </md-toolbar>  
    <md-drawer :md-active.sync="showNavigation" md-swipeable>  
      <md-toolbar class="md-transparent" md-elevation="0">  
        <span class="md-title">Vee Validate Address Book App</span>  
      </md-toolbar><md-list>  
        <md-list-item>  
          <router-link to="/">  
            <span class="md-list-item-text">Home</span>  
          </router-link>  
        </md-list-item><md-list-item>  
          <router-link to="/contact">  
            <span class="md-list-item-text">Add Contact</span>  
          </router-link>  
        </md-list-item>  
      </md-list>  
    </md-drawer>
    <router-view />  
  </div>  
</template>

<script>  
export default {  
  name: "app",  
  data: () => {  
    return {  
      showNavigation: false  
    };  
  }  
};  
</script>

<style lang="scss">  
.center {  
  text-align: center;  
}  
</style>

In main.js , we add our boilerplate code to include Vue Material and Vee-Validate in our app:

import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
import VueMaterial from 'vue-material'  
import 'vue-material/dist/vue-material.min.css'  
import VeeValidate from 'vee-validate';Vue.use(VeeValidate);  
Vue.use(VueMaterial);Vue.config.productionTip = falsenew Vue({  
  router,  
  store,  
  render: h => h(App)  
}).$mount('#app')

In router.js , we add our routes so we can see our pages:

import Vue from 'vue'  
import Router from 'vue-router'  
import HomePage from './views/HomePage.vue'  
import ContactFormPage from './views/ContactFormPage.vue'
Vue.use(Router)

export default new Router({  
  mode: 'history',  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: '/',  
      name: 'home',  
      component: HomePage  
    },  
    {  
      path: '/contact',  
      name: 'contact',  
      component: ContactFormPage  
    }  
  ]  
})

In store.js , we put:

import Vue from 'vue'  
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({  
  state: {  
    contacts: []  
  },  
  mutations: {  
    setContacts(state, payload) {  
      state.contacts = payload;  
    }  
  },  
  actions: {}  
})

to store our contact in a place where all components can access. The store uses the Vuex library so that we have a this.$store object to call our mutation with the this.$store.commit function and get the latest data from the store via the computed property of our component object, like so:

contacts() {  
  return this.$store.state.contacts;  
}

Finally in index.html , we put:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
  <link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
  <link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default.css">
  <title>Address Book App</title>
</head>
<body>
  <noscript>
    <strong>We're sorry but vee-validate-tutorial-app doesn't work properly without JavaScript enabled. Please enable it
      to continue.</strong>
  </noscript>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>
</html>

to add the Roboto font and Material icons to our app.

Now we are ready to start our JSON server. Go to our project folder and run json-server — watch db.json to start the server. It will allow us to call these routes without any configuration:

GET    /contacts  
POST   /contacts  
PUT    /contacts/1  
DELETE /contacts/1

These are all the routes we need. Data will be saved to db.json of the folder that we’re in, which should be our app’s project folder.

Categories
JavaScript Nodejs React

How To Build A Chat App With React, Socket.io, And Express

WebSockets is a great technology for adding real time communication to your apps. It works by allowing apps to send events to another app, passing data along with it. This means that users can see new data on their screen without manually retrieving new data, allowing better interactivity and making the user experience easier for the user. HTTP also has a lot of overhead with sending data that not all apps need like headers, this increases the latency of the communication between apps.

Socket.io is a library that uses both WebSockets and HTTP requests to allow apps to send and receive data between each other. Sending data between apps is almost instant. It works by allow apps to emit events to other apps and the apps receiving the events can handle them the way they like. It also provides namespacing and chat rooms to segregate traffic.

One the best uses of WebSockets and Socket.io is a chat app. Chat apps requires real time communication since messages are sent and received all the time. If we use HTTP requests, we would have to make lots of requests repeatedly to do something similar. It will be very slow and taxing on computing and networking resources if we send requests all the time to get new messages.

In this article, we will build a chat app that allows you to join multiple chat rooms and send messages with different chat handles. Chat handle is the username you use for joining the chat. We will use React for front end, and Express for back end. Socket.io client will be used on the front end and Socket.io server will be used on the back end.

To start we make an empty folder for our project and then inside the folder we make a folder called backend for our back end project. Then we go into the backend folder and run the Express Generator to generate the initial code for the back end app. To do this, run npx express-generator . Then in the same folder, run npm install to install the packages. We will need to add more packages to our back end app. We need Babel to use the latest JavaScript features, including the import syntax for importing modules, which is not yet supported by the latest versions of Node.js. We also need the CORS package to allow front end to communicate with back end. Sequelize is needed for manipulate our database, which we will use for storing chat room and chat message data. Sequelize is a popular ORM for Node.js. We also need the dotenv package to let us retrieve our database credentials from environment variables. Postgres will be our database system of choice to store the data.

We run npm i @babel/cli @babel/core @babel/node @babel/preset-env cors dotenv pg pg-hstore sequelize sequelize-cli socket.io to install the packages. After installing the packages, we will run npx sequelize-cli init in the same folder to add the code needed to use Sequelize for creating models and migrations.

Now we need to configure Babel so that we can run our app with the latest JavaScript syntax. First, create a file called .babelrc in the backend folder and add:

{  
    "presets": [  
        "@babel/preset-env"  
    ]  
}

Next we replace the scripts section of package.json with:

"scripts": {  
    "start": "nodemon --exec npm run babel-node --  ./bin/www",  
    "babel-node": "babel-node"  
},

Note that we also have to install nodemon by running npm i -g nodemon so that the app will restart whenever file changes, making it easier for us to develop the app. Now if we run npm start , we should be able to run with the latest JavaScript features in our app.

Next we have to change config.json created by running npx sequelize init . Rename config.json to config.js and replace the existing code with:

require("dotenv").config();  
const dbHost = process.env.DB_HOST;  
const dbName = process.env.DB_NAME;  
const dbUsername = process.env.DB_USERNAME;  
const dbPassword = process.env.DB_PASSWORD;  
const dbPort = process.env.DB_PORT || 5432;

module.exports = {  
  development: {  
    username: dbUsername,  
    password: dbPassword,  
    database: dbName,  
    host: dbHost,  
    port: dbPort,  
    dialect: "postgres",  
  },  
  test: {  
    username: dbUsername,  
    password: dbPassword,  
    database: "chat_app_test",  
    host: dbHost,  
    port: dbPort,  
    dialect: "postgres",  
  },  
  production: {  
    use_env_variable: "DATABASE_URL",  
    username: dbUsername,  
    password: dbPassword,  
    database: dbName,  
    host: dbHost,  
    port: dbPort,  
    dialect: "postgres",  
  },  
};

This is allow us to read the database credentials from our .env located in the backend folder, which should look something like this:

DB_HOST='localhost'  
DB_NAME='chat_app_development'  
DB_USERNAME='postgres'  
DB_PASSWORD='postgres'

Now that we have our database connection configured, we can make some models and migrations. Run npx sequelize model:generate --name ChatRoom --attributes name:string to create the ChatRooms table with the name column and the ChatRoom model in our code along with the associated migration. Next we make the migration and model for storing the messages. Run npx sequelize model:generate --name ChatRoomMessages --attributes author:string,message:text,chatRoomId:integer . Note that in both commands, we use singular word for the model name. There should also be no spaces after the comma in the column definitions.

Next we add a unique constraint to the name column of the ChatRooms table. Create a new migration by running npx sequelize-cli migration:create add-unique-constraint-for-chatroom-name to make an empty migration. Then in there, put:

"use strict";

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return queryInterface.addConstraint("ChatRooms", ["name"], {  
      type: "unique",  
      name: "unique_name",  
    });  
  }, 

  down: (queryInterface, Sequelize) => {  
    return queryInterface.removeConstraint("ChatRooms", "unique_name");  
  },  
};

After all that is done, we run npx sequelize-cli db:migrate to run the migrations.

Next in bin/www , we add the code for sending and receiving events with Socket.io. Replace the existing code with:

#!/usr/bin/env node
/**
 * Module dependencies.
 */
const app = require("../app");
const debug = require("debug")("backend:server");
const http = require("http");
const models = require("../models");
/**
 * Get port from environment and store in Express.
 */
const port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
/**
 * Create HTTP server.
 */
const server = http.createServer(app);
const io = require("socket.io")(server);
io.on("connection", socket => {
  socket.on("join", async room => {
    socket.join(room);
    io.emit("roomJoined", room);
  });
  socket.on("message", async data => {
    const { chatRoomName, author, message } = data;
    const chatRoom = await models.ChatRoom.findAll({
      where: { name: chatRoomName },
    });
    const chatRoomId = chatRoom[0].id;
    const chatMessage = await models.ChatMessage.create({
      chatRoomId,
      author,
      message: message,
    });
    io.emit("newMessage", chatMessage);
  });
});
/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port);
server.on("error", onError);
server.on("listening", onListening);
/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
  const port = parseInt(val, 10);
  if (isNaN(port)) {
    // named pipe
    return val;
  }
  if (port >= 0) {
    // port number
    return port;
  }
  return false;
}
/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
  if (error.syscall !== "listen") {
    throw error;
  }
  const bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
// handle specific listen errors with friendly messages
  switch (error.code) {
    case "EACCES":
      console.error(bind + " requires elevated privileges");
      process.exit(1);
      break;
    case "EADDRINUSE":
      console.error(bind + " is already in use");
      process.exit(1);
      break;
    default:
      throw error;
  }
}
/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  const addr = server.address();
  const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
  debug("Listening on " + bind);
}

so that the app will listen to connect from clients, and let the join rooms when the join event is received. We process messages received with the message event in this block of code:

socket.on("message", async data => {  
    const { chatRoomName, author, message } = data;  
    const chatRoom = await models.ChatRoom.findAll({  
      where: { name: chatRoomName },  
    });  
    const chatRoomId = chatRoom\[0\].id;  
    const chatMessage = await models.ChatMessage.create({  
      chatRoomId,  
      author,  
      message: message,  
    });  
    io.emit("newMessage", chatMessage);  
  });

and emit a newMessage event once the message sent with the message event is saved by getting the chat room ID and saving everything to the ChatMessages table.

In our models, we have to create a has many relationship between the ChatRooms and ChatMessages table by changing our model code. In chatmessage.js , we put:

'use strict';
module.exports = (sequelize, DataTypes) => {
  const ChatMessage = sequelize.define('ChatMessage', {
    chatRoomId: DataTypes.INTEGER,
    author: DataTypes.STRING,
    message: DataTypes.TEXT
  }, {});
  ChatMessage.associate = function(models) {
    // associations can be defined here
    ChatMessage.belongsTo(models.ChatRoom, {
      foreignKey: 'chatRoomId',
      targetKey: 'id'
    });
  };
  return ChatMessage;
};

to make the ChatMessages table belong to the ChatRooms table.

In ChatRoom.js , we put:

"use strict";  
module.exports = (sequelize, DataTypes) => {  
  const ChatRoom = sequelize.define(  
    "ChatRoom",  
    {  
      name: DataTypes.STRING,  
    },  
    {}  
  );  
  ChatRoom.associate = function(models) {  
    // associations can be defined here  
    ChatRoom.hasMany(models.ChatMessage, {  
      foreignKey: "chatRoomId",  
      sourceKey: "id",  
    });  
  };  
  return ChatRoom;  
};

so that we make each ChatRoom have many ChatMessages .

Next we need to add some routes to our back end for getting and setting chat rooms, and getting messages messages. Create a new file called chatRoom.js in the routes folder and add:

const express = require("express");
const models = require("../models");
const router = express.Router();
/* GET users listing. */
router.get("/chatrooms", async (req, res, next) => {
  const chatRooms = await models.ChatRoom.findAll();
  res.send(chatRooms);
});
router.post("/chatroom", async (req, res, next) => {
  const room = req.body.room;
  const chatRooms = await models.ChatRoom.findAll({
    where: { name: room },
  });
  const chatRoom = chatRooms[0];
  if (!chatRoom) {
    await models.ChatRoom.create({ name: room });
  }
  res.send(chatRooms);
});
router.get("/chatroom/messages/:chatRoomName", async (req, res, next) => {
  try {
    const chatRoomName = req.params.chatRoomName;
    const chatRooms = await models.ChatRoom.findAll({
      where: {
        name: chatRoomName,
      },
    });
    const chatRoomId = chatRooms[0].id;
    const messages = await models.ChatMessage.findAll({
      where: {
        chatRoomId,
      },
    });
    res.send(messages);
  } catch (error) {
    res.send([]);
  }
});
module.exports = router;

The /chatrooms route get all the chat rooms from the database. The chatroom POST route adds a new chat room if it does not yet exist by looking up any existing one by name. The /chatroom/messages/:chatRoomName route gets the messages for a given chat room by chat room name.

Finally in app.js , we replace the existing code with:

var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require("./routes/index");
var chatRoomRouter = require("./routes/chatRoom");
var app = express();
const cors = require("cors");

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());
app.use("/", indexRouter);
app.use("/chatroom", chatRoomRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

and add our chat room routes by adding:

app.use("/chatroom", chatRoomRouter);

Now that back end is done, we can build our front end. Go to the project’s root folder and run npx create-react-app frontend . This create the initial code for front end with the packages installed. Next we need to install some packages ourselves. Run npm i axios bootstrap formik react-bootstrap react-router-dom socket.io-client yup to install our Axios HTTP client, Bootstrap for styling, React Router for routing URLs to our pages, and Formik and Yup for easy form data handling and validation respectively.

After we installed our packages, we can write some code. All files we change are in the src folder except when the path is mentioned explicitly. First, in App.js , we change the existing code to the following:

import React from "react";  
import { Router, Route, Link } from "react-router-dom";  
import HomePage from "./HomePage";  
import TopBar from "./TopBar";  
import { createBrowserHistory as createHistory } from "history";  
import "./App.css";  
import ChatRoomPage from "./ChatRoomPage";  
const history = createHistory();function App() { return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/chatroom" exact component={ChatRoomPage} />  
      </Router>  
    </div>  
  );  
}

export default App;

To define our routes and include the top bar in our app, which will build later. Then in App.css , replace the existing code with:

.App {  
  margin: 0 auto;  
}

Next create a new page called ChatRoomPage.js and add the following:

import React from "react";
import { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import io from "socket.io-client";
import "./ChatRoomPage.css";
import { getChatRoomMessages, getChatRooms } from "./requests";
const SOCKET_IO_URL = "http://localhost:3000";
const socket = io(SOCKET_IO_URL);
const getChatData = () => {
  return JSON.parse(localStorage.getItem("chatData"));
};
const schema = yup.object({
  message: yup.string().required("Message is required"),
});
function ChatRoomPage() {
  const [initialized, setInitialized] = useState(false);
  const [messages, setMessages] = useState([]);
  const [rooms, setRooms] = useState([]);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    const data = Object.assign({}, evt);
    data.chatRoomName = getChatData().chatRoomName;
    data.author = getChatData().handle;
    data.message = evt.message;
    socket.emit("message", data);
  };
  const connectToRoom = () => {
    socket.on("connect", data => {
      socket.emit("join", getChatData().chatRoomName);
    });
    socket.on("newMessage", data => {
      getMessages();
    });
    setInitialized(true);
  };
  const getMessages = async () => {
    const response = await getChatRoomMessages(getChatData().chatRoomName);
    setMessages(response.data);
    setInitialized(true);
  };
  const getRooms = async () => {
    const response = await getChatRooms();
    setRooms(response.data);
    setInitialized(true);
  };
  useEffect(() => {
   if (!initialized) {
      getMessages();
      connectToRoom();
      getRooms();
    }
  });
  return (
    <div className="chat-room-page">
      <h1>
        Chat Room: {getChatData().chatRoomName}. Chat Handle:{" "}
        {getChatData().handle}
      </h1>
      <div className="chat-box">
        {messages.map((m, i) => {
          return (
            <div className="col-12" key={i}>
              <div className="row">
                <div className="col-2">{m.author}</div>
                <div className="col">{m.message}</div>
                <div className="col-3">{m.createdAt}</div>
              </div>
            </div>
          );
        })}
      </div>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="handle">
                <Form.Label>Message</Form.Label>
                <Form.Control
                  type="text"
                  name="message"
                  placeholder="Message"
                  value={values.message || ""}
                  onChange={handleChange}
                  isInvalid={touched.message && errors.message}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.message}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Send
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}
export default ChatRoomPage;

This contains our main chat room code. The user will see the content of this page after going through the home page where they will fill out their chat handle and chat room name. First we connect to our Socket.io server by running const socket = io(SOCKET_IO_URL); Then, we connect to the given chat room name , which we stored in local storage in the connectToRoom function. The function will have the handler for the connect event, which is executed after the connect event is received. Once the event is received, the the client emits the join event by running socket.emit(“join”, getChatData().chatRoomName); , which sends the join event with our chat room name. Once the join event is received by the server. It will call the socket.join function in its event handler. Whenever the user submits a message the handleSubmit function is called, which emits the message event to our Socket.io server. Once the message is delivered to the server, it will save the message to the database and then emit the newMessage event back to the front end. The front end will then get the latest messages using the route we defined in back end using an HTTP request.

Note that we send the chat data to the server via Socket.io instead of HTTP requests, so that all users in the chat room will get the same data right away since the newMessage event will be broadcasted to all clients.

We create a file called ChatRoom.css , then in the file, add:

.chat-room-page {
  width: 90vw;
  margin: 0 auto;
}
.chat-box {
  height: calc(100vh - 300px);
  overflow-y: scroll;
}

Next we create the home page, which is the first page that the users sees when the user first opens the app. It is where the user will enter their chat handle and the name of the chat room. Create a file called HomePage.js and add:

import React from "react";
import { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import { Redirect } from "react-router";
import "./HomePage.css";
import { joinRoom } from "./requests";
const schema = yup.object({
  handle: yup.string().required("Handle is required"),
  chatRoomName: yup.string().required("Chat room is required"),
});
function HomePage() {
  const [redirect, setRedirect] = useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    localStorage.setItem("chatData", JSON.stringify(evt));
    await joinRoom(evt.chatRoomName);
    setRedirect(true);
  };
  if (redirect) {
    return <Redirect to="/chatroom" />;
  }
  return (
    <div className="home-page">
      <h1>Join Chat</h1>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={JSON.parse(localStorage.getItem("chatData") || "{}")}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="handle">
                <Form.Label>Handle</Form.Label>
                <Form.Control
                  type="text"
                  name="handle"
                  placeholder="Handle"
                  value={values.handle || ""}
                  onChange={handleChange}
                  isInvalid={touched.handle && errors.handle}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.firstName}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="chatRoomName">
                <Form.Label>Chat Room Name</Form.Label>
                <Form.Control
                  type="text"
                  name="chatRoomName"
                  placeholder="Chat Room Name"
                  value={values.chatRoomName || ""}
                  onChange={handleChange}
                  isInvalid={touched.chatRoomName && errors.chatRoomName}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.chatRoomName}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Join
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}
export default HomePage;

Once the user enters the data into the form, it will be checked if they are filled in and once they are, a request will be sent to back end to add the chat room if it is not there. We also save the filled in data to local storage and redirect the user to the chat room page, where they will connect to the chat room with the name that they entered.

Both forms are built with React Bootstrap’s Form component.

Next we create a file called HomePage.css and add:

.home-page {  
    width: 90vw;  
    margin: 0 auto;  
}

to add some margins to our page.

Then we create a file called requests.js in the src folder to add the code for making the requests to our server for manipulating chat rooms and getting chat messages. In the file, add the following code:

const APIURL = "http://localhost:3000";  
const axios = require("axios");  
export const getChatRooms = () => axios.get(`${APIURL}/chatroom/chatrooms`);
export const getChatRoomMessages = chatRoomName =>  
  axios.get(`${APIURL}/chatroom/chatroom/messages/${chatRoomName}`);
export const joinRoom = room =>  
  axios.post(`${APIURL}/chatroom/chatroom`, { room });

Finally, in we create the top bar. Create a file called TopBar.js and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  const { pathname } = location;
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Chat Room App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            Join Another Chat Room
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

We create the top bar using the Navbar widget provided by React Bootstrap with a link to the home page. We wrap the component with the withRouter function so that we get the location object from React Router.