Categories
JavaScript Vue

Create Web Components with Vue.js

Component-based architecture is the main architecture for front end development today. The World Wide Web Consortium (W3C) has caught up to the present by creating the web components API. It lets developers build custom elements that can be embedded in web pages. The elements can be reused and nested anywhere, allowing for code reuse in any pages or apps.

The custom elements are nested in the shadow DOM, which is rendered separately from the main DOM of a document. This means that they are completely isolated from other parts of the page or app, eliminating the chance of conflict with other parts,

There are also template and slot elements that aren’t rendered on the page, allowing you to reused the things inside in any place.

To create web components without using any framework, you have to register your element by calling CustomElementRegistry.define() and pass in the name of the element you want to define. Then you have to attach the shadow DOM of your custom element by calling Element.attachShawdow() so that your element will be displayed on your page.

This doesn’t include writing the code that you want for your custom elements, which will involve manipulating the shadow DOM of your element. It is going to be frustrating and error-prone if you want to build a complex element.

Vue.js abstracts away the tough parts by letting you build your code into a web component. You write code by importing and including the components in your Vue components instead of globally, and then you can run commands to build your code into one or more web components and test it.

We build the code into a web component with Vue CLI by running:

npm run build -- --target wc --inline-vue --name custom-element-name

The --inline-vue flag includes a copy of view in the built code, --target wc builds the code into a web component, and --name is the name of your element.

In this article, we will build a weather widget web component that displays the weather from the OpenWeatherMap API. We will add a search to let users look up the current weather and forecast from the API.

We will use Vue.js to build the web component. To begin building it, we start with creating the project with Vue CLI. Run npx @vue/cli create weather-widget to create the project. In the wizard, select Babel, SCSS and Vuex.

The OpenWeatherMap API is available at https://openweathermap.org/api. You can register for an API key here. Once you got an API key, create an .env file in the root folder and add VUE_APP_APIKEY as the key and the API key as the value.

Next, we install some packages that we need for building the web component. We need Axios for making HTTP requests, BootstrapVue for styling, and Vee-Validate for form validation. To install them, we run npm i axios bootstrap-vue vee-validate to install them.

With all the packages installed we can start writing our code. Create CurrentWeather.vue in the components folder and add:

<template>  
  <div>  
    <br />  
    <b-list-group v-if="weather.main">  
      <b-list-group-item>Current Temparature: {{weather.main.temp - 273.15}} C</b-list-group-item>  
      <b-list-group-item>High: {{weather.main.temp_max - 273.15}} C</b-list-group-item>  
      <b-list-group-item>Low: {{weather.main.temp_min - 273.15}} C</b-list-group-item>  
      <b-list-group-item>Pressure: {{weather.main.pressure }}mb</b-list-group-item>  
      <b-list-group-item>Humidity: {{weather.main.humidity }}%</b-list-group-item>  
    </b-list-group>  
  </div>  
</template>

<script>  
import { requestsMixin } from "@/mixins/requestsMixin";  
import store from "../store";  
import { BListGroup, BListGroupItem } from "bootstrap-vue";  
import 'bootstrap/dist/css/bootstrap.css'  
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {  
  store,  
  name: "CurrentWeather",  
  mounted() {},  
  mixins: [requestsMixin],  
  components: {  
    BListGroup,  
    BListGroupItem  
  },  
  computed: {  
    keyword() {  
      return this.$store.state.keyword;  
    }  
  },  
  data() {  
    return {  
      weather: {}  
    };  
  },  
  watch: {  
    async keyword(val) {  
      const response = await this.searchWeather(val);  
      this.weather = response.data;  
    }  
  }  
};  
</script>

<style scoped>  
p {  
  font-size: 20px;  
}  
</style>

This component displays the current weather from the OpenWeatherMap API is the keyword from the Vuex store is updated. We will create the Vuex store later. The this.searchWeather function is from the requestsMixin, which is a Vue mixin that we will create. The computed block gets the keyword from the store via this.$store.state.keyword and return the latest value.

Note that we’re importing all the BootstrapVue components individually here. This is because we aren’t building an app. main.js in our project will not be run, so we cannot register components globally by calling Vue.use. Also, we have to import the store here, so that we have access to the Vuex store in the component.

Next, create Forecast.vue in the same folder and add:

<template>  
  <div>  
    <br />  
    <b-list-group v-for="(l, i) of forecast.list" :key="i">  
      <b-list-group-item>  
        <b>Date: {{l.dt_txt}}</b>  
      </b-list-group-item>  
      <b-list-group-item>Temperature: {{l.main.temp - 273.15}} C</b-list-group-item>  
      <b-list-group-item>High: {{l.main.temp_max - 273.15}} C</b-list-group-item>  
      <b-list-group-item>Low: {{l.main.temp_min }}mb</b-list-group-item>  
      <b-list-group-item>Pressure: {{l.main.pressure }}mb</b-list-group-item>  
    </b-list-group>  
  </div>  
</template>

<script>  
import { requestsMixin } from "@/mixins/requestsMixin";  
import store from "../store";  
import { BListGroup, BListGroupItem } from "bootstrap-vue";  
import 'bootstrap/dist/css/bootstrap.css'  
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {  
  store,  
  name: "Forecast",  
  mixins: [requestsMixin],  
  components: {  
    BListGroup,  
    BListGroupItem  
  },  
  computed: {  
    keyword() {  
      return this.$store.state.keyword;  
    }  
  },  
  data() {  
    return {  
      forecast: []  
    };  
  },  
  watch: {  
    async keyword(val) {  
      const response = await this.searchForecast(val);  
      this.forecast = response.data;  
    }  
  }  
};  
</script>

<style scoped>  
p {  
  font-size: 20px;  
}  
</style>

It’s very similar to CurrentWeather.vue. The only difference is that we are getting the current weather instead of the weather forecast.

Next, we create a mixins folder in the src folder and add:

const APIURL = "http://api.openweathermap.org";  
const axios = require("axios");
export const requestsMixin = {  
  methods: {  
    searchWeather(loc) {  
      return axios.get(  
        `${APIURL}/data/2.5/weather?q=${loc}&appid=${process.env.VUE_APP_APIKEY}`  
      );  
    },

    searchForecast(loc) {  
      return axios.get(  
        `${APIURL}/data/2.5/forecast?q=${loc}&appid=${process.env.VUE_APP_APIKEY}`  
      );  
    }  
  }  
};

These functions are for getting the current weather and the forecast respectively from the OpenWeatherMap API. process.env.VUE_APP_APIKEY is obtained from our .env file that we created earlier.

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

<template>  
  <div>  
    <b-navbar toggleable="lg" type="dark" variant="info">  
      <b-navbar-brand href="#">Weather App</b-navbar-brand>  
    </b-navbar>  
    <div class="page">  
      <ValidationObserver ref="observer" v-slot="{ invalid }">  
        <b-form @submit.prevent="onSubmit" novalidate>  
          <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><br />
      <b-tabs>  
        <b-tab title="Current Weather">  
          <CurrentWeather />  
        </b-tab>  
        <b-tab title="Forecast">  
          <Forecast />  
        </b-tab>  
      </b-tabs>  
    </div>  
  </div>  
