Categories
JavaScript

Manipulate URLs and Query String with JavaScript

Like any other programming language, JavaScript has many handy tricks that let us write our programs more easily. In this article, we will look at how to manipulate URLs with the URL object with various properties and manipulate query strings with the URLSearchParams object.

Manipulate URLs

With the URL object, we can pass in a URL string and extract and set various parts of a URL and create a new URL. We can create a URL object by using the constructor. The constructor takes up to 2 arguments. We can have one argument being the complete URL string, or we can pass in a relative URL string as the first argument and the hostname as the second argument. For example, we can either write:

new URL('http://medium.com');

or

new URL('/@hohanga', 'http://medium.com');

In the last part, we looked at the hash and host property by looking at the following examples:

const url = new URL('http://example.com/#hash');
console.log(url.hash);
url.hash = 'newHash';
console.log(url.toString());

If we run the code, we can see that the first console.log statement logs '#hash'. Then we assigned the value 'newHash' to the url’s hash property. Then when we run the toString() method on the url object and the run the console.log method on the value returned by toString(), we get '[http://example.com/#newHash](http://example.com/#newHash)' which is the new value of the URL with the new hash.

const url = new URL('http://example.com/#hash');
console.log(url.host);
url.host = 'newExample.com';
console.log(url.toString());

