Categories
Express JavaScript Nodejs

Writing Express Middleware

Middleware functions are functions that have access to the request and response objects, and the next function for call the next middleware.

In this article, we’ll look at what Express middleware does and how we can write them.

Characteristics of Middleware

Middleware functions can run any code, make changes to the request and response object, end the request-response cycle, and call the next middleware in the stack.

A middleware may look something like the following code:

app.get('/', (req, res, next) => {  
  next();  
});

The code above has the request method in which the middleware will be called, which is the get in app.get .

Then we have the '/' which is the route path.

Finally, the middleware function that we pass in has the request and response objects as the first 2 parameters respectively, and the next function, which we call to run the next middleware.

Example

For example, we can write a middleware to log some output when we make a request to the / route.

We can write this as follows:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res, next) => {  
  console.log('middleware called');  
  next();  
});

app.get('/', (req, res) => {  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

next will call our route handler for the / route.

We should get middleware called from the console.log output and an empty response.

To load the middleware for all routes, we can use app.use instead of app.METHOD where METHOD is the request method is in lower case.

For example, we can write an app-wide middleware as follows:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  console.log('middleware called');  
  next();  
});

app.get('/', (req, res) => {  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

We should get the same thing as before, but if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  console.log('middleware called');  
  next();  
});

app.get('/', (req, res) => {  
  res.send();  
})

app.post('/foo', (req, res) => {  
  res.send('foo');  
})

app.listen(3000, () => console.log('server started'));

We get middleware called when we make a GET request to / and a POST request to /foo .

Modifying the Request and Response Objects

We can attach new properties and set values for the request and response objects.

For example, we can write:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  req.requestTime = Date.now();  
  next();  
});

app.get('/', (req, res) => {  
  res.json(req.requestTime);  
})

app.post('/foo', (req, res) => {  
  res.json(req.requestTime);  
})

app.listen(3000, () => console.log('server started'));

Then we get the timestamp of when the request is made when we make a GET request to / and a POST request to /foo .

Likewise, we can do something similar with the response object:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  res.responseTime = Date.now();  
  next();  
});

app.get('/', (req, res) => {  
  res.json(res.responseTime);  
})

app.post('/foo', (req, res) => {  
  res.json(res.responseTime);  
})

app.listen(3000, () => console.log('server started'));

We also will get the timestamp of when the response is made when we make a GET request to / and a POST request to /foo .

Configurable Middleware

We can make a function that has optional parameters and return a middleware function to create a configurable middleware function.

For example, we can write one as follows:

const express = require('express')  
const app = express()  
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

const configurableMiddleware = (options) => {  
  return (req, res, next) => {  
    req.options = options;  
    next();  
  }  
}

app.use(configurableMiddleware({ date: new Date(), foo: 'bar' }));

app.get('/', (req, res) => {  
  res.json(req.options);  
})

app.listen(3000, () => console.log('server started'));

The configurableMiddleware function takes an options object as a parameter and then return a middleware function with the req.options property set to the options parameter.

Then when we make a request to the / route then we get:

{"date":"2019-12-23T22:37:04.927Z","foo":"bar"}

as the response.

Conclusion

We can use Express middleware functions to run code before a route handler or another middleware is run.

To create a middleware function, we just have to create a function with the request and response objects as the first 2 parameters and the next function as the third parameter.

We can modify the request and response objects by adding new properties to it and set a value for them.

Then we call next to call the next middleware or route handler.

We can create configurable middleware by creating a function that takes an options parameter and returns a middleware function.

Categories
JavaScript React

Getting Started with React and JSX

React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

In this article, we’ll look at how to create simple apps with React.

Getting Started

The easiest way to create a React app is to use the Create React App Node package.

We can run it by running:

npx create-react-app my-app

Then we can go to the my-app and run the app by running:

cd my-app  
npm start

Create React App is useful for creating a single-page app.

React apps don’t handle any backend logic or databases.

We can use npm run build to build the app to create the built app for production.

Creating Our First React App

Once we ran Create React App as we did above, we can create our first app. To create it, we go into App.js and then start changing the code there.

To make writing our app easy, we’ll use JSX to do it. It’s a language that resembles HTML, but it’s actually just syntactic sugar on top of JavaScript.

Therefore, we’ll use the usual JavaScript constructs in JSX.

We’ll start by creating a Hello World app. To do this, we replace what’s there with the following in index.js:

import React from "react";  
import ReactDOM from "react-dom";

function App() {  
  return (  
    <div>  
      <h1>Hello World</h1>  
    </div>  
  );  
}

const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

In the code above, we have the App component, which is just a function. It returns:

<div>  
  <h1>Hello World</h1>  
</div>

which is our JSX code to display Hello World. h1 is a heading and div is a div element.

The code above looks like HTML, but it’s actually JSX.

What we have above is a function-based component since the component is written as a function.

In the last 2 lines, we get the element with ID root from public/index.html and put our App component inside it.

Another way to write the code above is to write:

import React from "react";  
import ReactDOM from "react-dom";

class App extends React.Component {  
  render() {  
    return (  
      <div>  
        <h1>Hello World</h1>  
      </div>  
    );  
  }  
}

const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

The code above is a class-based component, which has a render method to render the same JSX code into HTML.

The difference between the 2 examples is that one is a function and the other is a class that extends React.Component .

Otherwise, they’re the same. Any component file has to include:

import React from "react";  
import ReactDOM from "react-dom";

Otherwise, we’ll get an error.

React doesn’t require JSX, it’s just much more convenient to use it. A third way to create a Hello World app is to use the React.createElement method.

We can use the method as follows:

import React from "react";  
import ReactDOM from "react-dom";

const e = React.createElement;  
const App = e("h1", {}, "Hello World");

const rootElement = document.getElementById("root");  
ReactDOM.render(App, rootElement);

The first argument of the createElement method is the tag name as a string, the second argument has the props, which are objects that we pass to the component created, and the third argument is the inner text of them element.

We won’t be using this very often since it’ll get very complex if we have to nest components and adding interaction.

Embedding Expressions in JSX

We can embed JavaScript expressions between curly braces. For example, we can write:

import React from "react";  
import ReactDOM from "react-dom";  
function App() {  
  const greeting = "Hello World";  
  return (  
    <div>  
      <h1>{greeting}</h1>  
    </div>  
  );  
}  
const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

Then we see Hello World on the screen again.

Something more useful world calling a function as follows:

import React from "react";  
import ReactDOM from "react-dom";  
function App() {  
  const formatName = user => {  
    return `${user.firstName} ${user.lastName}`;  
  }; 

  const user = {  
    firstName: "Jane",  
    lastName: "Smith"  
  }; 

  return (  
    <div>  
      <h1>{formatName(user)}</h1>  
    </div>  
  );  
}  
const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