</template>

<script>  
import CurrentWeather from "@/components/CurrentWeather.vue";  
import Forecast from "@/components/Forecast.vue";  
import store from "./store";  
import {  
  BTabs,  
  BTab,  
  BButton,  
  BForm,  
  BFormGroup,  
  BFormInvalidFeedback,  
  BNavbar,  
  BNavbarBrand,  
  BFormInput  
} from "bootstrap-vue";  
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";  
import { required } from "vee-validate/dist/rules";  
extend("required", required);

export default {  
  store,  
  name: "App",  
  components: {  
    CurrentWeather,  
    Forecast,  
    ValidationProvider,  
    ValidationObserver,  
    BTabs,  
    BTab,  
    BButton,  
    BForm,  
    BFormGroup,  
    BFormInvalidFeedback,  
    BNavbar,  
    BNavbarBrand,  
    BFormInput  
  },  
  data() {  
    return {  
      form: {}  
    };  
  },  
  methods: {  
    async onSubmit() {  
      const isValid = await this.$refs.observer.validate();  
      if (!isValid) {  
        return;  
      }  
      localStorage.setItem("keyword", this.form.keyword);  
      this.$store.commit("setKeyword", this.form.keyword);  
    }  
  },  
  beforeMount() {  
    this.form = { keyword: localStorage.getItem("keyword") || "" };  
  },  
  mounted() {  
    this.$store.commit("setKeyword", this.form.keyword);  
  }  
};  
</script>

<style lang="scss">  
@import "./../node_modules/bootstrap/dist/css/bootstrap.css";  
@import "./../node_modules/bootstrap-vue/dist/bootstrap-vue.css";  
.page {  
  padding: 20px;  
}  
</style>

We add the BootstrapVue b-navbar here to add a top bar to show the extension’s name. Below that, we added the form for searching the weather info. Form validation is done by wrapping the form in the ValidationObserver component and wrapping the input in the ValidationProvider component. We provide the rule for validation in the rules prop of ValidationProvider. The rules will be added in main.js later.

The error messages are displayed in the b-form-invalid-feedback component. We get the errors from the scoped slot in ValidationProvider. It’s where we get the errors object from.

When the user submits the number, the onSubmit function is called. This is where the ValidationObserver becomes useful as it provides us with the this.$refs.observer.validate() function to check for form validity.

If isValid resolves to true , then we set the keyword in local storage, and also in the Vuex store by running this.$store.commit(“setKeyword”, this.form.keyword); .

In the beforeMount hook, we set the keyword so that it will be populated when the extension first loads if a keyword was set in local storage. In the mounted hook, we set the keyword in the Vuex store so that the tabs will get the keyword to trigger the search for the weather data.

Like in the previous components, we import and register all the components and the Vuex store in this component, so that we can use the BootstrapVue components here. We also called Vee-Validate’s extend function so that we can use its required form validation rule for checking the input.

In style section of this file, we import the BootstrapVue styles, so that they can be accessed in this and the child components. We also add the page class so that we can add some padding to the page.

Then in store.js , we replace the existing code with:

import Vue from "vue";  
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({  
  state: {  
    keyword: ""  
  },  
  mutations: {  
    setKeyword(state, payload) {  
      state.keyword = payload;  
    }  
  },  
  actions: {}  
});

to add the Vuex store that we referenced in the components. We have the keyword state for storing the search keyword in the store, and the setKeyword mutation function so that we can set the keyword in our components.

Finally, in package.json , we add 2 scripts to the scripts section of the file:

"wc-build": "npm run build -- --target wc --inline-vue --name weather-widget","wc-test": "cd dist && live-server --port=8080 --entry-file=./demo.html"

The wc-build script builds our code into a web component as we described before, and the wc-test runs a local web server so that we can see what the web component looks like when it’s included in a web page. We use the live-server NPM package for serving the file. The --entry-file option specifies that we server demo.html as the home page, which we get when we run npm run wc-build .

Categories
JavaScript JavaScript Basics

Introducing the JavaScript Spread Operator

The spread syntax allows us to break up a collection of objects, like arrays, into individual arguments or insert them into a different iterable object, like an array.

With the 2018 version of JavaScript, we can also spread properties of an object into another object, with keys and values spread into another object. The spread syntax is denoted by three periods before your object.

For example, we can write:

...arr

The spread syntax works by copying the values of the original array and then inserting them into another array, or putting them in the order they appeared in the array as the list of arguments in a function in the same order.

When the spread operator is used with objects, the key-value pairs appear in the same order they appeared in the original object.

We can use the spread syntax to spread an array of values as arguments of a function. For example, we can write:

const arr = [1,2,3];  
const add = (a,b,c) => a+b+c;  
add(...arr) // returns 6

In the example above, the spread operator spreads the variables into the argument in the same order they appeared in the array. So 1 is passed into a, 2 is passed into b, and 3 is passed into c.


Spread Arrays

For arrays, we can also use the spread syntax to insert one array’s values into another array. For example, we can write:

const arr = [1,2,3];  
const arr2 = ['a','b','c',...arr,'d']; // arr2 is ['a','b','c',1,2,3,'d']

As we can see, the spread operator inserts the values exactly where we spread the array, in the same order they appeared in the array.

So, 1 is inserted between a and d, then 2 is inserted between 1 and d, and 3 is inserted between 2 and d. The result is that we copied an array’s values into another array with the spread operator in the same order they appeared in, and exactly where you put the array spread expression.

Without the spread operator, we have to write loops to insert them into the position we want. We slice the array into two and then call concat on the three parts, then assign the result to the array you inserted the stuff into. It sounds painful just thinking about it.

Note that with the spread operator, only the first level of the array is spread. If we have nested or multi-dimensional arrays, it’s going to copy the references as-is. It will not do anything to nested items.

With ES2018, we can do the same thing with objects, like the following:

const obj = {a: 1, b: 2};  
let objClone = { ...obj }; // objClone is {a: 1, b: 2}

This creates a shallow copy of the object. It means that only the first level of the object is copied.

For nested objects, it’s going to copy the references as-is. It will not do anything to nested items. The top-level keys and values of the object will be copied to objClone.

So, if we have nested objects, we get:

const obj = {  
  a: 1,  
  b: {  
    c: 2  
  }  
};  
let objClone = {  
  ...obj  
};  
console.log(objClone) 

In objClone, we get:

{  
  a: 1,  
  b: {  
    c: 2  
  }  
}

So, nested objects will reference the same ones as the original.

The spread operator can be used as an alternative to other functions that existed before.

For example, we can use it to replace the apply function for passing in arguments to a function. The apply function takes an array of arguments for the function it’s called on as the second argument.

With the apply function, we call it as follows:

const arr = [1,2,3]  
const sum = (a,b,c)=> a+b+c;  
sum.apply(null, arr); // 6

With the spread syntax, we can write the following instead:

const arr = [1,2,3]  
const sum = (a,b,c)=> a+b+c;  
sum(...arr)