When we run the code, we can see that the first console.log statement logs '#hash'. Then we assigned the value 'example.com' to the url’s hash property. When we run the toString() method on the url object and use the console.log method on the value returned by toString() , we get ‘[http://newexample.com/#hash](http://newexample.com/#hash)’ which is the new value of the URL with the new hash.

In this part of the series, we will look at the other parts of the URL object. To get and set the whole URL we can use the href property. For example, we can write the following code:

const url = new URL('http://example.com/href1/href2');
console.log(url.href);
url.href = 'http://example.com/newHref1/newHref2';
console.log(url.toString());

In the code above, the first console.log statement would get us the whole URL, that is, ‘[http://example.com/href1/href2'](http://example.com/href1/href2%27) . Then once we set a new value to the href property as we did on the third line, then we should get a string with a new URL when we run the console.log statement at the bottom. Therefore, we should get ‘[http://example.com/newHref1/newHref2](http://example.com/newHref1/newHref2)’ from the console.log statement on the last line.

We can use the username and password properties to set the username and password portion of the URL. The username is the part of the URL that’s right after the protocol and before the colon. The password portion of the URL is between the colon and the ampersand sign. For example, https://username:password@example.com. To get and set the username portion of the URL, we can use the username property. For example, we can write:

const url = new URL('https://username:password@example.com');
console.log(url.username);
url.username = 'newUsername';
console.log(url.toString());

The first console.log should output 'username' which is the username of the original URL. Then we set the username property to 'newUsername' so that when returning the URL object converted to a string, we get that we have ‘[https://newUsername:password@example.com/](https://newUsername:password@example.com/)’ .

Likewise, we can do the same thing with the password property. For example, we can write:

const url = new URL('https://username:password@example.com');
console.log(url.password);
url.password = 'newPassword';
console.log(url.toString());

The first console.log should output 'password' which is the username of the original URL. Then we set the password property to 'newPassword' so that when returning the URL object converted to a string, we get that we have ‘[https://newUsername:password@example.com/](https://username:newPassword@example.com/)’ .

With the pathname the property, we can set the part of the URL after the host part — that is, the relative URL after the first slash. For instance, we can write the following code:

const url = new URL('https://example.com/path1/path2');
console.log(url.pathname);
url.pathname = '/newPath1/newPath2';
console.log(url.toString());

The first console.log should output '/path1/path2' , which is the username of the original URL. Then we set the pathname property to '/newPath1/newPath2' so that when returning the URL object converted to a string, we get that we have ‘[https://newUsername:password@example.com/](https://example.com/newPath1/newPath2)’ .

There’s also the port and protocol properties to manipulate the port portion and the protocol portion of the URL respectively. If we have a URL like https://example.com:8888 then 8888 would be the port number. For HTTP the default it 80 and for HTTPS the default port is 443, so if it’s not specified then the default port is used. We can get and set them like all the other properties mentioned above. For example, we can write:

const url = new URL('https://example.com:8888');
console.log(url.port);
url.port = '3333';
console.log(url.toString());

console.log(url.protocol);
url.protocol = 'http';
console.log(url.toString());

Then we should get something like:

8888
https://example.com:3333/
https:
http://example.com:3333/

from the 4 console.log statements.

Manipulate Query Strings

With the URLSearchParams object, we can get the query string from a URL string. For example, if we have had the URL https://example.com/?key1=value1&key2=value2 then we can use the URL’s search property to get the query string from the URL as we do with the following code:

const url = new URL('[https://example.com/?key1=value1&key2=value2'](https://example.com/?key1=value1&key2=value2%27));
console.log(url.search);

Then we get back ?key1=value1&key2=value2 from the console.log output. Note that the search property only gets the whole query string. There’s nothing in the URL object that lets us manipulate the query string. This is where the URLSearchParams object comes in. We can get it from the URL object by using its searchParams property. For example, if we have the same URL as the earlier example, then we can write:

const url = new URL('https://example.com/?key1=value1&key2=value2');
console.log(url.`searchParams`);

With the URLSearchParams object, we can use its methods to get and set various parts of the query string. We can use the get method to get the value of a search parameter given a key for it. For instance, we can write:

const url = new URL('https://example.com/?key1=value1&key2=value2');
console.log(url.searchParams.get('key1'));
console.log(url.searchParams.get('key2'));

Then we get back the following:

value1
value2

These are the values of the search parameters that we expect. There’s also a getAll method to get the values of all search parameters with the given key. If we write the following code:

const url = new URL('https://example.com/?key1=value1&key1=value2&key2=value3');
console.log(url.searchParams.getAll('key1'));

So if we have the URL with the query string above, which has 2 values set for the key1 search parameter, then when we call url.searchParams.getAll(‘key1’) we get back [“value1”, “value2”] .

There’s also a keys method to get all the keys from the query string. Again, if we have URL the example above, we can get all the keys from the query string with the key method like we do below:

const url = new URL('[ttps://example.com/?key1=value1&key1=value2&key2=value3');
console.log([...url.searchParams.keys()]);

The keys method returns an iterator object, so if we want to get an array, we have to use the spread operator as we did above. Once we did that, we get back [“key1”, “key1”, “key2”] .

To check if a key exists in the search parameters of our query string, we can use the has method. It’s very straightforward to use, we just have to pass in the string with the key name we want to look for:

const url = new URL('https://example.com/?key1=value1&key1=value2&key2=value3');
console.log(url.searchParams.has('key1'));
console.log(url.searchParams.has('key4'));

If we run the code above, we should get:

true
false

The key method returns a boolean true if the value for the given key exists and false otherwise. To add a search parameter to the query string, we can use the append method. We can append as many search parameters with the same keys as many times as we like. For example, we can call the append method like in the following code:

const url = new URL('https://example.com/?key1=value1&key1=value2&key2=value3');
url.searchParams.append('key4', 'value4');
url.searchParams.append('key4', 'value5');
url.searchParams.append('key5', 'value5');
console.log(url.toString());

Then the console.log at the end should output https://example.com/?key1=value1&key1=value2&key2=value3&key4=value4&key4=value5&key5=value5.

To edit a key that’s in a URL, we can use the URLSearchParamsset method. It takes in the string of the key of the search parameter as the first argument the string of the value you want to set for the search parameter as the second argument. For example, we call it like in the following code:

const url = new URL('https://example.com/?key1=value1&key1=value2&key2=value3');
url.searchParams.set('key1', 'newValue');
console.log(url.toString());

Then we get back https://example.com/?key1=newValue&key2=value3. The set method will remove duplicate search parameters with the same keys when it runs and set the new value to the one that’s left. So the second instance of the key1 search parameter is removed and newValue is set as the value of the first instance of the key1 search parameter.

Finally, to remove a search parameter with the given key string, we can call the delete method. For example, we can use it as in the following code:

const url = new URL('https://example.com/?key1=value1&key1=value2&key2=value3');
url.searchParams.delete('key1');
console.log(url.toString());

Then we get back https://example.com/?key2=value3. Note that it removes all search parameters with the given key.

Conclusion

JavaScript, like any other programming language, has many handy tricks that let us write our programs more easily. In this article, we looked at how to manipulate URLs with the URL object with various properties and manipulate query strings with the URLSearchParams object. With these tricks, we reduce the effort we put into writing our code making our lives easier.

Categories
JavaScript

Handy JavaScript Tricks

JavaScript, like any other programming language, has many handy tricks that let us write our programs more easily.

In this article, we will look at how to remove duplicate elements, remove falsy values from arrays, create empty objects, and check for required function parameters.

Remove Duplicate Elements

Before ES6, removing duplicate elements from an array was a pain. We had to check if each one exists and, if it already exists, then we remove the entry that duplicates the same one in another place that already exists.

Another way was to make a dictionary that had the array entries as the key and then loop through the dictionary’s key to create another array.

With ES6, we don’t have to do this anymore since we have a new iterable object called Sets. It’s exactly what it sounds like, in that it represents a set as it does in mathematics.

A Set in math is a collection of things that can’t have duplicates. With the Set constructor, we can pass in an array straight into the Set constructor to create a set.

For example, if we have an array that has duplicate elements then we can convert it to a Set and back into an array. For example, we can write:

let arr = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5];
const set = new Set(arr);
arr = Array.from(set);
console.log(arr);

In the code above, we have an array of numbers arr that has many duplicate entries. To remove the duplicate elements, we first create a Set with the arr array.

Then, we convert it back to an array with the Array.from method which takes any array like object as a valid argument and then we assigned it back to arr.

That way, we use the same variable for the array, but all the duplicate elements are gone. If we run the console.log statement in the code above, we get [1, 2, 3, 4, 5] back.

Alternatively, we can replace the Array.from method with the spread operator since a Set is an iterable object, which means that it works with the spread operator. We can write the code above like the following:

let arr = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5];
const set = new Set(arr);
arr = [...set]
console.log(arr);

If we run the code above, we should get the same console.log output as the first example. To make it even shorter, we don’t even have to create the set variable, rather, we just use the spread operator, like in the following code:

let arr = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5];
arr = [...new Set(arr)];
console.log(arr);

This is even shorter and uses less memory since we don’t have to create another variable just to remove duplicate elements. With the spread operator, now we don’t need a loop just for removing duplicate elements from an array.

Remove Falsy Values

To remove falsy values like 0, undefined, null, NaN, or false from an array, we can call the Boolean method to do this, since the Boolean will convert falsy values to the boolean value false.

We can remove those values, like in the following code:

let arr = [1, 2, 3, 4, 5, 6, false, null, undefined, , null, NaN, 0];
arr = arr.filter(a => Boolean(a));
console.log(arr);

In the code above, we have an array arr with some numbers and falsy values, like false, null, undefined, NaN, and 0 in the array as well as a whole in the middle of the array which is interpreted as undefined.

We filter them out by using the Boolean factory function which returns the truth value of these values. The falsy ones are converted to false which are then a new array. With the falsy values removed it returns the filter function.

Then the returned array is assigned back to the arr variable, which makes the arr array take on the new value with the array with the falsy values removed. In the end, we get the console.log output [1, 2, 3, 4, 5, 6].

Since the Boolean function takes in one argument, which is the value that you want to convert to a boolean, and that matches the signature of the callback function that we pass into the filter method, we can make this even shorter by passing in the Boolean function straight into the filter method as its callback function like in the following code:

let arr = [1, 2, 3, 4, 5, 6, false, null, undefined, , null, NaN, 0];
arr = arr.filter(Boolean);
console.log(arr);

With the code above, we should get the same output from the console.log as we have above. Note that we remove the parentheses from the Boolean function.

This is because we are passing in the function reference to the filter method as its callback function. If we pass in a reference to a function as an argument of another function, then we shouldn’t include the parentheses as we aren’t calling the function, we just want to pass in the function reference.

Create Empty Objects

If we want to create a pure empty object without any prototype, we shouldn’t be using the {} literal since this will create an object with the prototype type. If we run:

console.log({}.__proto__)

Or:

console.log(Object.getPrototypeOf({}))

We get stuff like:

constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()

As the output. As we can see, there’s already a bunch of methods that are inherited from the {}’s prototype.

If we don’t want those methods, we can create an object without any prototype by passing in null into the Object.create method like in the following code:

const obj = Object.create(null);
console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj))

The code above will create an object without a prototype, which is why we passed in null into the argument of the Object.create method.

If we run the first console.log statement above, we should get undefined, and for the second one, we should get null.

Check for Required Parameters

With ES6, we can set default values of function parameters in the function signature. We can set a value as the default parameter or we can set the return value of another function as the default parameter.

This means that we can run another function as we’re calling a function that takes arguments. We can use this to check for required values by assigning the default value of a parameter with a function call.

For example, we can write the following code:

const checkRequired = (paramName) => {
  throw new Error(`${paramName} is required`);
};

const greet = (name = checkRequired('name'), greeting = checkRequired('greeting')) => {
  console.log(`Hello ${name}, ${greeting}`)
};

greet('Jane', 'How are you?');
try {
  greet('Jane');
} catch (error) {
  console.log(error);
}

try {
  greet('How are you?');
} catch (error) {
  console.log(error);
}

try {
  greet();
} catch (error) {
  console.log(error);
}

Then, we should get something like this:

Hello Jane, How are you?

Error: greeting is required
    at checkRequired ((index):33)
    at greet ((index):36)
    at window.onload ((index):42)

Error: greeting is required
    at checkRequired ((index):33)
    at greet ((index):36)
    at window.onload ((index):48)

Error: name is required
    at checkRequired ((index):33)
    at greet ((index):36)
    at window.onload ((index):54)

As the output from the console.log statements.

What we were doing with the code above is that we are running the checkRequired function as we’re calling the greet function with the given arguments.

With each function call, we run the checkRequired function as each argument is being passed into the parameters if the argument is undefined or omitted as we set the return value of the checkRequired function to be the default value of the parameters if the argument is omitted or undefined is passed in.

It doesn’t matter what the return of checkRequired is. We just want it to run if the argument value is omitted or it’s undefined. This way, the greeting function won’t run if both arguments aren’t passed in.

JavaScript, like any other programming language, has many handy tricks that let us write our programs more easily.

In this article, we looked at how to remove duplicate elements, remove falsy values from arrays, create empty objects, and check for required function parameters.

With these tricks, we reduce the effort we put into writing our code, making our lives easier.

Categories
Vue

How to Use Axios to Make HTTP Requests in Vue.js

HTTP requests are made in almost all front end web apps. They are needed to communicate with back end to send and receive data. Vue.js apps are no different.

However, it doesn’t come with a HTTP client like Angular does, so we need to add our own. Axios is an easy to use HTTP client that many people use in their apps. It supports basic requests like GET, POST, PUT and DELETE requests. You can send headers with it, and also you can intercept the request and response to do something to something to all HTTP requests before it’s sent, or handle the response in a uniform way.

In this article, we will make a simple Bitbucket app with authentication. We will let users sign up for an account and set their Bitbucket username and password for their account. Once the user is logged in and set their Bitbucket credentials, they can view their repositories and the commits for each repository.

The front end will be a Vue.js app that uses the Vue Router for routing.

Bitbucket is a great repository hosting service that lets you host Git repositories for free. You can upgrade to their paid plans to get more features for a low price. It also has a comprehensive API for automation and getting data.

Developers have made Node.js clients for Bitbucket’s API. We can easily use it to do what we want like manipulating commits, adding, removing, or editing repositories, tracking issues, manipulating build pipelines and a lot more.

The Bitbucket.js package is one of the easiest packages to use for writing Node.js Bitbucket apps. All we have to do is log in with the Bitbucket instance with our Bitbucket username and password and call the built in functions listed at https://bitbucketjs.netlify.com/#api-_ to do almost anything we want with the Bitbucket packages.

Our app will consist of an back end and a front end. The back end handles the user authentication and communicates with Bitbucket via the Bitbucket API. The front end has the sign up form, log in form, a settings form for setting password and Bitbucket username and password.

Back End

To start building the app, we create a project folder with the backend folder inside. We then go into the backend folder and run the Express Generator by running npx express-generator . Next we install some packages ourselves. We need Babel to use import , BCrypt for hashing passwords, Bitbucket.js for using the Bitbucket API. Crypto-JS for encrypting and decrypting our Bitbucket password, Dotenv for storing hash and encryption secrets, Sequelize for ORM, JSON Web Token packages for authentication, CORS for cross domain communication and SQLite for database.

Run npm i @babel/cli @babel/core @babel/node @babel/preset-env bcrypt bitbucket cors crypto-js dotenv jsonwebtoken sequelize sqlite3 to install the packages.

In the script section of package.json , put:

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

to start our app with Babel Node instead of the regular Node.js runtime so that we get the latest JavaScript features.

Then we create a file called .babelrc in the backend folder and add:

{
    "presets": [
        "[@babel/preset-env](http://twitter.com/babel/preset-env "Twitter profile for @babel/preset-env")"
    ]
}

to enable the latest features.

Then we have to add Sequelize code by running npx sequelize-cli init . After that we should get a config.json file in the config folder.

In config.json , we put:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

To use SQLite as our database.

Next we add a middleware for verifying the authentication token, add a middllewares folder in the backend folder, and in there add authCheck.js . In the file, add:

const jwt = require("jsonwebtoken");
const secret = process.env.JWT_SECRET;

export const authCheck = (req, res, next) => {
  if (req.headers.authorization) {
    const token = req.headers.authorization;
    jwt.verify(token, secret, (err, decoded) => {
      if (err) {
        res.send(401);
      } else {
        next();
      }
    });
  } else {
    res.send(401);
  }
};

We return 401 response if the token is invalid.

Next we create some migrations and models. Run:

npx sequelize-cli model:create --name User --attributes username:string,password:string,bitBucketUsername:string,bitBucketPassword:string

to create the model. Note that the attributes option has no spaces.

Then we add unique constraint to the username column of the Users table. To do this, run:

npx sequelize-cli migration:create addUniqueConstraintToUser

Then in newly created migration file, add:

"use strict";

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

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

Run npx sequelize-cli db:migrate to run all the migrations.

Next we create the routes. Create a file called bitbucket.js and add:

var express = require("express");
const models = require("../models");
const CryptoJS = require("crypto-js");
const Bitbucket = require("bitbucket");
const jwt = require("jsonwebtoken");
import { authCheck } from "../middlewares/authCheck";

const bitbucket = new Bitbucket();
var router = express.Router();

router.post("/setBitbucketCredentials", authCheck, async (req, res, next) => {
  const { bitBucketUsername, bitBucketPassword } = req.body;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const cipherText = CryptoJS.AES.encrypt(
    bitBucketPassword,
    process.env.CRYPTO_SECRET
  );
  await models.User.update(
    {
      bitBucketUsername,
      bitBucketPassword: cipherText.toString()
    },
    {
      where: { id }
    }
  );
  res.json({});
});

router.get("/repos/:page", authCheck, async (req, res, next) => {
  const page = req.params.page || 1;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const user = users[0];
  const bytes = CryptoJS.AES.decrypt(
    user.bitBucketPassword.toString(),
    process.env.CRYPTO_SECRET
  );
  const password = bytes.toString(CryptoJS.enc.Utf8);
  bitbucket.authenticate({
    type: "basic",
    username: user.bitBucketUsername,
    password
  });
  let { data } = await bitbucket.repositories.list({
    username: user.bitBucketUsername,
    page,
    sort: "-updated_on"
  });
  res.json(data);
});

router.get("/commits/:repoName", authCheck, async (req, res, next) => {
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const user = users[0];
  const repoName = req.params.repoName;
  const bytes = CryptoJS.AES.decrypt(
    user.bitBucketPassword.toString(),
    process.env.CRYPTO_SECRET
  );
  const password = bytes.toString(CryptoJS.enc.Utf8);
  bitbucket.authenticate({
    type: "basic",
    username: user.bitBucketUsername,
    password
  });
  let { data } = await bitbucket.commits.list({
    username: user.bitBucketUsername,
    repo_slug: repoName,
    sort: "-date"
  });
  res.json(data);
});

module.exports = router;

In each route, we get the user from the token, since we will add the user ID into the token, and from there we get the Bitbucket username and password, which we use to log into the Bitbucket API. Note that we have to decrypt the password since we encrypted it before saving it to the database.

We set the Bitbucket credentials in the setBitbucketCredentials route. We encrypt the password before saving to keep it secure.

Then in the repos route, we get the repos of the user and sort by reversed update_on order since we specified -updated_on in the sort parameter. The commits are listed in reverse date order since we specified -date in the sort parameter.

Next we add the user.js in the routes folder and add:

const express = require("express");
const models = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
import { authCheck } from "../middlewares/authCheck";
const router = express.Router();

router.post("/signup", async (req, res, next) => {
  try {
    const { username, password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = await models.User.create({
      username,
      password: hashedPassword
    });
    res.json(user);
  } catch (error) {
    res.status(400).json(error);
  }
});

router.post("/login", async (req, res, next) => {
  const { username, password } = req.body;
  const users = await models.User.findAll({ where: { username } });
  const user = users[0];
  const response = await bcrypt.compare(password, user.password);
  if (response) {
    const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
    res.json({ token });
  } else {
    res.status(401).json({});
  }
});

router.post("/changePassword", authCheck, async (req, res, next) => {
  const { password } = req.body;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const hashedPassword = await bcrypt.hash(password, 10);
  await models.User.update({ password: hashedPassword }, { where: { id } });
  res.json({});
});

router.get("/currentUser", authCheck, async (req, res, next) => {
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const { username, bitBucketUsername } = users[0];
  res.json({ username, bitBucketUsername });
});

module.exports = router;

We have routes for sign up, log in, and change password. We hash the password before saving when we sign up or changing password.

The currentUser route will be used for a settings form in the front end.

In app.js we replace the existing code with:

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
const cors = require("cors");

var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var bitbucketRouter = require("./routes/bitbucket");

var app = express();

// 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("/users", usersRouter);
app.use("/bitbucket", bitbucketRouter);

// 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;

to add the CORS add-on to enable cross domain communication, and add the users and bitbucket routes in our app by adding:

app.use("/users", usersRouter);
app.use("/bitbucket", bitbucketRouter);

This finishes the back end portion of the app.

Front End

Now we can build the front end.

We will build it with Vue, so we start by running npx @vue/cli frontend in the project’s root folder. Be sure to choose ‘Manually select features’ then choose to include Babel and Vue Router.

Next we have to install some packages. We need Axios for making HTTP requests, Bootstrap for styling, Vee-Validate for form validation, and Vue-Filter-Date-Format package for formatting dates.

We install all the packages by running:

npm i axios bootstrap-vue vee-validate vue-filter-date-format

Once that’s done, we can start writing the front end app. First we add the top bars for our front end. We make one to display when the user is logged in and another one for when the user is logged out.

Create LoggedInTopBar.vue in the components folder and add:

<template>
  <b-navbar toggleable="lg" type="dark" variant="info">
    <b-navbar-brand to="/">Bitbucket App</b-navbar-brand>

    <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

     <b-collapse id="nav-collapse" is-nav>
      <b-navbar-nav>
        <b-nav-item to="/settings" :active="path  == '/settings'">Settings</b-nav-item>
        <b-nav-item to="/repos" :active="path.includes('/repos')">Repos</b-nav-item>
        <b-nav-item @click="logOut()">Log Out</b-nav-item>
      </b-navbar-nav>
    </b-collapse>
  </b-navbar>
</template>

<script>
export default {
  name: "LoggedInTopBar",
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  },
  methods: {
    logOut() {
      localStorage.clear();
      this.$router.push("/");
    }
  }
};
</script>

We watch the URL that the user is currently navigated to to set the active prop, which highlights the link when the user goes to the URL with the condition in the code.

b-navbar is provided by BootstrapVue.

Similarly, we create LoggedOutTopBar.vue in the same folder and add:

<template>
  <b-navbar toggleable="lg" type="dark" variant="info">
    <b-navbar-brand to="/">Bitbucket App</b-navbar-brand>

<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

<b-collapse id="nav-collapse" is-nav>
      <b-navbar-nav>
        <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
      </b-navbar-nav>
    </b-collapse>
  </b-navbar>
</template>

<script>
export default {
  name: "LoggedOutTopBar",
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

Next we create a mixins folder in the src folder and add a requestsMixin.js file to make shared code that lets our components make HTTP requests. In this file, we add:

const APIURL = "http://localhost:3000";
const axios = require("axios");

axios.interceptors.request.use(
  config => {
    config.headers.authorization = localStorage.getItem("token");
    return config;
  },
  error => Promise.reject(error)
);

axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response.status == 401) {
      localStorage.clear();
    }
    return error;
  }
);