In the code above, we defined a formatName function inside the App component that takes a user object and returns user.firstName and user.lastName joined together.

Then we defined a user object with those properties and called the function inside the curly braces.

Whatever’s return will be displayed between the braces. In this case, it’ll be Jane Smith.

Conclusion

We can create a React app with the Create React App Node package.

Then we can add components as a function, class, or with React.createElement .

The first 2 ways are used most often since they return JSX in the function or the render method of the component class respectively.

JSX is much more convenient than createElement for writing JavaScript code with React, especially when our app gets complex.

We can embed JavaScript expressions in between curly braces.

Categories
JavaScript Vue

How To Add Parallax Scrolling to Your Vue.js App

Parallax scrolling is the effect where the background image scrolls slower than the elements in the foreground, creating an illusion of depth of the page.

Websites often use this for informational pages, where you have some text in the foreground and an image in the background that scrolls more slowly to create a more interesting experience for the user.

https://www.mockplus.com/blog/post/parallax-scrolling-websites has some examples of web pages with parallax scrolling.

With React, it is quick and simple to create the parallax scrolling effect with the Vue-Parallaxy library, located at https://github.com/apertureless/vue-parallax.

In this article, we will make an app that displays a list of images in the background with tags text in the foreground. The images will be provided by the Pixabay API. You can register for an API key at Pixabay.

To start the project, we create the project by running:

npx @vue/cli create photo-app

Then we select ‘Manually select features’ then choose to include Babel and Vue Router.

We need to install Axios to get images from the Pixabay API, BootstrapVue for styling, and Vue-Parallaxy to create the parallax scrolling effect. To install the packages run:

npm i axios bootstra-vue vue-parallaxy

With all the packages installed, we can start building the app. To start, we add a mixin for making the HTTP requests. Create a mixins folder in the src folder, then in the folder, create requestsMixins.js . In the file, add:

const axios = require("axios");  
const APIURL = "https://pixabay.com/api";
export const requestsMixin = {  
  methods: {  
    getImages(page = 1) {  
      return axios.get(  
        `${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}`  
      );  
    }  
  }  
};

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

<template>  
  <div class="page">  
    <div v-for="(img, i) of images" :key="i" class="parallax-container">  
      <parallax :speed-factor="0.5" direction="down" :parallax="true">  
        <div>  
          <img :src="img.largeImageURL" :alt="img.tags" style="image" />  
          <h1 class="parallax-title">{{img.tags}}</h1>  
        </div>  
      </parallax>  
      <br />  
    </div>  
  </div>  
</template>

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

export default {  
  name: "home",  
  mixins: [requestsMixin],  
  components: {  
    Parallax  
  },  
  data() {  
    return {  
      images: []  
    };  
  },  
  methods: {  
    async getImagesByPage() {  
      const response = await this.getImages();  
      this.images = response.data.hits;  
    }  
  },  
  beforeMount() {  
    this.getImagesByPage();  
  }  
};  
</script>

<style>  
.parallax-container {  
  position: relative;  
  height: 1000px;  
}

.parallax-title {  
  position: absolute;  
  top: 30%;  
  left: 0;  
  right: 0;  
  padding: 20px;  
  color: white;  
  text-align: center;  
}

.image {  
  height: 700px;  
}  
</style>

We include the Vue-Parallaxy component in this component by adding Parallax in the components object. Then we get the images by calling the this.getImages function from the requestsMixin we just created. We call the getImagesByPage function in the beforeMount hook to get the images when the page loads.

In the template, we use the parallax component provided by Vue-Parallaxy to create the parallax scrolling effect. The parallax serves as the container for the parallax effect. We make the speed of the scrolling different from the foreground with the speed-factor prop, we set the direction to down so that it scrolls down. parallax prop is set to true so that we get the different scrolling speed between the foreground and background.

We change the height of the parallax-container divs to the same height of 1000px, and the images to 700px to keep the spacing consistent.

In the component, we loop through the images and show some text from the Pixbay API. We position the text inside the photo by specifying:

<style>  
.parallax-container {  
  position: relative;  
}

.parallax-title {  
  position: absolute;  
  top: 30%;  
  left: 0;  
  right: 0;  
  padding: 20px;  
  color: white;  
  text-align: center;  
}  
</style>

We place the text in the center of the images and change the text color to white.

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

<template>  
  <div id="app">  
    <b-navbar toggleable="lg" type="dark" variant="info">  
      <b-navbar-brand href="#">Photo 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>  
    <router-view />  
  </div>  
</template>

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

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

We add some padding to the pages with the page class, and we add the BootstrapVue navigation bar to the top of the page. Also, we have the router-view so that we see the home page.

Next in main.js , we 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";

Vue.config.productionTip = false  
Vue.use(BootstrapVue);

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

to add the BootstrapVue libraries and styles to the app so we can use the code in our app and see the styling in the whole app.

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

import Vue from "vue";  
import Router from "vue-router";  
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({  
  mode: "history",  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: "/",  
      name: "home",  
      component: Home  
    }  
  ]  
});

We added the home page route so that we can see the page we built.

Then in the root folder of the project, we add an .env file so store the API key:

VUE_APP_API_KEY='Pixabay API key'

We can use this keep by referencing process.env.VUE_APP_API_KEY like we have in the requestsMixin.js.

Next 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>Photo App</title>  
  </head>  
  <body>  
    <noscript>  
      <strong  
        >We're sorry but vue-parallax-scrolling-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 title.

Categories
Angular JavaScript TypeScript

Build a Native Desktop App with Angular and Electron

Electron is a JavaScript framework that allows you to build cross-platform apps by converting a web app into a native app for the supported platforms. This provides a convenient way for web developers to write software for the platform that they are not familiar with by using web technologies. By using Node.js, it supports some native functionality like file manipulation and hardware interaction. Electron has been getting more popular over time, and now it is used for companies like Slack and Microsoft to develop their popular apps.

Angular is a popular framework for building web apps, and it has a powerful CLI that makes the building process seamless. However, it does not support building an app into an Electron without some changes. There is an Angular Electron boilerplate repository that combines the 2, making it easy for you to get started.

In this story, we will build an Electron app based on Angular which gets news headlines from the New York Times API, located at https://developer.nytimes.com/. To use it, you have to register for a free API key.

The API supports CORS, so front-end apps from domains outside of nytimes.com can access their APIs. This means that we can build an Angular app with it. To build our app, you have to go to the website and register for an API key.

To start building the app, we install the Angular CLI by running npm i -g @angular/cli. After that, instead of running ng new to create the project, we check out the angular-electron repo located at https://github.com/maximegris/angular-electron.git and use the latest version on the master branch.