The spread operator also work with strings. We apply the spread operator to strings, we get an array with the individual characters of the string.

For example, if we write:

const str = 'abcd';  
const chars = [...str];

We get [“a”, “b”, “c”, “d”] as the value of chars.


Using Spread Operator Multiple Times

We can use the spread syntax multiple times in one place. For example, we can have the following:

const arr = [1,2,3];  
const arr2 = [4,5];  
const sum = (a,b,c,d,e,f)=> a+b+c+d+e+f;  
sum(...arr, ...arr2, 6)

As usual, the spread syntax will spread the array of numbers into arguments of the array in the order as they appeared in.

So, sum(…arr, …arr2, 6) is the same as sum(1,2,3,4,5,6).

1, 2, and 3 are the first three arguments, which are the entries of arr in the same order, and 4 and 5 are the fourth and fifth arguments, which are spread after 1, 2, and 3.

Then, in the end, we have 6 as the last argument. We can also see the spread syntax work with the normal function call syntax.


Use It in Constructors

We can use the spread operator as arguments for object constructors. For example, if we want to create a new Date object, we can write:

let dateFields = [2001, 0, 1];  
let date = new Date(...dateFields);

The items in the dateFields array are passed into the constructors as arguments in the order they appeared in. The alternative way to write that would be much longer, something like:

let dateFields = [2001, 0, 1];  
const year = dateFields[0];  
const month = dateFields[1];  
const day = dateFields[2];  
let date = new Date(year, month, day);

Copying Items

The spread syntax can also be used to make a shallow copy of an array or an object as it works by creating copies of the top-level elements of an array or key-value pairs of an object and then inserting them into the place you used the spread operator with.

For copying arrays, we can write:

const arr = [1, 2, 3];  
const arr2 = [...arr, 4, 5];

The above example, arr2, is [1,2,3,4,5], while arr1 is still [1,2,3].

arr1 is not referenced by arr2 because the spread operator actually makes a copy of the array and then inserts the values. Note that this doesn’t work with multi-dimensional arrays as it only makes copies of the top-level elements.

We can apply the spread syntax multiple times in one array or object. An example for array would be:

let arr = [1, 2, 3];  
let arr2 = [4, 5];  
let arr3 = [...arr2, ...arr];

In the above example, we get [4,5,1,2,3]. arr1 and arr2 are unaffected as a copy of the values from arr1 and arr2 are inserted into arr3.


Spread Operator and Objects

With ES2018, the spread operator works with object literals. Then, key-value pairs of an object can be inserted into another object with the spread operator.

If there are two objects with the same key that the spread operator is applied to in the same object, the one that’s inserted later will overwrite the one that’s inserted earlier.

For example, if we have the following:

let obj1 = {foo: 'bar', a: 1};  
let obj2 = {foo: 'baz', b: 1};  
let obj3 = {...obj1, ...obj2 }

Then we get {foo: “baz”, a: 1, b: 1} as the value of obj3 because obj1 is spread before obj2.

They both have foo as a key in the object. First, foo: 'bar' is inserted by the spread operator to obj3. Then, foo: 'baz' overwrites the value of foo after obj2 is merged in, as it has the same key foo but inserted later.

This is great for merging objects as we don’t have to loop through the keys and put in the values, which is much more than one line of code.

One thing to note is that we can’t mix the spread operator between regular objects and iterable objects. For example, we will get TypeError if we write the following:

let obj = {foo: 'bar'};  
let array = [...obj];

Conclusion

As we can see, the spread syntax is a great convenience feature of JavaScript. It lets us combine different arrays into one.

Also, it lets us pass arrays into a function as arguments with just one line of code. With ES2018, we can also use the same operator to spread key-value pairs into other objects to populate one object’s key-value pairs into another object.

The spread operator works by copying the top-level items and populating them in the place you use the spread operator, so we can also use it to make shallow copies of arrays and objects.

Categories
JavaScript JavaScript Basics

Introducing JavaScript Template Strings

Template strings are a powerful feature of modern JavaScript released in ES6. It lets us insert/interpolate variables and expressions into strings without needing to concatenate like in older versions of JavaScript. It allows us to create strings that are complex and contain dynamic elements. Another great thing that comes with template strings is tags. Tags are functions that take a string and the decomposed parts of the string as parameters and are great for converting strings to different entities.

The syntax for creating template strings is by using backticks to delimit them. For example, we can write:

`This is a string`

This is a very simple example of a template string. All the content is constant and there are no variables or expressions in it. To add variables and or expressions to a string, we can do the following.

Interpolating Variables and Expressions

// New JS with templates and interpolation  
const oldEnoughToDrink = age >= 21;  
const oldEnough = `You are ${oldEnoughToDrink ? 'old enough' : 'too young'} to drink.`

We insert a variable or expression by adding a dollar sign $ and curly braces {} into the string `${variable}`. This is much better than the alternative in old JavaScript where we had to concatenate a string like the following:

// Old JS using concatenation  
const oldEnoughToDrink = age >= 21;  
const oldEnough = 'You are ' + (oldEnoughToDrink ? 'old enough' : 'too young') + ' to drink.'

As we can see it’s easy to make syntax errors with the old concatenation syntax if we have complex variables and expressions. Therefore template strings are a great improvement from what we had before.