export const requestsMixin = {
  methods: {
    signUp(data) {
      return axios.post(`${APIURL}/users/signup`, data);
    },

    logIn(data) {
      return axios.post(`${APIURL}/users/login`, data);
    },

    changePassword(data) {
      return axios.post(`${APIURL}/users/changePassword`, data);
    },

    currentUser() {
      return axios.get(`${APIURL}/users/currentUser`);
    },

    setBitbucketCredentials(data) {
      return axios.post(`${APIURL}/bitbucket/setBitbucketCredentials`, data);
    },

    repos(page) {
      return axios.get(`${APIURL}/bitbucket/repos/${page || 1}`);
    },

    commits(repoName) {
      return axios.get(`${APIURL}/bitbucket/commits/${repoName}`);
    }
  }
};

We have a HTTP request interceptor to add the authentication token to the header of our requests and in the response interceptor, we intercept the response and clear local storage if 401 response is received.

Next we make our pages. First we make a page to list the commits of a repository given the repository name in the URL. Create a file in the views folder called CommitsPage.vue and add:

<template>
  <div>
    <LoggedInTopBar />
    <div class="page">
      <h1 class="text-center">Commits - {{repoName}}</h1>
      <b-card :title="b.message" v-for="b in bitBucketCommits" :key="b.hash">
        <b-card-text>
          <p>Hash: {{b.hash}}</p>
          <p>Date: {{ new Date(b.date) | dateFormat('YYYY-MM-DD hh:mm:ss a') }}</p>
        </b-card-text>
      </b-card>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import LoggedInTopBar from "@/components/LoggedInTopBar.vue";
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    LoggedInTopBar
  },
  data() {
    return {
      bitBucketCommits: [],
      repoName: ""
    };
  },
  methods: {},
  async beforeMount() {
    this.repoName = this.$route.params.repoName;
    const response = await this.commits(this.repoName);
    this.bitBucketCommits = response.data.values;
  }
};
</script>