Next, copy the code into your own project folder. Now we can start building the app. To start, we rename the app name to new-york-times. The renaming has to be done in multiple files: electron-builer.json, package.json, angular.json.

In electron-builder.json, we replace the existing code with the following:

{  
  "productName": "new-york-times",  
  "directories": {  
    "output": "release/"  
  },  
    "files": [  
        "**/*",  
        "!**/*.ts",  
        "!*.code-workspace",  
        "!LICENSE.md",  
        "!package.json",  
        "!package-lock.json",  
        "!src/",  
        "!e2e/",  
        "!hooks/",  
        "!angular.json",  
        "!_config.yml",  
        "!karma.conf.js",  
        "!tsconfig.json",  
        "!tslint.json"  
    ],  
  "win": {  
    "icon": "dist",  
    "target": [  
      "portable"  
    ]  
  },  
  "mac": {  
    "icon": "dist",  
    "target": [  
      "dmg"  
    ]  
  },  
  "linux": {  
    "icon": "dist",  
    "target": [  
      "AppImage"  
    ]  
  }  
}

Note the productName’s value is different from the original.

Then in package.json, we replace the existing code with:

{
  "name": "new-york-times",
  "version": "1.0.0",
  "description": "Angular 8 with Electron (Typescript + SASS + Hot Reload)",
  "homepage": "https://github.com/maximegris/angular-electron",
  "author": {
    "name": "Maxime GRIS",
    "email": "maxime.gris@gmail.com"
  },
  "keywords": [
    "angular",
    "angular 8",
    "electron",
    "typescript",
    "sass"
  ],
  "main": "main.js",
  "private": true,
  "scripts": {
    "postinstall": "npm run postinstall:electron && electron-builder install-app-deps",
    "postinstall:web": "node postinstall-web",
    "postinstall:electron": "node postinstall",
    "ng": "ng",
    "start": "npm run postinstall:electron && npm-run-all -p ng:serve electron:serve",
    "build": "npm run postinstall:electron && npm run electron:serve-tsc && ng build --base-href ./",
    "build:dev": "npm run build -- -c dev",
    "build:prod": "npm run build -- -c production",
    "ng:serve": "ng serve",
    "ng:serve:web": "npm run postinstall:web && ng serve -o",
    "electron:serve-tsc": "tsc -p tsconfig-serve.json",
    "electron:serve": "wait-on http-get://localhost:4200/ && npm run electron:serve-tsc && electron . --serve",
    "electron:local": "npm run build:prod && electron .",
    "electron:linux": "npm run build:prod && electron-builder build --linux",
    "electron:windows": "npm run build:prod && electron-builder build --windows",
    "electron:mac": "npm run build:prod && electron-builder build --mac",
    "test": "npm run postinstall:web && ng test",
    "e2e": "npm run build:prod && mocha --timeout 300000 --require ts-node/register e2e/**/*.spec.ts",
    "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
    "lint": "ng lint"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.802.2",
    "@angular/cli": "8.2.2",
    "@angular/common": "8.2.2",
    "@angular/compiler": "8.2.2",
    "@angular/compiler-cli": "8.2.2",
    "@angular/core": "8.2.2",
    "@angular/forms": "8.2.2",
    "@angular/language-service": "8.2.2",
    "@angular/platform-browser": "8.2.2",
    "@angular/platform-browser-dynamic": "8.2.2",
    "@angular/router": "8.2.2",
    "@ngx-translate/core": "11.0.1",
    "@ngx-translate/http-loader": "4.0.0",
    "@types/jasmine": "3.3.16",
    "@types/jasminewd2": "2.0.6",
    "@types/mocha": "5.2.7",
    "@types/node": "12.6.8",
    "chai": "4.2.0",
    "codelyzer": "5.1.0",
    "conventional-changelog-cli": "2.0.21",
    "core-js": "3.1.4",
    "electron": "6.0.2",
    "electron-builder": "21.2.0",
    "electron-reload": "1.5.0",
    "jasmine-core": "3.4.0",
    "jasmine-spec-reporter": "4.2.1",
    "karma": "4.2.0",
    "karma-chrome-launcher": "3.0.0",
    "karma-coverage-istanbul-reporter": "2.1.0",
    "karma-jasmine": "2.0.1",
    "karma-jasmine-html-reporter": "1.4.2",
    "mocha": "6.2.0",
    "npm-run-all": "4.1.5",
    "rxjs": "6.5.2",
    "spectron": "8.0.0",
    "ts-node": "8.3.0",
    "tslint": "5.18.0",
    "typescript": "3.5.3",
    "wait-on": "3.3.0",
    "webdriver-manager": "12.1.5",
    "zone.js": "0.9.1"
  },
  "engines": {
    "node": ">=10.9.0"
  },
  "dependencies": {
    "@angular/animations": "^8.2.3",
    "@angular/cdk": "^8.1.3",
    "@angular/material": "^8.1.3",
    "@angular/material-moment-adapter": "^8.1.3",
    "@ngrx/store": "^8.2.0",
    "moment": "^2.24.0"
  }
}

Note that in the build script, we have --base-href ./ at the end. This is important because the styles and script will have the wrong path if we didn’t add it. The base-href is needed to specify that we tell the Electron web view to find the scripts and style files by their relative path instead of the absolute path.

We also included all the libraries we need like Angular Material, Moment.js, NGRX store, and the scripts for building and running our app. We changed the first key value pair to “name”: “new-york-times”.