If we want to use the backtick character in the content of string just put a \ before the backtick character in the string. => `will have a literal ` backtick`

Multiline Strings

Another great feature of template strings is that we can have strings with multiple lines for better code readability. This cannot be done in an easy way with the old style of strings. With the old style of strings, we had to concatenate each line of the string to put long strings in multiple lines like the following examples:

const multilineStr = 'This is a very long string' +   
  'that spans multiple lines.'

const multllineStr2 = 'At vero eos et accusamus et iusto odio' + 'dignissimos ducimus qui blanditiis praesentium voluptatum ' + 'deleniti atque corrupti quos dolores et quas molestias ' + 'excepturi sint occaecati cupiditate non provident, ' +   
'similique sunt in culpa qui ' +   
'officia deserunt mollitia animi.'

As we can see, this will become really painful is we have many more lines. It’s very frustrating to have all those plus signs and divide the string into multiple lines.

With template strings, we don’t have this problem:

const longString = `At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi.`

This is much better than concatenating strings together. It takes a lot less time to write the code and a lower probability of syntax errors. It’s also much more readable.

Note that this method does add an actual new like to the string \n and if you don’t want the string to have multiple lines in its final format, just put a \ at the end of the line.

Nesting Templates

Template strings can be nested in each other. This is a great feature because many people want to create dynamic strings. Template strings allow us to nest regular strings or template strings within template strings. For example, we can write the following:

const oldEnoughToDrink = age >= 21;  
const oldEnough = `You are ${oldEnoughToDrink ? 'old enough' : 'too young'} to drink.`

We can write the same thing with the expression interpolated in the string with template strings instead:

const TOO_YOUNG = 'too young';  
const OLD_ENOUGH = 'old enough';
const oldEnoughToDrink = age >= 21;
const oldEnough = `You are ${oldEnoughToDrink ? OLD_ENOUGH : TOO_YOUNG} to drink.`

This case is simple, but with complex string combinations, it is very powerful to be able to add logic to how we build our strings.

Tagged Templates

With template strings, we can add something called tags in front of a template string. They are functions that take a string and the decomposed parts of the string as parameters. A tagged function decomposes the template string into an array as the first parameter, with each item of the array as the constant parts of the string. The additional parameters of the function are all the variables and expressions in the order in which they appear in the string.

const personOldTag = (strings, nameExp, ageExp, genderExp)=>{  
  let str0 = strings[0]; // " is a "  
  let str1 = strings[1]; // " year old "  
  
  let oldStr;  
  if (ageExp > 75){  
    oldStr = 'senior';  
  } else {  
    oldStr = 'young';  
  }  
  
  // We can even return a string built using a template literal  
  return `${nameExp}${str0}${oldStr}${genderExp}.`;  
}

const name = 'Bob'  
const age = 80;  
const gender = 'male;
const result = personOldTag`${ name } is a ${ age } year old ${ gender }`;// result should be `Bob is a senior man.`

Tagged templates are great for converting strings to anything you want since it’s just a regular function. However, it is a special function because the first parameter is an array containing the constant parts of the string. The rest of the parameters contain the returned values that each expression returns. This is great for manipulating the string and transforming the returned values to what we want.

The return value of tags can be anything you want. So we can return strings, arrays, objects, or anything else.

As we see in the function above, in the string that we put beside the personOldTag, we first interpolated the name, then the age , then the gender. So in the parameters, they also appear in this order — nameExp, ageEx, and genderExp. They are the evaluated versions of the name, age, and gender in the template string.

Since tags are functions, we can return anything we want:

const isOldTag = (strings, nameExp, ageExp, genderExp)=>{  
  let str0 = strings[0]; // " is a "  
  let str1 = strings[1]; // " year old "  
  return ageExp > 75  
}

const name = 'Bob'  
const age = 80;  
const gender = 'male;
const result = isOldTag`${ name } is a ${ age } year old ${ gender }`;
// result is true

As you can see, we can manipulate the data to return a boolean instead of a string. Template tags are a feature that’s only available for template strings. To do the same thing with the old style of strings, we have to decompose the strings with our own string manipulating code and the built-in string manipulation functions, which is nowhere near as flexible as using tags. It decomposes the parts of a string into arguments for you that get passed in the template tags.

Raw Strings

Template strings have a special raw property that you can get from tags. The raw property gets us the string without escaping the special characters, hence the property is called raw. To access the raw property of a string, we can follow the example:

const logTag = (strings) => {  
  console.log(strings.raw[0]);  
}  
  
logTag`line 1 \r\n line 2\`;  
// logs "line 1 \r\n line 2" including characters '\', 'r' and 'n'

This is handy for visualizing the string as-is with the special characters intact.

There’s also the String.raw tag where we can take a template string and return a string with the escape characters without actually escaping them to see the string in its full form. For example, with the String.raw tag, we can write:

let hello = String.raw`Hello\n${'world'}!`;  
// "Hi\nworld!"  
  
hello.length;  
// 13  
  
hello.split('').join(',');  
// "H,e,l,l,o,\,n,w,o,r,l,d,!"

As we can see, we have all the characters of the string with the expressions evaluated, but we keep the escape characters as is.

Support for Template Strings

Template strings are supported by almost all browsers that are regularly maintained today. The only major one that doesn’t support it right out of the box is Internet Explorer. However, browsers that do not support it can add it with Babel. Node.js also supports it in version 4 or later.

Template strings are the new standard for JavaScript strings. It is much better than strings before ES6 in that it supports interpolating expressions inside strings. We also have tagged templates which are just functions, with the decomposed parts of the template string as parameters. The constant string in the first parameter is the string in a decomposed array form, and the evaluated versions of the expressions in the order they appeared in the string are the remaining arguments. Tagged template functions can return any variable type.

The raw property of a string, which is available in tags, can get the string with the escape characters unescaped.

Categories
JavaScript

Introduction to DOM Manipulation

Making websites dynamic is important for a lot of websites. Many of them have animations and dynamically display data. To change data without refreshing the page, we have to manipulate the document being displayed. Web pages are displayed in the browser by parsing them in the tree model called the Document Object Model (DOM).

The DOM tree is made up of individual components, called nodes. All web pages’ DOMs start with the root node, which is the document node. The node under the document node is the root element node.

For web pages, the root element node is the HTML node. Below that, every other node is under the HTML node, and there lie nodes below some of the nodes which are linked to the parent node. Together, the nodes form something that’s like an inverted tree.

There are several types of nodes in the DOM:

  • Document node — the root of the DOM in all HTML documents
  • Element node — the HTML elements
  • Attribute nodes — attributes of the element nodes
  • Text nodes — text content of HTML elements
  • Comment nodes — HTML comments in a document

Relationship of Nodes

The DOM is a tree with a root node and elements linked to the root node.

Every node has one parent node — except the root node. Each node can have one or more children. Nodes can also have siblings that reside at the same level of the given node.

If there are multiple elements of the same type, then you can get the node by getting the same type of node as a node list and then get the one you want by its index. A node list looks like an array, but it’s not one. You can loop through the elements of a node list, but array methods aren’t available in node lists.

For example, if we have the following HTML document …

<html>  
  <head>  
    <title>HTML Document</title>  
  </head>  
  <body>  
    <section>  
      <p>First</p>  
      <p>Second</p>  
      <p>Third</p>  
    </section>  
  </body>  
</html>

… then we can get the document p tags, by adding the following:

const list = document.body.childNodes\[1\].childNodes;  
for (let i = 0; i < list.length; i++) {  
  console.log(list)  
}

In the script of the document to get the nodes, the script gets the body and then gets the second child node (which is the section tag). Then it gets the child node by accessing the childNodes property again, which will get the p tags.

Together, we have:

<html>  
  <head>  
    <title>HTML Document</title>  
  </head>  
  <body>  
    <section>  
      <p>First</p>  
      <p>Second</p>  
      <p>Third</p>  
    </section>  
    <script>  
      const list = document.body.childNodes[1].childNodes;  
      for (let i = 0; i < list.length; i++) {  
        console.log(list)  
      }      
    </script>  
  </body>  
</html>

There are also convenience properties for accessing the first and last child and siblings of elements of a given node.

In a node element, we have the firstChild property to get the first child of a node. In the lastChild property, we can get the last child of a given node. nextSibling gets the next child node in the same parent node, and previousSibling gets the previous child node of the same parent node.

Each node has some properties and methods that allow us to get and set properties of a node. They are the following:

  • anchors gets a list of all anchors, elements with name attributes, in the document
  • applets gets an ordered list of all the applets in the document
  • baseURI gets the base URI of the document
  • body gets the <body> or the <frameset> node of the document body
  • cookie gets/sets a key value pair in the browser cookie
  • doctype gets the document type declaration of a document
  • documentElement gets the root document element
  • documentMode gets the mode used by the browser to render the document
  • documentURI gets/sets the location of the document
  • domain gets the domain name of the server that loaded the document
  • embeds gets all the embed elements in the document
  • forms gets all the form elements in the document
  • head gets the head element of the document
  • images gets all the img elements in the document
  • implementation gets the DOMImplementation object that handles the document
  • lastModified gets the latest date and time the document was modified
  • links gets all area and a tags that contain the href attribute
  • readyState gets the loading status of the document. readyState is loading when the document is loading, interactive when the document finishes parsing, and complete when it completes loading.
  • referrer gets the URL the current document is loaded from
  • scripts get the script elements in the document
  • title get the title element of the document
  • URL get the URL of of the document

The DOM has methods to get and manipulate elements and handle events by attaching event handlers. The methods are below:

  • addEventListener() attaches an event handler to the document
  • adoptNode() adopts a node from another document
  • close() closes the output writing stream of the document that was previously opened with document.open()
  • createAttribute() creates an attribute node
  • createComment() creates a common node
  • createDocumentFragment() creates an empty document fragment
  • createElement() creates an element node
  • createTextNode() creates a text node
  • getElementById() gets an element in the document with the given ID
  • getElementByClassName() gets all elements with the given class name
  • getElementByName() gets all elements with the given name attribute
  • getElementsByTagName() gets all elements with the given tag name
  • importNode() imports a node from another document
  • normalize() clears the text node and joins together adjacent nodes
  • querySelector() gets one element with the given CSS selector(s)
  • querySelectorAll() gets all elements with the given CSS selector(s)
  • removeEventListener() removes an event handler that’s been added using the addEventListener() method from the document
  • renameNode() renames an existing node
  • write() write JavaScript or HTML code to a document
  • writeln() write JavaScript or HTML code to a document and adds a new line after each statement

Element objects have special properties that can be get or set to modify the given element. The are:

  • accessKey gets/sets the accessKey attribute of an element
  • attributes gets all attributes of a node
  • childElementCount gets the number of child elements of a node
  • childNodes gets the child nodes of a node
  • children gets the child elements of an element
  • classList gets all the class names of an element
  • className gets or sets the class names of an element
  • clientHeight gets the height of an element including padding
  • clientLeft gets the left border width of an element
  • clientTop gets the top border width of an element
  • clientWidth gets the width of an element including padding
  • contentEditable gets or sets whether content is editable
  • dir gets or sets the dir attribute of an element
  • firstChild gets the first child node of an element
  • firstElementChild gets the first child element of an element
  • id gets or sets the ID of an element
  • innerHTML gets or sets the content of an element
  • isContentEditable gets whether the content ID is editable as a Boolean
  • lang gets or sets the language of the element or attribute
  • lastChild gets the last child node of an element
  • lastElementChild gets the last child element of an element
  • namespaceURI gets the namespace URI of the first node of an element
  • nextSibling gets the next node at the same node level
  • nextElementSibling gets the next element at the same level
  • nodeName gets the selected node’s name
  • nodeType gets the node type
  • nodeValue gets the value of the node
  • offsetHeight gets the height of the node, which includes padding, borders, and the scrollbar
  • offsetWidth gets the width of the node, which includes padding, borders, and the scrollbar
  • offsetLeft gets the horizontal offset of an element
  • offsetParent gets the offset container of an element
  • offsetTop gets the vertical offset of an element
  • ownerDocument gets the root element of an element
  • parentNode gets the parent node of an element
  • parentElement gets the parent element of an element
  • previousSibling gets the previous node of an element
  • previousElementSibling gets the previous element of an element at the same level
  • scrollHeight gets the height of an element, including padding
  • scrollLeft gets the number of pixels of the element that has been scrolled horizontally
  • scrollTop gets the number of pixels of the element that has been scrolled vertically
  • scrollWidth gets the width of an element, including padding
  • style gets the CSS styles of the element
  • tabIndex gets the tabindex attribute of an element
  • tagName gets the tag name of an element
  • textContent gets or sets the text content of an element
  • title gets or sets the title attribute of an element
  • length gets the number of nodes in a NodeList

An element has the following methods for manipulating it:

  • addEventLIstener() attaches an event handler to an element
  • appendChild() adds a child node to an element at the end
  • blur() removes the focus from an element
  • click() clicks on an element
  • cloneNode() clones the given node
  • compareDocumentPosition() compares the position of two elements
  • contains() checks if an element has a given node
  • focus() focuses on an element
  • getAttribute() gets an attribute of an element
  • getAttributeNode() gets an attribute of an element
  • getElementsByClassName() gets an element with the given class name
  • getElementByTagName() gets an element with the given tag name
  • getFeature() gets an object that implements the API of a given feature
  • hasAttribute() returns true if an element has the given attribute or false otherwise
  • hasAttributes() returns true if an element has the given attributes or false otherwise
  • hasChildNodes() returns true if an element has child node or false otherwise
  • insertBefore() adds a child node before the given element
  • isDefaultNamespace() returns true if the stated namespaceURI is the default or false otherwise
  • isEqualNode() checks whether two nodes are equal
  • isSameNode() checks if two nodes are the same
  • isSupported() returns true if a given feature is supported on an element
  • querySelector() gets the first element with the given CSS selector
  • querySelectorAll() gets all elements with the given CSS selector
  • removeAttribute() removes an attribute from the given element
  • removeAttributeNode() removes an attribute node from the given element
  • removeChild() removes the first child node
  • replaceChild() replaces a specified child node with another
  • removeEventListener() removes a specified event handler
  • setAttribute() sets the stated attribute to the specified value
  • setAttributeNode() sets the stated attribute node
  • toString() converts an element to a string
  • item() gets a node with the given index in the NodeList

Changing Element Content

If we select an element, we can set the innerHTML property to set the content of the element. For example, if we have an element DOM element, then we can write:

element.innerHTML = 'content';

We set the content inside the element and leave the rest unchanged.


Getting Elements

The most convenient way to work with elements is to get them with the methods listed above. The most commonly used ones are getElementById, getElementsByTagName, getElementsByClassName, and querySelector, querySelectorAll.

getElementById

getElementById lets us get an element by its ID, as its name suggested. It doesn’t matter where the element is, the browser will search the DOM until it finds an element with the given ID or returns null if it doesn’t exist.

We can use it as follows:

<html> 
  <head>  
    <title>Hello</title>  
  </head> 
  <body>  
    <p>  
      Hello <span id='name'>  
      </span>  
    </p>  
    <script>  
     const nameEl = document.getElementById('name');  
     nameEl.innerHTML = 'Jane';  
    </script> 
  </body>
</html>

In the code above, we get the element with the ID name and set the content to Jane, so we get “‘Hello Jane” on the screen.

getElementsByTagName

To get all the elements with the given tag name, we use the getElementsByTagName function to get all the elements with the given tag name.

We can use it as follows:

<html> 
  <head>  
    <title>How are You</title>  
  </head> 
  <body>  
    <p>  
      Hello <span></span>  
    </p>  
    <p>  
      How are you <span></span>?  
    </p>  
    <p>  
      Goodbye <span></span>  
    </p>  
    <script>  
      const spanEls = document.getElementsByTagName('span');  
      for (let i = 0; i < spanEls.length; i++) {  
        spanEls[i].innerHTML = 'Jane';  
      }  
    </script>  
  </body>  
</html>

In the code above, we get all the span elements with getElementsByTag to get all the span elements.

getElementsByClassName

To get all the elements with the given class name, we use the getElementsByClassName function.

We can use it as follows:

<html>
  <head>  
    <title>How are You</title>  
  </head><body>  
    <p>  
      Hello <span class='name'>  
      </span>  
    </p>  
    <p>  
      How are you <span class='name'>  
      </span>?  
    </p>  
    <p>  
      Goodbye <span class='name'>  
      </span>  
    </p>  
    <script>  
      const nameEls = document.getElementsByClassName('name');  
      for (let i = 0; i < nameEls.length; i++) {  
        nameEls[i].innerHTML = 'Jane';  
      }  
    </script>  
  </body>  
</html>

Appending Element to an existing Element

We can create a new element and attach it to an existing element as a child by using the createElement method and then using the appendChild method and pass in the element created with createElement as the argument of the appendChild function. We can use it like in the following example:

<html> 
  <head>  
    <title>Hello</title>  
  </head> 
  <body>  
    <h1>  
      Hello,  
    </h1>  
    <ul id='helloList'> </ul> <script>  
      const names = ['Mary', 'John', 'Jane'];  
      const helloList = document.getElementById('helloList');  
      for (let name of names) {  
        const li = document.createElement('li');  
        li.innerHTML = name;  
        helloList.appendChild(li);  
      }  
    </script>  
  </body>
</html>

Removing Elements

We can remove elements with the removeChild function. This means you have to find the child you want to remove and then get the parent node of that node. Then you can pass in that element object to the removeChild function to remove it.

Below is an example of this:

<html> 
  <head>  
    <title>Remove Items</title>  
  </head> 
  <body>  
    <ul>  
      <li id='1'>One <button onclick='remove(1)'>  
          remove  
        </button></li>  
      <li id='2'>Two <button onclick='remove(2)'>  
          remove  
        </button></li>  
      <li id='3'>Three <button onclick='remove(3)'>  
          remove  
        </button></li>  
    </ul> <script>  
      remove = function(id) {  
        const el = document.getElementById(id);  
        el.parentNode.removeChild(el);  
      }  
    </script>  
  </body>
</html>

As you can see, we have to get the node we want to remove and then get the parent node of that by using the parentNode property. Then we call removeChild on that. There’s no easier way to remove a node directly.

Now that we can create, manipulate, and remove nodes, we can make simple dynamic web pages without too much effort. Manipulating the DOM directly isn’t very sustainable for complex pages because lots of elements change in the DOM, making getting DOM elements directly difficult and error-prone. The DOM tree changes too much, so it’s very fragile if we make complex dynamic websites this way.

Categories
JavaScript Nodejs

Use JSON Web Tokens to Make a Secure Web App

With single page front end apps and mobile apps being more popular than ever, the front end is decoupled from the back end. Since almost all web apps need authentication, there needs to be a way for front end or mobile apps to store user identity data in a secure fashion.

JSON Web Tokens (JWT) is one of the most common ways to store authentication data on front end apps. With Node.js there are popular libraries that can generate and verify the JWT by check for its authenticity by check against a secret key stored in the back end and also check for expiry date.

The token is encoded in a standard format that is understood by most apps. It usually contains user identity data like user ID, user name, etc. It is given to the user when the user can successfully complete authentication.

In this piece, we will build an app that uses JWT to store authentication data. On the back end, we will use the Express framework, which runs on Node.js, and the jsonwebtoken package for generating and verify the token. For the front end, we will use the Angular framework and the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key and returned to the user, and stored on the front end app in local storage. Whenever the user needs to access authenticated routes on the back end, they will need the token. There will be a function in the back end app called middleware to check for a valid token. A valid token is one that is not expired and verifies to be valid against our secret key. There will also be signed up, and user credential settings pages in addition to the login page.


Now that we have our plan, we can begin by creating the front and back end app folders. Make one for each. Then we start writing the back end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we need to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors . @babel/register allows us to use the latest JavaScript features. express-jwt generates the JWT and verifies it against a secret.bcrypt does the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests.

Then we make our database migrations. Run npx sequelize-cli init to generate the skeleton code for our database to object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string

We make another migration and put:

'use strict';

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.addConstraint(  
        "Users",  
        ["email"],  
        {  
          type: "unique",  
          name: 'emailUnique'  
        }), 
      queryInterface.addConstraint(  
        "Users",  
        ["userName"],  
        {  
          type: "unique",  
          name: 'userNameUnique'  
        }),  
  }, 

  down: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.removeConstraint(  
        "Users",  
        'emailUnique'  
      ), 
      queryInterface.removeConstraint(  
        "Users",  
        'userNameUnique'  
      ),  
    ])  
  }  
};

This makes sure we don’t have duplicate entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Let’s write some code. Put the following in app.js:

require("babel/register");  
require("babel-polyfill");  
require('dotenv').config();  
const express = require('express');  
const bodyParser = require('body-parser');  
const cors = require('cors');  
const user = require('./controllers/userController');  
const app = express();
app.use(cors())  
app.use(bodyParser.urlencoded({ extended: true }));  
app.use(bodyParser.json());

app.use((req, res, next) => {  
  res.locals.session = req.session;  
  next();  
});  
  
app.use('/user', user);

app.get('*', (req, res) => {  
  res.redirect('/home');  
});

app.listen((process.env.PORT || 8080), () => {  
  console.log('App running on port 8080!');  
});

We need:

require("babel/register");  
require("babel-polyfill");

to use the latest features in JavaScript.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in controllers folder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

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

router.post('/login', async (req, res) => {  
    const secret = process.env.JWT_SECRET;  
    const userName = req.body.userName;  
    const password = req.body.password;  
    if (!userName || !password) {  
        return res.send({  
            error: 'User name and password required'  
        })  
    }  
    const users = await models.User.findAll({  
        where: {  
            userName  
        }  
    }) 

    const user = users[0];  
    if (!user) {  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    } 

    try {  
        const compareRes = await bcrypt.compare(password, user.hashedPassword);  
        if (compareRes) {  
            const token = jwt.sign(  
                {  
                    data: {  
                        userName,  
                        userId: user.id  
                    }  
                },  
                secret,  
                { expiresIn: 60 * 60 }  
            );  
            return res.send({ token });  
        }  
        else {  
            res.status(401);  
            return res.send({  
                error: 'Invalid username or password'  
            });  
        }  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    }});

router.post('/signup', async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const password = req.body.password;  
    try {  
        const hashedPassword = await bcrypt.hash(password, 10)  
        await models.User.create({  
            userName,  
            email,  
            hashedPassword  
        })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }  
});

router.put('/updateUser', authCheck, async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const token = req.headers.authorization;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        await models.User.update({  
            userName,  
            email  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

router.put('/updatePassword', authCheck, async (req, res) => {  
    const token = req.headers.authorization;  
    const password = req.body.password;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        const hashedPassword = await bcrypt.hash(password, saltRounds)  
        await models.User.update({  
            hashedPassword  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

module.exports = router;

The login route searches for the User entry, then, if it’s found, checks for the hashed password with the compare function of bcrypt. If both are successful, a JWT is generated. The signup route gets JSON payload of username and password and saves it. Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text. bcrypt.hash takes two arguments. This first is the plain text password, and the second is the number of salt rounds.

updatePassword route is an authenticated route. It checks for the token and, if it’s valid, will continue to save the user’s password by searching for the User with the user ID that matches the decoded token. We will add the authCheck middleware next.

Create a middlewares folder and create authCheck.js inside it.

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

This allows us to check for authentication in authenticated routes without repeating code. We place if in between the URL and our main route code in each authenticated route by importing this and referencing it.

We make an .env file of the root of the back end app folder, with this content:

DB_HOST='localhost'  
DB_NAME='login-app'  
DB_USERNAME='db-username'  
DB_PASSWORD='db-password'  
JWT_SECRET='secret'

The back end app is now complete. Now we move on to the front end Angular app.

Switch to your front end app folder. To build the Angular app, you need the Angular CLI.

To install it, run npm i -g @angular/cli in your Node.js command prompt. Then, run ng new frontend to generate the skeleton code for your front end app.

Also, install @angular/material according to the Angular documentation.

After that, replace the default app.module.ts with the following:

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import {  
  MatButtonModule,  
  MatCheckboxModule,  
  MatInputModule,  
  MatMenuModule,  
  MatSidenavModule,  
  MatToolbarModule,  
  MatTableModule,  
  MatDialogModule,  
  MAT_DIALOG_DEFAULT_OPTIONS,  
  MatDatepickerModule,  
  MatSelectModule,  
  MatCardModule  
} from '@angular/material';  
import { MatFormFieldModule } from '[@angular/material](http://twitter.com/angular/material)/form-field';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { FormsModule } from '@angular/forms';  
import { TopBarComponent } from './top-bar/top-bar.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
import { SessionService } from './session.service';  
import { HttpReqInterceptor } from './http-req-interceptor';  
import { UserService } from './user.service';  
import { CapitalizePipe } from './capitalize.pipe';

@NgModule({  
  declarations: [  
    AppComponent,  
    TopBarComponent,  
    HomePageComponent,  
    LoginPageComponent,  
    SignUpPageComponent,  
    SettingsPageComponent,  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatCheckboxModule,  
    MatFormFieldModule,  
    MatInputModule,  
    MatMenuModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatTableModule,  
    FormsModule,  
    HttpClientModule,  
    MatDialogModule,  
    MatDatepickerModule,  
    MatMomentDateModule,  
    MatSelectModule,  
    MatCardModule,  
    NgxMaterialTimepickerModule  
  ],  
  providers: [  
    SessionService,  
    {  
      provide: HTTP_INTERCEPTORS,  
      useClass: HttpReqInterceptor,  
      multi: true  
    },  
    UserService,  
    {  
      provide: MAT_DIALOG_DEFAULT_OPTIONS,  
      useValue: { hasBackdrop: false }  
    },  
  ],  
  bootstrap: [AppComponent],  
})  
export class AppModule { }

This creates all the dependencies and components which we will add. In order to make authenticated requests easy with our token, we create an HTTP request interceptor by creating http-req-interceptor.ts:

import { Injectable } from '@angular/core';  
import {  
    HttpEvent,  
    HttpInterceptor,  
    HttpHandler,  
    HttpResponse,  
    HttpErrorResponse,  
    HttpRequest  
} from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { environment } from '../environments/environment'  
import { map, filter, tap } from 'rxjs/operators';  
import { Router } from '@angular/router';

@Injectable()  
export class HttpReqInterceptor implements HttpInterceptor {  
    constructor(  
        public router: Router  
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        let modifiedReq = req.clone({}); 
        if (localStorage.getItem('token')) {  
            modifiedReq = modifiedReq.clone({  
                setHeaders: {  
                    authorization: localStorage.getItem('token')  
                }  
            });  
        } 
        return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {  
            if (event instanceof HttpResponse) {}  
        });  
    }  
}

We set the token in all requests except the login request.

In our environments/environment.ts, we have:

export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:8080'
};

This points to our back end’s URL.

Now we need to make our side nav. We want to add @ngrx/store to store the side nav’s state. We install the package by running npm install @ngrx/store --save. We add our reducer by running ng add @ngrx/store to add our reducers.

We add menu-reducers.ts to set the state centrally in our flux store:

const TOGGLE_MENU = 'TOGGLE_MENU';function menuReducer(state, action) {  
    switch (action.type) {  
        case TOGGLE_MENU:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { menuReducer, TOGGLE_MENU };

To link our reducer to other parts of the app, put the following in index.ts:

import { menuReducer } from './menu-reducer';  
import { tweetsReducer } from './tweets-reducer';export const reducers = {  
  menu: menuReducer,  
};

To get our Material Design look, add the following to style.css:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";  
body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

Between the head tags in index.html, we add:

<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Then we add a service for the user functions, by running ng g service user . The will create user.service.ts . We then put:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';  
import { Router } from '[@angular/router](http://twitter.com/angular/router)';  
import { JwtHelperService } from "@auth0/angular-jwt";

const helper = new JwtHelperService();

@Injectable({  
  providedIn: 'root'  
})  
export class UserService { constructor(  
    private http: HttpClient,  
    private router: Router  
  ) { } 

  signUp(data) {  
    return this.http.post(`${environment.apiUrl}/user/signup`, data);  
  } 

  updateUser(data) {  
    return this.http.put(`${environment.apiUrl}/user/updateUser`, data);  
  } 

  updatePassword(data) {  
    return this.http.put(`${environment.apiUrl}/user/updatePassword`, data);  
  } 

  login(data) {  
    return this.http.post(`${environment.apiUrl}/user/login`, data);  
  } 

  logOut() {  
    localStorage.clear();  
    this.router.navigate(['/']);  
  } 

  isAuthenticated() {  
    try {  
      const token = localStorage.getItem('token');  
      const decodedToken = helper.decodeToken(token);  
      const isExpired = helper.isTokenExpired(token);  
      return !!decodedToken && !isExpired;  
    }  
    catch (ex) {  
      return false;  
    }  
  }}

Each function requests a subscription for HTTP request except for the isAuthenticated function which is used to check for the token’s validity.

We also need routing for our app so we can see the pages when we go to the URLs listed below. In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { TweetsPageComponent } from './tweets-page/tweets-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component';  
import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component';  
import { IsAuthenticatedGuard } from './is-authenticated.guard';const routes: Routes = [  
  { path: 'login', component: LoginPageComponent },  
  { path: 'signup', component: SignUpPageComponent },  
  { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] },  
  { path: '**', component: HomePageComponent }];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

Now we create the parts that are referenced in the file above. We need to prevent people from access authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated . This generates is-authenticated.guard.ts.

We put the following in is-authenticated.guard.ts :

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';  
import { Observable } from 'rxjs';  
import { UserService } from './user.service';

@Injectable({  
  providedIn: 'root'  
})  
export class IsAuthenticatedGuard implements CanActivate {  
  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    const isAuthenticated = this.userService.isAuthenticated();  
    if (!isAuthenticated) {  
      localStorage.clear();  
      this.router.navigate(['/']);  
    }  
    return isAuthenticated;  
  }}

This uses our isAuthenticated function from UserService to check for a valid token. If it’s not valid, we clear it and redirect it back to the home page.

Now we create the forms for logging in setting the user data after logging. We run ng g component homePage, ng g component loginPage, ng g component topBar, ng g component signUpPage, and ng g component settingsPage . These are for the forms and the top bar components.

The home page is just a static page. We should have home-page.component.html and home-page.component.tsgenerated after running the commands in our last paragraph.

In home-page.component.html we put:

<div class="center">  
    <h1>Home Page</h1>  
</div>

Now we make our login page. In login-page.component.ts, we put:

<div class="center">  
    <h1>Log In</h1>  
</div>  
<form #loginForm='ngForm' (ngSubmit)='login(loginForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='loginData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='loginData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Log In</button>  
    <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a>  
</form>

In login-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';
[@Component](http://twitter.com/Component)({  
  selector: 'app-login-page',  
  templateUrl: './login-page.component.html',  
  styleUrls: ['./login-page.component.scss']  
})  
export class LoginPageComponent implements OnInit {  
  loginData: any = <any>{}; 

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { } 

  ngOnInit() {  
  } 

  login(loginForm: NgForm) {  
    if (loginForm.invalid) {  
      return;  
    }  
    this.userService.login(this.loginData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/settings']);  
      }, err => {  
        alert('Invalid username or password');  
      })  
  }  
}

We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise, an error alert will be displayed.

Then in our sign up page, sign-up-page.component.html, we put:

<div class="center">  
    <h1>Sign Up</h1>  
</div>  
<br>  
<form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='signUpData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='signUpData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='signUpData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Sign Up</button>  
</form>

and in sign-up-page.component.ts, we put:

import { Component, OnInit } from '[@angular/core](http://twitter.com/angular/core)';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';  
import _ from 'lodash';

@Component({  
  selector: 'app-sign-up-page',  
  templateUrl: './sign-up-page.component.html',  
  styleUrls: ['./sign-up-page.component.scss']  
})  
export class SignUpPageComponent implements OnInit {  
  signUpData: any = <any>{}; constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  } 

  signUp(signUpForm: NgForm) {  
    if (signUpForm.invalid) {  
      return;  
    }  
    this.userService.signUp(this.signUpData)  
      .subscribe(res => {  
        this.login();  
      }, err => {  
        console.log(err);  
        if (  
          _.has(err, 'error.error.errors') &&  
          Array.isArray(err.error.error.errors) &&  
          err.error.error.errors.length > 0  
        ) {  
          alert(err.error.error.errors[0].message);  
        }  
      })  
  } 

  login() {  
    this.userService.login(this.signUpData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/tweets']);  
      })  
  }  
}

These two pieces of code get the sign-up data and send it to the back end, which will save the file if they are all valid.

Similarly, in the settings-page.component.html:

<div class="center">  
    <h1>Settings</h1>  
</div>  
<br>  
<div>  
    <h2>Update User Info</h2>  
</div>  
<br>  
<form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='updateUserData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='updateUserData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update User Info</button>  
</form>  
<br><div>  
    <h2>Update Password</h2>  
</div>  
<br>  
<form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='updatePasswordData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update Password</button>  
</form>  
<br>
<div *ngIf='currentTwitterUser.id' class="title">  
    <h2>Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button>  
    </div>  
</div>  
<div *ngIf='!currentTwitterUser.id' class="title">  
    <h2>Not Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button>  
    </div>  
</div>

In settings-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { ActivatedRoute, Router } from '@angular/router';  
import { SessionService } from '../session.service';  
import { NgForm } from '@angular/forms';  
import { UserService } from '../user.service';

@Component({  
  selector: 'app-settings-page',  
  templateUrl: './settings-page.component.html',  
  styleUrls: ['./settings-page.component.scss']  
})  
export class SettingsPageComponent implements OnInit {  
  currentTwitterUser: any = <any>{};  
  elements: any[] = [];  
  displayedColumns: string[] = ['key', 'value'];  
  updateUserData: any = <any>{};  
  updatePasswordData: any = <any>{}; constructor(  
    private sessionService: SessionService,  
    private userService: UserService,  
    private router: Router  
  ) {  
  
  } 

  ngOnInit() {  
  
  } 

  updateUser(updateUserForm: NgForm) {  
    if (updateUserForm.invalid) {  
      return;  
    }  
    this.userService.updateUser(this.updateUserData)  
      .subscribe(res => {  
        alert('Updated user info successful.');  
      }, err => {  
        alert('Updated user info failed.');  
      })  
  } 

  updatePassword(updatePasswordForm: NgForm) {  
    if (updatePasswordForm.invalid) {  
      return;  
    }  
    this.userService.updatePassword(this.updatePasswordData)  
      .subscribe(res => {  
        alert('Updated password successful.');  
      }, err => {  
        alert('Updated password failed.');  
      })  
  }  
}

Similar to other pages, this sends a request payload for changing user data and password to our back end.

Finally, to make our top bar, we put the following in top-bar.component.html:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Twitter Automator  
</mat-toolbar>

And in top-bar.component.ts:

import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from '../reducers/menu-reducer';

@Component({  
  selector: 'app-top-bar',  
  templateUrl: './top-bar.component.html',  
  styleUrls: ['./top-bar.component.scss']  
})  
export class TopBarComponent implements OnInit {  
  menuOpen: boolean; constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  ngOnInit() {  
  } 

  toggleMenu() {  
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });  
  }  
}

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from './reducers/menu-reducer';  
import { UserService } from './user.service';

@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean; 

  constructor(  
    private store: Store<any>,  
    private userService: UserService  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  isAuthenticated() {  
    return this.userService.isAuthenticated();  
  }

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

  logOut() {  
    this.userService.logOut();  
  }  
}

and in app.component.html, we have:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    Twitter Automator  
                </b>  
            </li>  
            <li>  
                <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a>  
            </li>  
            <li>  
                <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a>  
            </li>  
            <li>  
                <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a>  
            </li>  
            <li>  
                <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a>  
            </li>  
            <li>  
                <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a>  
            </li>  
        </ul>
    </mat-sidenav>  
    <mat-sidenav-content>  
        <app-top-bar></app-top-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This allows us to toggle our side navigation menu. Note that we have:

@HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

to detect clicks out the side nav. If we click outside, i.e. not clicking on any element with those classes, then we close the menu. this.store.dispatch propagates the closed state to all components.