We get the repository name from the URL, then get the commits of the repository with the given name and display them in BootstrapVue cards.

Next we replace the content of Home.vue with:

<template>
  <div>
    <LoggedOutTopBar />
    <div class="page">
      <h1 class="text-center">Log In</h1>
      <ValidationObserver ref="observer" v-slot="{ invalid }">
        <b-form @submit.prevent="onSubmit">
          <b-form-group label="Username" label-for="username">
            <ValidationProvider name="username" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="form.username"
                type="text"
                required
                placeholder="Username"
                name="username"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Username is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-form-group label="Password" label-for="password">
            <ValidationProvider name="password" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="form.password"
                type="password"
                required
                placeholder="Password"
                name="password"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Password is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-button type="submit" variant="primary" style="margin-right: 10px">Log In</b-button>
          <b-button type="button" variant="primary" @click="$router.push('/signup')">Sign Up</b-button>
        </b-form>
      </ValidationObserver>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import LoggedOutTopBar from "@/components/LoggedOutTopBar.vue";
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    LoggedOutTopBar
  },
  data() {
    return {
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      try {
        const response = await this.logIn(this.form);
        localStorage.setItem("token", response.data.token);
        this.$router.push("/settings");
      } catch (error) {
        alert("Invalid username or password");
      }
    }
  }
};
</script>