After that, we do a similar renaming in angular.json by replacing what is there with the following:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "new-york-times": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "tsConfig": "src/tsconfig.app.json",
            "polyfills": "src/polyfills.ts",
            "assets": [
              "src/assets",
              "src/favicon.ico",
              "src/favicon.png",
              "src/favicon.icns",
              "src/favicon.256x256.png",
              "src/favicon.512x512.png"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "dev": {
              "optimization": false,
              "outputHashing": "all",
              "sourceMap": true,
              "extractCss": true,
              "namedChunks": false,
              "aot": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": false,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.dev.ts"
                }
              ]
            },
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "new-york-times:build"
          },
          "configurations": {
            "dev": {
              "browserTarget": "new-york-times:build:dev"
            },
            "production": {
              "browserTarget": "new-york-times:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "new-york-times:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills-test.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "scripts": [],
            "styles": [
              "src/styles.scss"
            ],
            "assets": [
              "src/assets",
              "src/favicon.ico",
              "src/favicon.png",
              "src/favicon.icns",
              "src/favicon.256x256.png",
              "src/favicon.512x512.png"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    },
    "new-york-times-e2e": {
      "root": "e2e",
      "projectType": "application",
      "architect": {
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "e2e/tsconfig.e2e.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "new-york-times",
  "schematics": {
    "@schematics/angular:component": {
      "prefix": "app",
      "styleext": "scss"
    },
    "@schematics/angular:directive": {
      "prefix": "app"
    }
  }
}

We renamed all instances of angular-electron with new-york-times in this file.

Next, in environment.ts, environment.prod.ts, and environment.dev.ts, we put

apikey: 'new york times api key',  
apiUrl: 'https://api.nytimes.com/svc'

into the AppConfig object. Replace the New York Times API key with your own.

In index.html, we change it to:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>New York Times</title>
  <base href="/">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

The text between the <title> tags will be the title that will be displayed in our app.

Also, we need to install the packages listed in package.json. We do this by running npm i. Next we run ng add @ngrx/store to add the boilerplate of @ngrx/store flux store into our app.

With that completed, we can begin building the app. To start, we run the following commands:

$ ng g component homePage  
$ ng g component articleSearchPage  
$ ng g component articleSearchResults  
$ ng g component toolBar  
$ ng g class menuReducer  
$ ng g class searchResultsReducer  
$ ng g service nyt

These commands will create the files that we need for our app. Now we can include the library modules into our main app module. To do this, we put the following into app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { ArticleSearchPageComponent } from './article-search-page/article-search-page.component';
import { ArticleSearchResultsComponent } from './article-search-results/article-search-results.component';
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { NytService } from './nyt.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { ToolBarComponent } from './tool-bar/tool-bar.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { HttpClientModule } from '@angular/common/http';
import { MatSelectModule } from '@angular/material/select';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list';
@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    ArticleSearchPageComponent,
    ArticleSearchResultsComponent,
    ToolBarComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    StoreModule.forRoot(reducers),
    FormsModule,
    MatSidenavModule,
    MatToolbarModule,
    MatInputModule,
    MatFormFieldModule,
    MatDatepickerModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatMomentDateModule,
    HttpClientModule,
    MatSelectModule,
    MatCardModule,
    MatListModule,
    MatMenuModule,
    MatIconModule,
    MatGridListModule
  ],
  providers: [
    NytService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Most of the modules in the import array are Angular Material modules. We will use them throughout the app.

Now we have to create the part of the app where we get and store data. To do this, run:

$ ng g service nyt

This is where we make our HTTP calls to the New York Times API. Now we should have a file called nyt.service.ts. In there, we put:

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable({
  providedIn: 'root'
})
export class NytService {
  constructor(
    private http: HttpClient
  ) { }

  search(data) {
    let params: HttpParams = new HttpParams();
    params = params.set('api-key', environment.apikey);
    if (data.q !== undefined) {
      params = params.set('q', data.q);
    }
    if (data.begin_date !== undefined) {
      params = params.set('begin_date', data.begin_date);
    }
    if (data.end_date !== undefined) {
      params = params.set('end_date', data.end_date);
    }
    if (data.sort !== undefined) {
      params = params.set('sort', data.sort);
    }
    return this.http.get(`${environment.apiUrl}/search/v2/articlesearch.json`, { params });
  }

  getArticles(section: string = 'home') {
    let params: HttpParams = new HttpParams();
    params = params.set('api-key', environment.apikey);
    return this.http.get(`${environment.apiUrl}/topstories/v2/${section}.json`, { params });
  }
}

The search function takes the data we will pass in, and if it is defined, then it will be included in the GET request’s query string. The second parameter in the this.http.get function takes a variety of options, including headers and query parameters. HttpParams objects are converted to query strings when the code is executed. getArticles does similar things as the search function except with a different URL.

Then in environment.ts, we put:

export const environment = {  
  production: false,  
  apikey: 'your api key',  
  apiUrl: 'https://api.nytimes.com/svc' 
};

This makes the URL and API key referenced in the service file available.

Next, we need to add a Flux data store to persist our menu state and search results. First we have to run:

$ ng add @ngrx/store

This adds the boilerplate code to the Flux store. Then, we run:

$ ng g class menuReducer  
$ ng g class searchResultsReducer

We execute this in the src\app\reducers folder, which was created after running ng add @ngrx/store to make the files for our reducers.

Then in menu-reducer.ts, we put:

export const SET_MENU_STATE = 'SET_MENU_STATE';

export function MenuReducer(state: boolean, action) {  
    switch (action.type) {  
        case SET_MENU_STATE:  
            return action.payload;default:  
            return state;  
    }  
}

And in search-result-reducer.ts, we put:

export const SET_SEARCH_RESULT = 'SET_SEARCH_RESULT';

export function SearchResultReducer(state, action) {  
    switch (action.type) {  
        case SET_SEARCH_RESULT:  
            return action.payload;default:  
            return state;  
    }  
}

These two pieces of code will allow the menu and search results to be stored in memory and be propagated to components that subscribe to the data.

Next in src\app\reducers\index.ts, we put:

import { SearchResultReducer } from './search-results-reducer';  
import { MenuReducer } from './menu-reducer';

export const reducers = {  
  searchResults: SearchResultReducer,  
  menuState: MenuReducer  
};

This will allow our module to access our reducers since we have StoreModule.forRoot(reducers) in app.module.ts.

Now we’ll work on our app’s toolbar. To make the toolbar, we put the following in tool-bar.component.ts:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { SET_MENU_STATE } from '../reducers/menu-reducer';
@Component({
  selector: 'app-tool-bar',
  templateUrl: './tool-bar.component.html',
  styleUrls: ['./tool-bar.component.scss']
})
export class ToolBarComponent implements OnInit {
  menuOpen: boolean;
  constructor(
    private store: Store<any>
  ) {
    store.pipe(select('menuState'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }

  ngOnInit() {
  }

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

This sends the state of the menu to the rest of the app.

store.pipe(select('menuState'))  
  .subscribe(menuOpen => {  
    this.menuOpen = menuOpen;  
  })

The above gets the state of the menu and is used for displaying and toggling the menu state.

In the corresponding template, tool-bar.component.html, we put:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    New York Times App  
</mat-toolbar>

And in tool-bar.component.scss, we put:

.menu-button {  
  margin-top: 6px;  
  margin-right: 10px;  
  cursor: pointer;  
}.mat-toolbar {  
  background: #009688;  
  color: white;  
}

In app.component.scss, we put:

#content {  
  padding: 20px;  
  min-height: 100vh;  
}

ul {  
  list-style-type: none;  
  margin: 0;  
  li {  
    padding: 20px 5px;  
  }  
}

This changes the color of the toolbar.

Then in app.component.ts, we put:

import { Component, HostListener } from '@angular/core';
import { SET_MENU_STATE } from './reducers/menu-reducer';
import { Store, select } from '@ngrx/store';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  menuOpen: boolean;
  constructor(
    private store: Store<any>,
  ) {
    store.pipe(select('menuState'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }

  @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: SET_MENU_STATE, payload: this.menuOpen });
    }
  }
}

This makes it so when we click outside of the left side menu, it’ll be closed.

In app.component.html, we put:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    New York Times  
                </b>  
            </li>  
            <li>  
                <a routerLink='/'>Home</a>  
            </li>  
            <li>  
                <a routerLink='/search'>Search</a>  
            </li>  
        </ul></mat-sidenav>  
    <mat-sidenav-content>  
        <app-tool-bar></app-tool-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This displays our left side menu and routes.

In style.scss, we put:

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

This imports the Material Design styles and sets the width of our forms.

Then in app-routing.module.ts, we put the following so we can see the pages we made when we go the specified URLs:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { ArticleSearchPageComponent } from './article-search-page/article-search-page.component';

const routes: Routes = [  
  { path: '', component: HomePageComponent },  
  { path: 'search', component: ArticleSearchPageComponent }  
];

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

In index.html, we put:

<!doctype html>  
<html lang="en"><head>  
  <meta charset="utf-8">  
  <title>New York Time</title>  
  <base href="/">  
  <link href="[https://fonts.googleapis.com/icon?family=Material+Icons](https://fonts.googleapis.com/icon?family=Material+Icons)" rel="stylesheet">  
  <link href="[https://fonts.googleapis.com/css?family=Roboto&display=swap](https://fonts.googleapis.com/css?family=Roboto&display=swap)" rel="stylesheet">  
  <meta name="viewport" content="width=device-width, initial-scale=1">  
  <link rel="icon" type="image/x-icon" href="favicon.ico">  
</head><body>  
  <app-root></app-root>  
</body></html>

This includes the Roboto font commonly used with Material Design and Material Icons.

Now we build the logic for our two pages. First we start with our home page. In home-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { NytService } from '../nyt.service';

@Component({  
  selector: 'app-home-page',  
  templateUrl: './home-page.component.html',  
  styleUrls: ['./home-page.component.scss']  
})  
export class HomePageComponent implements OnInit {  
  sections: string[] =  
    `arts, automobiles, books, business, fashion, food, health,  
    home, insider, magazine, movies, national, nyregion, obituaries,  
    opinion, politics, realestate, science, sports, sundayreview,  
    technology, theater, tmagazine, travel, upshot, world`  
      .replace(/ /g, '')  
      .split(',');  
  results: any[] = [];  
  selectedSection: string = 'home';

  constructor(  
    private nytService: NytService  
  ) { }

  ngOnInit() {  
    this.getArticles();  
  }

  getArticles() {  
    this.nytService.getArticles(this.selectedSection)  
      .subscribe(res => {  
        this.results = (res as any).results;  
      })  
  }  
}

We get the articles on the first load with the ngOnInit function, and then once the page is loaded, we can choose which section to load.

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

<div class="center">  
    <h1>{{selectedSection | titlecase }}</h1>  
    <mat-menu #appMenu="matMenu">  
        <button mat-menu-item *ngFor='let s of sections' (click)='selectedSection = s; getArticles()'>{{s | titlecase }}  
        </button>  
    </mat-menu><button mat-raised-button [matMenuTriggerFor]="appMenu">  
        Sections  
    </button>  
</div>  
<br>
<mat-card *ngFor='let r of results'>  
    <mat-list role="list">  
        <mat-list-item>  
            <mat-card-title>  
                {{r.title}}  
            </mat-card-title>  
        </mat-list-item>  
    </mat-list>  
    <mat-card-subtitle>  
        <mat-list role="list">  
            <mat-list-item>Published Date: {{r.published_date | date: 'full' }}</mat-list-item>  
            <mat-list-item><a href='{{r.url}}'>Link</a></mat-list-item>  
            <mat-list-item *ngIf='r.byline'>{{r.byline}}</mat-list-item>  
        </mat-list>  
    </mat-card-subtitle>  
    <mat-card-content>  
        <mat-list role="list">  
            <mat-list-item>{{r.abstract}}</mat-list-item>  
        </mat-list>  
        <img *ngIf='r.multimedia[r.multimedia.length - 1]?.url' [src]='r.multimedia[r.multimedia.length - 1]?.url'  
            [alt]='r.multimedia[r.multimedia.length - 1]?.caption' class="image">  
    </mat-card-content>  
</mat-card>

This is where we display the results from the New York Times API, including headline titles, pictures, publication date, and other data. | titlecase is called a pipe. It maps the object to the left of the pipe symbol by calling the function on the right.

The titlecase pipe for example, converts a string to a title-case string. Pipes can also take arguments like date: ‘full’ to set options that can used with the pipe.

The code on the top of the file is where we enable users to select the section they want to load by letting them choose the section they want to load. It’s here:

<div class="center">  
    <h1>{{selectedSection | titlecase }}</h1>  
    <mat-menu #appMenu="matMenu">  
        <button mat-menu-item *ngFor='let s of sections' (click)='selectedSection = s; getArticles()'>{{s | titlecase }}  
        </button>  
    </mat-menu><button mat-raised-button [matMenuTriggerFor]="appMenu">  
        Sections  
    </button>  
</div>  
<br>

This block splits the string into an array of strings with those names and without the spaces:

sections: string[] =  
    `arts, automobiles, books, business, fashion, food, health,  
    home, insider, magazine, movies, national, nyregion, obituaries,  
    opinion, politics, realestate, science, sports, sundayreview,  
    technology, theater, tmagazine, travel, upshot, world`  
      .replace(/ /g, '')  
      .split(',');

Then in home-page.component.scss, we put:

.image {  
  width: 100%;  
  margin-top: 30px;  
}

This styles the pictures displayed.

Next we build the page to search for articles. It has a form and a space to display the results.

In article-search.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { SearchData } from '../search-data';  
import { NgForm } from '@angular/forms';  
import { NytService } from '../nyt.service';  
import * as moment from 'moment';  
import { Store } from '@ngrx/store';  
import { SET_SEARCH_RESULT } from '../reducers/search-results-reducer';

@Component({  
  selector: 'app-article-search-page',  
  templateUrl: './article-search-page.component.html',  
  styleUrls: ['./article-search-page.component.scss']  
})  
export class ArticleSearchPageComponent implements OnInit {  
  searchData: SearchData = <SearchData>{  
    sort: 'newest'  
  };  
  today: Date = new Date();constructor(  
    private nytService: NytService,  
    private store: Store<any>  
  ) {}

  ngOnInit() {  
  }

  search(searchForm: NgForm) {  
    if (searchForm.invalid) {  
      return;  
    }  
    const data: any = {  
      begin_date: moment(this.searchData.begin_date).format('YYYYMMDD'),  
      end_date: moment(this.searchData.end_date).format('YYYYMMDD'),  
      q: this.searchData.q  
    }  
    this.nytService.search(data)  
      .subscribe(res => {  
        this.store.dispatch({ type: SET_SEARCH_RESULT, payload: (res as any).response.docs });  
      })  
  }}

This gets the data when we click search and propagates the results to the Flux store, which will be used to display the data at the end.

In article-search.component.html, we put:

<div class="center">
    <h1>Search</h1>
</div>
<br>
<form #searchForm='ngForm' (ngSubmit)='search(searchForm)'>
    <mat-form-field>
        <input matInput placeholder="Keyword" required #keyword='ngModel' name='keyword' [(ngModel)]='searchData.q'>
        <mat-error *ngIf="keyword.invalid && (keyword.dirty || keyword.touched)">
            <div *ngIf="keyword.errors.required">
                Keyword is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <mat-form-field>
        <input matInput [matDatepicker]="startDatePicker" placeholder="Start Date" [max]="today" #startDate='ngModel'
            name='startDate' [(ngModel)]='searchData.begin_date'>
        <mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
        <mat-datepicker #startDatePicker></mat-datepicker>
    </mat-form-field>
    <br>
    <mat-form-field>
        <input matInput [matDatepicker]="endDatePicker" placeholder="End Date" [max]="today" #endDate='ngModel'
            name='endDate' [(ngModel)]='searchData.end_date'>
        <mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
        <mat-datepicker #endDatePicker></mat-datepicker>
    </mat-form-field>
    <br>
    <mat-form-field>
        <mat-label>Sort By</mat-label>
        <mat-select required [(value)]="searchData.sort">
            <mat-option value="newest">Newest</mat-option>
            <mat-option value="oldest">Oldest</mat-option>
            <mat-option value="relevance">Relevance</mat-option>
        </mat-select>
    </mat-form-field>
    <br>
    <button mat-raised-button type='submit'>Search</button>
</form>
<br>
<app-article-search-results></app-article-search-results>

This is the search form for the articles. It includes a keyword field, start and end date datepickers, and a drop-down to select the way to sort. These are all Angular Material components. <app-article-search-results></app-article-search-results> is the article search result component which we generated but have not built yet.

Note that the [( in [(ngModel)] denotes two-way data binding between the component or directive, and the current component and [ denote one-way binding from the current component to the directive or component.

Next in article-search.results.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
@Component({
  selector: 'app-article-search-results',
  templateUrl: './article-search-results.component.html',
  styleUrls: ['./article-search-results.component.scss']
})
export class ArticleSearchResultsComponent implements OnInit {
  searchResults: any[] = [];
  constructor(
    private store: Store<any>
  ) {
    store.pipe(select('searchResults'))
      .subscribe(searchResults => {
        this.searchResults = searchResults;
      })
  }

  ngOnInit() {
  }
}

This block gets the article search results stored in our Flux store and sends it to our template for displaying:

store.pipe(select('searchResults'))  
      .subscribe(searchResults => {  
        this.searchResults = searchResults;  
      })

In article-search.results.html, we put:

<mat-card *ngFor='let s of searchResults'>
    <mat-list role="list">
        <mat-list-item>
            <mat-card-title>
                {{s.headline.main}}
            </mat-card-title>
        </mat-list-item>
    </mat-list>
    <mat-card-subtitle>
        <mat-list role="list">
            <mat-list-item>Date: {{s.pub_date | date: 'full' }}</mat-list-item>
            <mat-list-item><a href='{{s.web_url}}'>Link</a></mat-list-item>
            <mat-list-item *ngIf='s.byline.original'>{{s.byline.original}}</mat-list-item>
        </mat-list>
    </mat-card-subtitle>
    <mat-card-content>
        <div class="content">
            <p>{{s.lead_paragraph}}</p>
            <p>{{s.snippet}}</p>
        </div>
    </mat-card-content>
</mat-card>

This just displays the results from the store.

In article-search-results.component.scss, we add:

.content {  
  padding: 0px 15px;  
}

This adds some padding to the paragraphs.

With the logic done, we just need to replace the code in core.module.ts, with the following:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppModule } from '../app.module';
@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    AppModule
  ]
})
export class CoreModule { }

And in shared.module.ts, we put:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { WebviewDirective } from './directives/';
import { PageNotFoundComponent } from './components';
@NgModule({
  declarations: [ WebviewDirective, PageNotFoundComponent],
  imports: [CommonModule, TranslateModule],
  exports: [TranslateModule, WebviewDirective]
})
export class SharedModule {}

to include the PageNotFoundComponent in the module so that the build will proceed.

After adding all the code, we can run npm run ng:serve:web to preview the app in a browser, and to preview it as an Electron, we run npm run electron:local .

Finally, to build the app into a Windows executable, run npm run electron:windows . The executable should be located in the root of the release folder in the project folder.

Categories
JavaScript React

How to Add Browser Notifications to Your React App

With the HTML5 Notification API, browsers can display native popup notifications to users. With notifications, you can display text and icons, and also play sound with them. The full list of options is located at https://developer.mozilla.org/en-US/docs/Web/API/notification. Users have to grant permission to display notifications when they visit a web app to see browser notifications.

Developers have done the hard work for us if we use React because a React component is created to display browser notifications. The React-Web-Notification package, located at https://www.npmjs.com/package/react-web-notification can let us display popups and handle the events that are associated with display the notifications like when use clicks on the notification or handle cases when permissions or granted or denied for display notifications.

In this article, we will build a password manager that lets you enter, edit and delete passwords to the websites and show notifications whenever these actions are taken. We will use React to build the app.

To start, we will run Create React App to create the app. Run:

npx create-react-app password-manager

to create the app. Next, we add our own libraries, we will use Axios for making HTTP requests to our back end, Formik and Yup for form value handling and form validation respectively, MobX for state management, React Bootstrap for styling, React-Copy-To-Clipboard for letting us copy data to the clipboard, and React Router for routing.

We install them by running:

npm i axios formik mobx mobx-react react-bootstrap react-copy-to-clipboard react-router-fom yup react-web-notifications

With all the libraries installed, we can start building our app. We create all the files in the src folder unless otherwise specified.

First, we replace the existing code in App.css with:

.bg-primary {  
  background-color: #09d3ac !important;  
}

to change the top bar’s background color. Next in App.js , replace the current code with:

import React from "react";  
import { Router, Route } from "react-router-dom";  
import HomePage from "./HomePage";  
import { createBrowserHistory as createHistory } from "history";  
import Navbar from "react-bootstrap/Navbar";  
import Nav from "react-bootstrap/Nav";  
import "./App.css";  
const history = createHistory();

function App({ passwordsStore }) {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <Navbar bg="primary" expand="lg" variant="dark">  
          <Navbar.Brand href="#home">Password Manager</Navbar.Brand>  
          <Navbar.Toggle aria-controls="basic-navbar-nav" />  
          <Navbar.Collapse id="basic-navbar-nav">  
            <Nav className="mr-auto">  
              <Nav.Link href="/" active>Home</Nav.Link>  
            </Nav>  
          </Navbar.Collapse>  
        </Navbar>  
        <Route  
          path="/"  
          exact  
          component={props => (  
            <HomePage {...props} passwordsStore={passwordsStore} />  
          )}  
        />  
      </Router>  
    </div>  
  );  
}export default App;

to add our React Bootstrap top bar and our route to the home page. passwordStore is our MobX store for storing our password list in the front end.

Next create HomePage.css and add:

.home-page {  
  padding: 20px;  
}

to add some padding to our page.

Then create HomePage.js and add:

import React from "react";  
import { useState, useEffect } from "react";  
import Table from "react-bootstrap/Table";  
import ButtonToolbar from "react-bootstrap/ButtonToolbar";  
import Button from "react-bootstrap/Button";  
import Modal from "react-bootstrap/Modal";  
import PasswordForm from "./PasswordForm";  
import "./HomePage.css";  
import { deletePassword, getPasswords } from "./requests";  
import { observer } from "mobx-react";  
import { CopyToClipboard } from "react-copy-to-clipboard";  
import Notification from "react-web-notification";

function HomePage({ passwordsStore }) {  
  const [openAddModal, setOpenAddModal] = useState(false);  
  const [openEditModal, setOpenEditModal] = useState(false);  
  const [initialized, setInitialized] = useState(false);  
  const [selectedPassword, setSelectedPassword] = useState({});  
  const [notificationTitle, setNotificationTitle] = React.useState(""); const openModal = () => {  
    setOpenAddModal(true);  
  }; 

  const closeModal = () => {  
    setOpenAddModal(false);  
    setOpenEditModal(false);  
    getData();  
  }; 

  const cancelAddModal = () => {  
    setOpenAddModal(false);  
  }; 

  const editPassword = contact => {  
    setSelectedPassword(contact);  
    setOpenEditModal(true);  
  }; 

  const cancelEditModal = () => {  
    setOpenEditModal(false);  
  }; 

  const getData = async () => {  
    const response = await getPasswords();  
    passwordsStore.setPasswords(response.data);  
    setInitialized(true);  
  }; 

  const deleteSelectedPassword = async id => {  
    await deletePassword(id);  
    setNotificationTitle("Password deleted");  
    getData();  
  }; 

  useEffect(() => {  
    if (!initialized) {  
      getData();  
    }  
  }); 

  return (  
    <div className="home-page">  
      <h1>Password Manager</h1>  
      <Modal show={openAddModal} onHide={closeModal}>  
        <Modal.Header closeButton>  
          <Modal.Title>Add Password</Modal.Title>  
        </Modal.Header>  
        <Modal.Body>  
          <PasswordForm  
            edit={false}  
            onSave={closeModal.bind(this)}  
            onCancelAdd={cancelAddModal}  
            passwordsStore={passwordsStore}  
          />  
        </Modal.Body>  
      </Modal> <Modal show={openEditModal} onHide={closeModal}>  
        <Modal.Header closeButton>  
          <Modal.Title>Edit Password</Modal.Title>  
        </Modal.Header>  
        <Modal.Body>  
          <PasswordForm  
            edit={true}  
            onSave={closeModal.bind(this)}  
            contact={selectedPassword}  
            onCancelEdit={cancelEditModal}  
            passwordsStore={passwordsStore}  
          />  
        </Modal.Body>  
      </Modal>  
      <ButtonToolbar onClick={openModal}>  
        <Button variant="outline-primary">Add Password</Button>  
      </ButtonToolbar>  
      <br />  
      <div className="table-responsive">  
        <Table striped bordered hover>  
          <thead>  
            <tr>  
              <th>Name</th>  
              <th>URL</th>  
              <th>Username</th>  
              <th>Password</th>  
              <th></th>  
              <th></th>  
              <th></th>  
              <th></th>  
            </tr>  
          </thead>  
          <tbody>  
            {passwordsStore.passwords.map(c => (  
              <tr key={c.id}>  
                <td>{c.name}</td>  
                <td>{c.url}</td>  
                <td>{c.username}</td>  
                <td>******</td>  
                <td>  
                  <CopyToClipboard text={c.username}>  
                    <Button  
                      variant="outline-primary"  
                      onClick={() => setNotificationTitle("Username copied")}  
                    >  
                      Copy Username to Clipboard  
                    </Button>  
                  </CopyToClipboard>  
                </td>  
                <td>  
                  <CopyToClipboard text={c.password}>  
                    <Button  
                      variant="outline-primary"  
                      onClick={() => setNotificationTitle("Password copied")}  
                    >  
                      Copy Password to Clipboard  
                    </Button>  
                  </CopyToClipboard>  
                </td>  
                <td>  
                  <Button  
                    variant="outline-primary"  
                    onClick={editPassword.bind(this, c)}  
                  >  
                    Edit  
                  </Button>  
                </td>  
                <td>  
                  <Button  
                    variant="outline-primary"  
                    onClick={deleteSelectedPassword.bind(this, c.id)}  
                  >  
                    Delete  
                  </Button>  
                </td>  
              </tr>  
            ))}  
          </tbody>  
        </Table>  
      </div> {notificationTitle ? (  
        <Notification  
          title={notificationTitle}  
          options={{  
            icon:  
              "http://mobilusoss.github.io/react-web-notification/example/Notifications_button_24.png"  
          }}  
          onClose={() => setNotificationTitle(undefined)}  
        />  
      ) : null}  
    </div>  
  );  
}  
export default observer(HomePage);