This page is our sign up page. We use Vee-Validate to validate our forms. We wrapped the whole form with ValidationObserver to get the validation status with await this.$refs.observer.validate() in the onSubmit function. We put each input in a ValidationProvider so that each field is validated against the defined rules.

Once this.logIn promise resolves successfully, we get an authentication token, which we set in local storage for access by our requests.

Next we create RepoPage.vue in the views folder and add:

<template>
  <div>
    <LoggedInTopBar />
    <div class="page">
      <h1 class="text-center">Repos</h1>
      <b-card :title="b.name" v-for="b in bitBucketRepos" :key="b.slug">
        <b-card-text>
          <p>Updated on: {{ new Date(b.updated_on) | dateFormat('YYYY-MM-DD hh:mm:ss a') }}</p>
        </b-card-text>

        <b-button :to="`/commits/${b.slug}`" variant="primary">See Commits</b-button>
      </b-card>
      <br />
      <b-pagination-nav :link-gen="linkGen" :number-of-pages="numPages" use-router v-model="page"></b-pagination-nav>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import LoggedInTopBar from "@/components/LoggedInTopBar.vue";
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    LoggedInTopBar
  },
  data() {
    return {
      bitBucketRepos: [],
      page: 1,
      numPages: 1
    };
  },
  methods: {
    linkGen(pageNum) {
      return pageNum === 1 ? "?" : `?page=${pageNum}`;
    },
    async getRepos() {
      const response = await this.repos(this.page);
      this.bitBucketRepos = response.data.values;
      this.numPages = response.data.size / response.data.pagelen;
    }
  },
  beforeMount() {
    this.getRepos();
  },
  watch: {
    async page(val) {
      await this.getRepos(val);
    }
  }
};
</script>

to get the repositories of the given user. Note that we have pagination, since pagination is provided by back end. We use the BootstrapVue paginator, which takes a link-gen prop for passing in a function to generate the link content, and number-of-pages prop which is the number of pages. We need v-model so that this.page is updated, and we can use the watch block that we defined to get the repositories of the set page.

this.getRepos is from requestsMixin .

Next create a SettingsPage.vue file in the views folder and add:

<template>
  <div>
    <LoggedInTopBar />
    <div class="page">
      <h1 class="text-center">Settings</h1>

      <h2>User Settings</h2>
      <ValidationObserver ref="userObserver" v-slot="{ invalid }">
        <b-form @submit.prevent="onUserSettingSubmit">
          <b-form-group label="Username" label-for="username">
            <ValidationProvider name="username" rules="required" v-slot="{ errors }">
              <b-form-input
                disabled
                :state="errors.length == 0"
                v-model="form.username"
                type="text"
                required
                placeholder="Username"
                name="username"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Username is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-form-group label="Password" label-for="password">
            <ValidationProvider name="password" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="form.password"
                type="password"
                required
                placeholder="Password"
                name="password"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Password is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-button type="submit" variant="primary">Save</b-button>
        </b-form>
      </ValidationObserver>

      <br />

      <h2>Bitbucket Settings</h2>
      <ValidationObserver ref="bitbucketObserver" v-slot="{ invalid }">
        <b-form @submit.prevent="onBitbucketSettingSubmit">
          <b-form-group label="Bitbucket Username" label-for="bitBucketUsername">
            <ValidationProvider name="username" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="bitBucketForm.bitBucketUsername"
                type="text"
                required
                placeholder="Username"
                name="bitBucketUsername"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Username is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-form-group label="Bitbucket Password" label-for="bitBucketPassword">
            <ValidationProvider name="password" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="bitBucketForm.bitBucketPassword"
                type="password"
                required
                placeholder="Password"
                name="bitBucketPassword"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Password is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-button type="submit" variant="primary">Save</b-button>
        </b-form>
      </ValidationObserver>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import LoggedInTopBar from "@/components/LoggedInTopBar.vue";
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    LoggedInTopBar
  },
  data() {
    return {
      form: {},
      bitBucketForm: {}
    };
  },
  methods: {
    async onUserSettingSubmit() {
      const isValid = await this.$refs.userObserver.validate();
      if (!isValid) {
        return;
      }
      await this.changePassword(this.form);
      alert("Password changed");
    },

    async onBitbucketSettingSubmit() {
      const isValid = await this.$refs.bitbucketObserver.validate();
      if (!isValid) {
        return;
      }
      await this.setBitbucketCredentials(this.bitBucketForm);
      alert("Bitbucket credentials changed");
    }
  },
  async beforeMount() {
    const response = await this.currentUser();
    const { username, bitBucketUsername } = response.data;
    this.form = { username };
    this.bitBucketForm = { bitBucketUsername };
  }
};
</script>

We have our forms for setting our account password and setting the Bitbucket credentials here. The forms work the same way as the log in form in Home.vue . The this.changePassword and this.setBitbucketCredentials functions are from our requestsMixin . The functions make the HTTP requests.

Next we create SignUpPage.vue in the views folder and add:

<template>
  <div>
    <LoggedOutTopBar />
    <div class="page">
      <h1 class="text-center">Sign Up</h1>
      <ValidationObserver ref="observer" v-slot="{ invalid }">
        <b-form @submit.prevent="onSubmit">
          <b-form-group label="Username" label-for="username">
            <ValidationProvider name="username" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="form.username"
                type="text"
                required
                placeholder="Username"
                name="username"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Username is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-form-group label="Password" label-for="password">
            <ValidationProvider name="password" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="form.password"
                type="password"
                required
                placeholder="Password"
                name="password"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Password is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-button type="submit" variant="primary">Sign Up</b-button>
        </b-form>
      </ValidationObserver>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import LoggedOutTopBar from "@/components/LoggedOutTopBar.vue";
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    LoggedOutTopBar
  },
  data() {
    return {
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      try {
        await this.signUp(this.form);
        alert("Sign up successful");
      } catch (error) {
        alert("Username is already taken");
      }
    }
  }
};
</script>

We create the sign up form here and submit the data with the HTTP request by calling this.signUp from the requestsMixin to submit the data.

Next in App.vue , replace the existing code with:

<template>
  <router-view />
</template>

<style lang="scss">
.page {
  padding: 20px;
}
</style>

We have router-view so we can view our components routed by Vue Router, and we add some padding to the pages.

Next in main.js , replace the existing code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import VueFilterDateFormat from "vue-filter-date-format";

extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.use(VueFilterDateFormat);

Vue.config.productionTip = false;

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem("token");
  if (to.fullPath != "/" && to.fullPath != "/signup") {
    if (!token) {
      router.push("/");
    }
  }

next();
});

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

This is where we include the libraries we used in this app, and also intercept the Vue Router navigation to check if the authentication token is present in the authenticated routes. We call next if the token is present if a user tries to go to authenticated routes.

We also import the Bootstrap CSS in this file so we see Bootstrap styling.

Finally, in index.html , replace the existing code with:

<!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" />
    <title>Bitbucket App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but frontend 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 change the title of our app.

After all the hard work is done, we run the back end app by going into the backend folder and run npm start . And then go into the frontend folder and run npm run serve .

Categories
JavaScript Basics

Handling Exceptions in JavaScript

Like any programs, JavaScript will encounter error situations, for example, like when JSON fails to parse, or null value is encounter unexpectedly in a variable. This means that we have to handle those errors gracefully if we want our app to give users a good user experience. This means that we have to handle those errors gracefully. Errors often come in the form of exceptions, so we have to handle those gracefully. To handle them, we have to use the try...catch statement to handle these errors so they do not crash the program.

Try…Catch

To use the try...catch block, we have to use the following syntax:

try{
  // code that we run that may raise exceptions
  // one or more lines is required in this block
}
catch (error){
  // handle error here
  // optional if finally block is present
}
finally {
  // optional code that run either
  // when try or catch block is finished
}

For example, we can write the following code to catch exceptions:

try {
  undefined.prop
} catch (error) {
  console.log(error);
}

In the code above, we were trying to get a property from undefined , which is obviously not allowed, so an exception is thrown. In the catch block, we catch the ‘TypeError: Cannot read property ‘prop’ of undefined’ that’s caused by running undefined.prop and log the output of the exception. So we get the error message outputted instead of crashing the program.

The try...catch statement has a try block. The try block must have at least one statement inside and curly braces must always be used, event for single statements. Then either the catch clause or finally clause can be included. This means that we can have:

try {
  ...
}
catch {
  ...
}

try {
  ...
}
finally{
  ...
}

try {
  ...
}
catch {
  ...
}
finally {
  ...
}

The catch clause has the code that specifies what to do when an exception is thrown in the try block. If they try block didn’t succeed and an exception is thrown, then the code in the catch block will be ran. If all the code in the try block is ran without any exception thrown, then the code in the catch block is skipped.

The finally block executes after all the code the try block or the catch block finishes running. It always runs regardless if exceptions are thrown or not.

try blocks can be nested within each other. If the inner try block didn’t catch the exception and the outer one has a catch block, then the outer one will catch the exception thrown in the inner try block. For example, if we have:

try {
  try {
    undefined.prop
  } finally {
    console.log('Inner finally block runs');
  }
} catch (error) {
  console.log('Outer catch block caught:', error);
}

If we run the code above, we should see ‘Inner finally block runs’ and ‘Outer catch block caught: TypeError: Cannot read property ‘prop’ of undefined’ logged, which is what we expect since the inner try block didn’t catch the exception with a catch block so the outer catch block did. As we can see the inner finally block ran before the outer catch block. try...catch...finally runs sequentially, so the code that’s added earlier will run before the ones that are added later.

The catch block that we wrote so far are all unconditional. That means that they catch any exceptions that were thrown. The error object holds the data about the exception thrown. It only holds the data inside the catch block. If we want to keep the data outside it then we have to assign it to a variable outside the catch block. After the catch block finishes running, the error object is no longer available.

The finally clause contains statements that are excepted after the code in the try block or the catch block executes, but before the statements executed below the try...catch...finally block. It’s executed regardless whether an exception was thrown. If an exception is thrown, then statements in the finally block is executed even if no catch block catches and handles the exception.

Therefore, the finally block is handy for making our program fail gracefully when an error occurs. For example, we can put cleanup code that runs no matter is an exception is thrown or not, like for close file reading handles. The remaining code in a try block doesn’t executed when an exception is thrown when running a line in the try block, so if we were excepted to close file handles in the try and an exception is thrown before the line that closes the file handle is ran, then to end the program gracefully, we should do that in the finally block instead to make sure that file handles always get cleaned up. We can just put code that runs regardless of whether an exception is thrown like cleanup code in the finally block so that we don’t have to duplicate them in the try and catch blocks. For example, we can write:

openFile();
try {
  // tie up a resource
  writeFile(data);
}
finally {
  closeFile();
  // always close the resource
}

In the code above, the closeFile function always run regardless of whether an exception is thrown when the writeFile is run, eliminating duplicate code.

We can have nested try blocks, like in the following code:

try {
  try {
    throw new Error('error');
  }
  finally {
    console.log('finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

If we look at the console log, we should see that ‘finally runs’ comes before ‘exception caught error.’ This is because everything in the try...catch block is ran line by line even if it’s nested. If we have more nesting like in the following code:

try {
  try {
    throw new Error('error');
  }
  finally {
    console.log('first finally runs');
  }

 try {
    throw new Error('error2');
  }
  finally {
    console.log('second finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

We see that we get the same console log output as before. This is because the first inner try block didn’t catch the exception, so the exception is propagated to and caught by the outer catch block. If we want to second try block to run, then we have to add a catch block to the first try block, like in the following example:

try {
  try {
    throw new Error('error');
  }
  catch {
    console.log('first catch block runs');
  }
  finally {
    console.log('first finally runs');
  }

 try {
    throw new Error('error2');
  }
  finally {
    console.log('second finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

Now we see the following message logged in order: ‘first catch block runs’, ‘first finally runs’, ‘second finally runs’, ‘exception caught error2’. This is because the first try block has a catch block, so the the exception caused by the throw new Error('error') line is now caught in the catch block of the first inner try block. Now the second inner try block don’t have an associated catch block, so error2 will be caught by the outer catch block.

We can also rethrow errors that were caught in the catch block. For example, we can write the following code to do that:

try {
  try {
    throw new Error('error');
  } catch (error) {
    console.error('error', error.message);
    throw error;
  } finally {
    console.log('finally block is run');
  }
} catch (error) {
  console.error('outer catch block caught', error.message);
}

As we can see, if we ran the code above, then we get the following logged in order: ‘error error’, ‘finally block is run’, and ‘outer catch block caught error’. This is because the inner catch block logged the exception thrown by throw new Error(‘error’) , but then after console.error(‘error’, error.message); is ran, we ran throw error; to throw the exception again. Then the inner finally block is run and then the rethrown exception is caught by the outer catch block which logged the error that was rethrown by the throw error statement in the inner catch block.

Since the code runs sequentially, we can run return statements at the end of a try block. For example, if we want to parse a JSON string into an object we we want to return an empty object if there’s an error parsing the string passed in, for example, when the string passed in isn’t a valid JSON string, then we can write the following code:

const parseJSON = (str) => {
  try {
    return JSON.parse(str);
  }
  catch {
    return {};
  }
}

In the code above, we run JSON.parse to parse the string and if it’s not valid JSON, then an exception will be thrown. If an exception is thrown, then the catch clause will be invokes to return an empty object. If JSON.parse successfully runs then the parsed JSON object will be returned. So if we run:

console.log(parseJSON(undefined));
console.log(parseJSON('{"a": 1}'))

Then we get an empty object on the first line and we get {a: 1} on the second line.

Try Block in Asynchronous Code

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. Using the async and await syntax for chaining promises, we can also use try and catch blocks to catch rejected promises and handle rejected promises gracefully. For example , if we want to catch promise rejections with a catch block, we can do the following:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('error')
    })
  } catch (error) {
    console.log(error);
  }

})();

In the code above, since we rejected the promise that we defined in the try block, the catch block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. Even though it looks a regular try...catch block, it’s not, since this is an async function. An async function only returns promises, so we can’t return anything other than promises in the try...catch block. The catch block in an async function is just a shorthand for the catch function which is chained to the then function. So the code above is actually the same as:

(() => {
  new Promise((resolve, reject) => {
      reject('error')
    })
    .catch(error => console.log(error))
})()

We see that we get the same console log output as the async function above when it’s run.

The finally block also works with the try...catch block in an async function. For example, we can write:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('error')
    })
  } catch (error) {
    console.log(error);
  } finally {
    console.log('finally is run');
  }
})();

In the code above, since we rejected the promise that we defined in the try block, the catch block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. The the finally block runs so that we get ‘finally is run’ logged. The finally block in an async function is the same as chaining the finally function to the end of a promise so the code above is equivalent to:

(() => {
  new Promise((resolve, reject) => {
      reject('error')
    })
    .catch(error => console.log(error))
    .finally(() => console.log('finally is run'))
})()

We see that we get the same console log output as the async function above when it’s run.

The rules for nested try...catch we mentioned above still applies to async function, so we can write something like:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('outer error')
    })
    try {
      await new Promise((resolve, reject) => {
        reject('inner error')
      })
    } catch (error) {
      console.log(error);
    } finally {

    }
  } catch (error) {
    console.log(error);
  } finally {
    console.log('finally is run');
  }
})();

This lets us easily nest promises and and handle their errors accordingly. This is cleaner than chaining the then , catch and finally functions that we did before we have async functions.

To handle errors in JavaScript programs, we can use the try...catch...finally blocks to catch errors. This can be done with synchronous or asynchronous code. We put the code that may throw exceptions in the try block, then put the code that handles the exceptions in the catch block. In the finally block we run any code that runs regardless of whether an exception is thrown. async functions can also use the try...catch block, but they only return promises like any other async function, but try...catch...finally blocks in normal functions can return anything.

Categories
Vue

How to Create Responsive Layout with Vue Slots

Slots is a useful feature of Vue.js that allows you to separate different parts of a component into an organized unit. With your component compartmentalized into slots, you can reuse components by putting them into the slots you defined. It also makes your code cleaner since it lets you separate the layout from the logic of the app.

Also, if you use slots, you no longer have to compose components with parent child relationship since you can put any components into your slots.

A simple example of Vue slots would be the following. You define your slot in Layout.vue file:

`<template>
  <div class="frame">
    <slot` name="`frame`"`></slot>
  </div>
</template>`

Then in another file, you can add:

<`Layout>` <template v-slot:frame>
`<img src="an-image.jpg">
   </template>
</Layout>`

To use the slot in your Layout component.

We will clarify the above example by building an example app. To illustrate the use of slots in Vue.js, we will build a responsive app that displays article snippets from the New York Times API and a search page where users can enter a keyword to search the API.

The desktop layout will have a list of item names on the left and the article snippets on the right. The mobile layout will have a drop down for selecting the section to display and the cards displaying the article snippets below it.

The search page will have a search form on top and the article snippets below it regardless of screen size.

To start building the app, we start by running the Vue CLI. We run:

npx @vue/cli create nyt-app

to create the Vue.js project. When the wizard shows up, we choose ‘Manually select features’. Then we choose to include Vue Router and Babel in our project.

Next we add our own libraries for styling and making HTTP requests. We use BootstrapVue for styling, Axios for making requests, VueFilterDateFormat for formatting dates and Vee-Validate for form validation.

To install all the libraries, we run:

npm i axios bootstrap-vue vee-validate vue-filter-date-format

After all the libraries are installed, we can start building our app.

First we use slots yo build our layouts for our pages. Create BaseLayout.vue in the components folder and add:

<template>
  <div>
    <div class="row">
      <div class="col-md-3 d-none d-lg-block d-xl-none d-xl-block">
        <slot name="left"></slot>
      </div>
      <div class="col">
        <div class="d-block d-sm-none d-none d-sm-block d-md-block d-lg-none">
          <slot name="section-dropdown"></slot>
        </div>
        <slot name="right"></slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "BaseLayout"
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

In this file, we make use of Vue slots to create the responsive layout for the home page. We have the left , right , and section-dropdown slots in this file. The left slot only displays when the screen is large since we added the d-none d-lg-block d-xl-none d-xl-block classes to the left slot. The section-dropdown slot only shows on small screens since we added the d-block d-sm-none d-none d-sm-block d-md-block d-lg-none classes to it. These classes are the responsive utility classes from Bootstrap.

The full list of responsive utility classes are at https://getbootstrap.com/docs/4.0/utilities/display/

Next, create a SearchLayout.vue file in the components folder and add:

<template>
  <div class="row">
    <div class="col-12">
      <slot name="top"></slot>
    </div>
    <div class="col-12">
      <slot name="bottom"></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "SearchLayout"
};
</script>

to create another layout for our search page. We have the top and bottom slots taking up the whole width of the screen.

Then we create a mixins folder and in it, create a requestsMixin.js file and add:

const axios = require("axios");
const APIURL = "https://api.nytimes.com/svc";

export const requestsMixin = {
  methods: {
    getArticles(section) {
      return axios.get(
        `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.VUE_APP_API_KEY}`
      );
    },

    searchArticles(keyword) {
      return axios.get(
        `${APIURL}/search/v2/articlesearch.json?api-key=${process.env.VUE_APP_API_KEY}&q=${keyword}`
      );
    }
  }
};

to create a mixin for making HTTP requests to the New York Times API. process.env.VUE_APP_API_KEY is the API key for the New York Times API, and we get it from the .env file in the project’s root folder, with the key of the environment variable being VUE_APP_API_KEY .

Next in Home.vue , replace the existing code with:

<template>
  <div class="page">
    <h1 class="text-center">Home</h1>
    <BaseLayout>
      <template v-slot:left>
        <b-nav vertical pills>
          <b-nav-item
            v-for="s in sections"
            :key="s"
            :active="s == selectedSection"
            @click="selectedSection = s; getAllArticles()"
          >{{s}}</b-nav-item>
        </b-nav>
      </template>

      <template v-slot:section-dropdown>
        <b-form-select
          v-model="selectedSection"
          :options="sections"
          @change="getAllArticles()"
          id="section-dropdown"
        ></b-form-select>
      </template>

      <template v-slot:right>
        <b-card
          v-for="(a, index) in articles"
          :key="index"
          :title="a.title"
          :img-src="(Array.isArray(a.multimedia) && a.multimedia.length > 0 && a.multimedia[a.multimedia.length-1].url) || ''"
          img-bottom
        >
          <b-card-text>
            <p>{{a.byline}}</p>
            <p>Published on: {{new Date(a.published_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
            <p>{{a.abstract}}</p>
          </b-card-text>

          <b-button :href="a.short_url" variant="primary" target="_blank">Go</b-button>
        </b-card>
      </template>
    </BaseLayout>
  </div>
</template>

<script>
// @ is an alias to /src
import BaseLayout from "@/components/BaseLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "home",
  components: {
    BaseLayout
  },
  mixins: [requestsMixin],
  data() {
    return {
      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`
        .split(",")
        .map(s => s.trim()),
      selectedSection: "arts",
      articles: []
    };
  },
  beforeMount() {
    this.getAllArticles();
  },
  methods: {
    async getAllArticles() {
      const response = await this.getArticles(this.selectedSection);
      this.articles = response.data.results;
    },
    setSection(ev) {
      this.getAllArticles();
    }
  }
};
</script>

<style scoped>
#section-dropdown {
  margin-bottom: 10px;
}
</style>

We use the slots defined in BaseLayout.vue in this file. In the left slot, we put the list of section names in there to display the list on the left when we have a desktop sized screen.

In the section-dropdown slot, we put the drop down that only shows in mobile screens as defined in BaseLayout .

Then in the right slot, we put the Bootstrap cards for displaying the article snippets, also as defined in BaseLayout .

We put all the slot contents inside BaseLayout and we use v-slot outside the items we want to put into the slots to make the items show in the designated slot.

In the script section, we get the articles by section by defining the getAllArticles function from requestsMixin .

Next create a Search.vue file and add:

<template>
  <div class="page">
    <h1 class="text-center">Search</h1>
    <SearchLayout>
      <template v-slot:top>
        <ValidationObserver ref="observer" v-slot="{ invalid }">
          <b-form @submit.prevent="onSubmit" novalidate id="form">
            <b-form-group label="Keyword" label-for="keyword">
              <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
                <b-form-input
                  :state="errors.length == 0"
                  v-model="form.keyword"
                  type="text"
                  required
                  placeholder="Keyword"
                  name="keyword"
                ></b-form-input>
                <b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback>
              </ValidationProvider>
            </b-form-group>

            <b-button type="submit" variant="primary">Search</b-button>
          </b-form>
        </ValidationObserver>
      </template>

      <template v-slot:bottom>
        <b-card v-for="(a, index) in articles" :key="index" :title="a.headline.main">
          <b-card-text>
            <p>By: {{a.byline.original}}</p>
            <p>Published on: {{new Date(a.pub_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
            <p>{{a.abstract}}</p>
          </b-card-text>

          <b-button :href="a.web_url" variant="primary" target="_blank">Go</b-button>
        </b-card>
      </template>
    </SearchLayout>
  </div>
</template>

<script>
// @ is an alias to /src
import SearchLayout from "@/components/SearchLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "home",
  components: {
    SearchLayout
  },
  mixins: [requestsMixin],
  data() {
    return {
      articles: [],
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const response = await this.searchArticles(this.form.keyword);
      this.articles = response.data.response.docs;
    }
  }
};
</script>

<style scoped>
</style>

It’s very similar to Home.vue . We put the search form in the top slot by putting it inside the SearchLayour , and we put our slot content for the top slot by putting our form inside the <template v-slot:top> element.

We use the ValidationObserver to validate the whole form, and ValidationProvider to validate the keyword input. They are both provided by Vee-Validate.

Once the Search button is clicked, we call this.$refs.observer.validate(); to validate the form. We get the this.$refs.observer since we wrapped the ValidationObserver outside the form.

Then once form validation succeeds, by this.$refs.observer.validate() resolving to true , we call searchArticles from requestsMixin to search for articles.

In the bottom slot, we put the cards for displaying the article search results. It works the same way as the other slots.

Next in App.vue , we put:

<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">New York Times App</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path == '/'">Home</b-nav-item>
          <b-nav-item to="/search" :active="path == '/search'">Search</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

<style>
.page {
  padding: 20px;
}
</style>

to we add the BootstrapVue b-navbar here and watch the route as it changes so that we can set the active prop to the link of the page the user is currently in.

Next we change main.js ‘s code to:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import VueFilterDateFormat from "vue-filter-date-format";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";

Vue.use(VueFilterDateFormat);
Vue.use(BootstrapVue);
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

We import all the app-wide packages we use here, like BootstrapVue, Vee-Validate and the calendar and date-time picker widgets.

The styles are also imported here so we can see them throughout the app.

Next in router.js , replace the existing code with:

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
    }
  ]
});

to set the routes for our app, so that when users enter the given URL or click on a link with it, they can see our page.

Finally, we replace the code in index.html with:

<!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" />
    <title>New York Times App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-slots-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 change the app’s title.

Finally we run our app by running npm run serve in our app’s project folder to run our app.