This component is the home page of our app. We have a table to display the list of passwords, a button to add a login and password entry, and buttons in each row of the table to copy username and password, and edit and delete each entry. We have the name, URL, username and password columns. The CopyToClipboard component allows us to copy the data we copy to the text prop of the component. Any component can be inside this component. We have one React Bootstrap modal for add password and another one for edit. PasswordForm is our form for adding the password entries, which we will create later.

We show the notifications whenever a username or password is copied, and when an entry is deleted. We do this by setting the notification title with the setNotificationTitle function. We add an onClose handler in the Notification component so that the notification will display again once it is closed.

We have the openModal , closeModal , cancelAddModal , and cancelEditModal functions to open and close the modals. In the editPassword function, we call the setSelectedPassword function to set the password entry to be edited.

The observer we wrap around the HomePage component is for letting us watch the latest values from passwordsStore.

Next, we modify index.js to have:

import React from "react";  
import ReactDOM from "react-dom";  
import "./index.css";  
import App from "./App";  
import * as serviceWorker from "./serviceWorker";  
import { PasswordsStore } from "./store";  
const passwordsStore = new PasswordsStore();

ReactDOM.render(  
  <App passwordsStore={passwordsStore} />,  
  document.getElementById("root")  
);

// If you want your app to work offline and load faster, you can change  
// unregister() to register() below. Note this comes with some pitfalls.  
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)  
serviceWorker.unregister();

We pass in our PasswordStore MobX store here, which will pass it to all the other components.

Next, we create PasswordForm.js and add:

import React from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import PropTypes from "prop-types";  
import { addPassword, getPasswords, editPassword } from "./requests";  
import Notification from "react-web-notification";

const schema = yup.object({  
  name: yup.string().required("Name is required"),  
  url: yup  
    .string()  
    .url()  
    .required("URL is required"),  
  username: yup.string().required("Username is required"),  
  password: yup.string().required("Password is required")  
});

function PasswordForm({  
  edit,  
  onSave,  
  contact,  
  onCancelAdd,  
  onCancelEdit,  
  passwordsStore  
}) {  
  const [notificationTitle, setNotificationTitle] = React.useState(""); 
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    if (!edit) {  
      await addPassword(evt);  
      setNotificationTitle("Password added");  
    } else {  
      await editPassword(evt);  
      setNotificationTitle("Password edited");  
    }  
    const response = await getPasswords();  
    passwordsStore.setPasswords(response.data);  
    onSave();  
  }; 

  return (  
    <>  
      <Formik  
        validationSchema={schema}  
        onSubmit={handleSubmit}  
        initialValues={contact || {}}  
      >  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="name">  
                <Form.Label>Name</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="name"  
                  placeholder="Name"  
                  value={values.name || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.name && errors.name}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.name}  
                </Form.Control.Feedback>  
              </Form.Group> <Form.Group as={Col} md="12" controlId="url">  
                <Form.Label>URL</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="url"  
                  placeholder="URL"  
                  value={values.url || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.url && errors.url}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.url}  
                </Form.Control.Feedback>  
              </Form.Group> <Form.Group as={Col} md="12" controlId="username">  
                <Form.Label>Username</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="username"  
                  placeholder="Username"  
                  value={values.username || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.username && errors.username}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.username}  
                </Form.Control.Feedback>  
              </Form.Group> <Form.Group as={Col} md="12" controlId="password">  
                <Form.Label>Password</Form.Label>  
                <Form.Control  
                  type="password"  
                  name="password"  
                  placeholder="Password"  
                  value={values.password || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.password && errors.password}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.password}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Save  
            </Button>  
            <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>  
              Cancel  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      {notificationTitle ? (  
        <Notification  
          title={notificationTitle}  
          options={{  
            icon:  
              "http://mobilusoss.github.io/react-web-notification/example/Notifications_button_24.png"  
          }}  
          onClose={() => setNotificationTitle(undefined)}  
        />  
      ) : null}  
    </>  
  );  
}

PasswordForm.propTypes = {  
  edit: PropTypes.bool,  
  onSave: PropTypes.func,  
  onCancelAdd: PropTypes.func,  
  onCancelEdit: PropTypes.func,  
  contact: PropTypes.object,  
  contactsStore: PropTypes.object  
};

export default PasswordForm;

Here, we add our form for letting users enter the username and password of their websites. We use the Yup schema object we created at the top of our code to make sure all fields are entered and check that the URL entered is actually a URL. We use the Formik component to handle the form of input changes and get the latest values.

Once the form is checked to be valid by schema.validate promise to resolve to true , then addPassword or editPassword functions from requests.js , which we will create later will be called depending if the user is adding or editing an entry. Once that succeeds, then the getPasswords from the same file is called, and then setPasswords from passwordsStore is called to store the passwords in the store. Finally, onSave passed in from the props in HomePage component is called to close the modal.

We show the notifications whenever an entry is added or edited, and when an entry is deleted. We do this by setting the notification title with the setNotificationTitle function. We add an onClose handler in the Notification component so that the notification will display again once it is closed.

Next, create requests.js and add:

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

export const getPasswords = () => axios.get(`${APIURL}/passwords`);
export const addPassword = (data) => axios.post(`${APIURL}/passwords`, data);
export const editPassword = (data) => axios.put(`${APIURL}/passwords/${data.id}`, data);
export const deletePassword = (id) => axios.delete(`${APIURL}/passwords/${id}`);

to let us make the requests to our back end to save the password entries.

Then we create our MobX store by creating store.js and add:

import { observable, action, decorate } from "mobx";

class PasswordsStore {  
  passwords = [];
  setPasswords(passwords) {  
    this.passwords = passwords;  
  }  
}

PasswordsStore = decorate(PasswordsStore, {  
  passwords: observable,  
  setPasswords: action  
});

export { PasswordsStore };

We have the passwords field which can be observed for the latest value if we wrap the observer function provided by MobX outside a component. The setPasswords is used to set the latest password entries in the store so that they can be propagated to the components.

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

<!DOCTYPE html>  
<html lang="en">  
  <head>  
    <meta charset="utf-8" />  
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />  
    <meta name="viewport" content="width=device-width, initial-scale=1" />  
    <meta name="theme-color" content="#000000" />  
    <meta  
      name="description"  
      content="Web site created using create-react-app"  
    />  
    <link rel="apple-touch-icon" href="logo192.png" />  
    <!--  
      manifest.json provides metadata used when your web app is installed on a  
      user's mobile device or desktop. See [https://developers.google.com/web/fundamentals/web-app-manifest/](https://developers.google.com/web/fundamentals/web-app-manifest/)  
    -->  
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />  
    <!--  
      Notice the use of %PUBLIC_URL% in the tags above.  
      It will be replaced with the URL of the `public` folder during the build.  
      Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will  
      work correctly both with client-side routing and a non-root public URL.  
      Learn how to configure a non-root public URL by running `npm run build`.  
    -->  
    <title>Password Manager</title>  
    <link  
      rel="stylesheet"  
      href="[https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css](https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css)"  
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"  
      crossorigin="anonymous"  
    />  
  </head>  
  <body>  
    <noscript>You need to enable JavaScript to run this app.</noscript>  
    <div id="root"></div>  
    <!--  
      This HTML file is a template.  
      If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file.  
      The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`.  
      To create a production bundle, use `npm run build` or `yarn build`.  
    -->  
  </body>  
</html>

to change the title and add the Bootstrap CSS.

Now we can run the app by running set PORT=3001 && react-scripts start on Windows or PORT=3006 react-scripts start on Linux.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{  
  "passwords": [  
  ]  
}

So we have the passwords endpoints defined in the requests.js available.