Categories
Vue Answers

How Check all checkboxes with Vue.js?

Sometimes, we want to check all checkboxes with Vue.js.

In this article, we’ll look at how to check all checkboxes with Vue.js.

Check All Checkboxes with Vue.js

To check all checkboxes with Vue.js, we can add a change event listener to the checkbox that checks all the other checkboxes.

For instance, we write:

<template>
  <div id="app">
    <table>
      <tr>
        <th>
          <input type="checkbox" v-model="allSelected" @change="selectAll" />
        </th>
        <th align="left">select all</th>
      </tr>
      <tr v-for="user of users" :key="user.id">
        <td>
          <input type="checkbox" v-model="selected" :value="user.id" number />
        </td>
        <td>{{ user.name }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      users: [
        { id: "1", name: "jane smith", email: "jane.smith@yahoo.com" },
        { id: "2", name: "john doe", email: "jdoe@yahoo.com" },
        { id: "3", name: "dave jones", email: "davejones@hotmail.com" },
        { id: "4", name: "alex smith", email: "alex@leannon.com" },
      ],
      selected: [],
      allSelected: false,
    };
  },
  methods: {
    async selectAll() {
      if (this.allSelected) {
        const selected = this.users.map((u) => u.id);
        this.selected = selected;
      } else {
        this.selected = [];
      }
    },
  },
};
</script>

We have the users array that’s rendered in a table.

In the template, we have a table row for the select all checkbox.

And below that, we use the v-for directive to render the checkboxes from the users data.

We set v-model to selected so we can set it to the values we want.

And we set the value prop of each checkbox to user.id so that we can put the user.id values for the users we want to select into the selected array.

We set the selectAll method as the change handler for the select all checkbox.

In selectAll, set check if this.allSelected is true.

If it’s true, then we set this.selected to an array with the id values from each this.users entry to select all the checkboxes.

Otherwise, we set this.selected to an empty array to deselect all checkboxes.

Conclusion

To check all checkboxes with Vue.js, we can add a change event listener to the checkbox that checks all the other checkboxes.

Categories
JavaScript Answers

How to Resize Then Crop an Image in the HTML5 Canvas with JavaScript?

Sometimes, we want to resize then crop an image in the HTML5 canvas with JavaScript.

In this article, we’ll look at how to resize then crop an image in the HTML5 canvas with JavaScript.

Resize Then Crop an Image in the HTML5 Canvas with JavaScript

To resize then crop an image in the HTML5 canvas with JavaScript, we can call drawImage with several arguments.

For instance, we can write:

<canvas style='width: 200px; height: 200px'></canvas>

to add a canvas element.

Then, we write:

const image = new Image()
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d');
image.src = 'https://picsum.photos/500';
image.onload = () => {
  ctx.drawImage(
    image,
    20, 20,
    50, 50,
    20, 20,
    100, 100
  );
}

We create an image with the Image constructor.

Then we select the canvas element with document.querySelector.

And then we call getContext to get the context.

Next, we set the src property of the image to the image’s URL.

Then we set the onload property to a function that calls drawImage with the image to draw it.

The 2rd and 3th arguments of drawImage are the x and y coordinates of the top left corner of the source image to clip.

The 4th and 5th arguments of drawImage is the width and height of the area of the source image to clip.

The 6th and 7th arguments of drawImage is the x and y coordinates of the source image on the canvas.

And the last 2 arguments are the width and height of the destination image on the canvas.

Now we should see the cropped and resized image displayed on the canvas.

Conclusion

To resize then crop an image in the HTML5 canvas with JavaScript, we can call drawImage with several arguments.

Categories
JavaScript Answers

How to Split an Array into Array Pairs in JavaScript?

Sometimes, we want to split an array into array pairs in JavaScript.

In this article, we’ll look at how to split an array into array pairs in JavaScript.

Split an Array into Array Pairs in JavaScript

To split an array into array pairs in JavaScript, we can use the JavaScript array’s reduce method.

For instance, we can write:

const initialArray = [2, 3, 4, 5, 6, 4, 3, 5, 5]
const newArr = initialArray.reduce((result, value, index, array) => {
  if (index % 2 === 0) {
    return [...result, array.slice(index, index + 2)];
  }
  return result
}, []);
console.log(newArr)

We have the initialArray that we want to split into chunks of 2.

To do this, we call reduce with a callback that returns the result array that’s created from the result array spread into a new array and an array with the next 2 items in the list if index is event.

We get the next 2 items with the slice with index and index + 2 as the indexes.

Otherwise, we just return result.

The 2nd argument is an empty array so result is always an array.

As a result, newArr is:

[
  [
    2,
    3
  ],
  [
    4,
    5
  ],
  [
    6,
    4
  ],
  [
    3,
    5
  ],
  [
    5
  ]
]

Conclusion

To split an array into array pairs in JavaScript, we can use the JavaScript array’s reduce method.

Categories
JavaScript Basics

Use Modules to Build a Modular JavaScript App

One of the big features of ES6 is JavaScript supporting built-in modules. Modules allow us to share code between files by using the export and import syntax. This is a big improvement over using script tags and global variables to share code across files.

Using script tags was error prone since the load order matters. Having scripts in the wrong order could cause our program to execute code that hasn’t been declared yet. It also forced us to write spaghetti code with no real structure or logical composition. This problem doesn’t exist with modules because everything is exported and imported directly between files. Also, we can know the definition of the imported code easily since it’s explicit in which modules is being imported and referenced.

Exports and Imports

To make code in a JavaScript file importable, we have to export them explicitly with the export statement. To do this, we just put export in front of the variable or constant you want to expose to other files. For example, we can write:

export let num = 1;

This exports the num variable so that other modules can import it and use it.

We can export anything declared with var, let, const, and also functions and classes. The items exported must be declared at the top level. export cannot be used anywhere else, like inside functions and classes.

We can export multiple members at once. All we have to do is wrap all the members in curly brackets separated by commas. For example, we can write:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

This will let us import num1, num2, and num3 in other JavaScript files.

Now that we have exported the members, we can import them in other JavaScript files. We can use the import statement to import one or more members into a module and work with them. For example, if we have the following in moduleA.js:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

Then in moduleB.js we can write the following code to import the items from moduleA.js:

import {num1, num2, num3} from './moduleA'

The path after the from keyword starts with a period. The period means that we’re in the current folder.

This assumes that moduleA.js and moduleB.js are in the same folder. If we have them in different folder, then we have the specify the path of moduleA.js relative to moduleB.js if we want to import the exported members of moduleA.js into moduleB.js. For example, if moduleA.js is one level above moduleB.js, then in moduleB.js we write:

import {num1, num2, num3} from '../moduleAFolder/moduleA'

The 2 periods before the path means that we go up one folder level and then get the moduleAFolder and then get moduleA.js.

We can also use JavaScript modules in script tags. To do this, we must set the type attribute of the script tag to module to use them. For example, if we want to use moduleA.js in our HTML file, we can write:

<script type='module' src='moduleA.js'></script>

We can use import and export in JavaScript modules. They won’t work with regular scripts.

Scripts run in strict mode automatically, so we can’t accidentally declare global variables and do other things that can be done without strict mode being enabled. They also load asynchronously automatically so that we won’t have to worry about long scripts holding up a page from loading. Also, import and export only happens between 2 scripts, so no global variables are set. Therefore, they can’t be viewed in the console directly.

Default Exports

There’s also a default export option for exporting a module’s members. We previously exported the variable in a way where we import them by the name. There’s also the default export which exports a single member from a module without needing to reference it explicitly by name when you’re importing it. For example, if we have a single member in a module that we want to export, we can write the following in moduleA.js:

const num = 1;
export default num;

There are no curly braces when you use export default .

Then in the file where you want to import the member. In moduleB.js we write:

import num from './moduleA'

Once again, we omit the curly braces. This is because only one default export is allowed per module. Alternatively, we can write the following in moduleB.js :

import {default as num} from './moduleA'

Renaming Imports and Exports

If we have many modules and their exported members have the same name, then there will be conflicts if we try to import multiple modules. This is will be a problem that we need to fix. Fortunately, JavaScript has the as keyword to let us rename exports and imports so we can avoid name conflicts in our code. To use the as keyword to rename exports, we write the following in moduleA.js:

export {
  num1 as numOne,
  num2 as numTwo
}

Then in moduleB.js, we can import them by writing:

import { numOne, numTwo } from './`moduleA'`

Alternatively, we can do the renaming when we import instead. To do this, in moduleA.js, we put:

export {
  num1,
  num2
}

Then in moduleB.js, we put:

import { num1 as numOne, num2 as numTwo } from './`moduleA'`

If we try to import members with modules where the members have the same name, like:

`import {` num1, num2 `} from './moduleA';
import {` num1, num2 `} from './moduleB';
import {` num1, num2 `} from './moduleC';`

We will see that we get SyntaxError. So we have to rename them so that the module will run:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

A cleaner way to import from multiple modules that have members with the same names is to import all of the module’s exported members as one object. We can do that with an asterisk. For example, instead of:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

We can instead write:

import * as moduleA from './moduleA';
import * as moduleB from './moduleB';
import * as moduleC from './moduleC';

Then in code below the imports, we can write:

moduleA.num1;
moduleA.num2;
moduleB.num1;
moduleB.num2;
moduleC.num1;
moduleC.num2;

We can also export and import classes. So if we have a file that contains one or more classes, like the Person.js file with the class below:

class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
  get firstName() {
    return this._firstName
  }
  get lastName() {
    return this._lastName
  }
  sayHi() {
    return `Hi, ${this.firstName} ${this.lastName}`
  }
  set firstName(firstName) {
    this._firstName = firstName;
  }
  set lastName(lastName) {
    this._lastName = lastName;
  }
}

Then we write the following to export a class:

export { Person };

This exports the Person class, and then to import it, we write:

import { Person } from './person';

Dynamic Module Loading

JavaScript modules can be loaded dynamically. This lets us only load modules when they’re needed rather than loading all of them when the app runs. To do this, we use the import() function, which returns a promise. When the module in the argument is loaded, then the promise is fulfilled. The promise resolves to a module object, which can then be used by the app’s code. If we have the Person class in Person.js, then we can dynamically import it with the following code:

import('./Person')
.then((module)=>{
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
})

Or using the async and await syntax, we can put that in a function:

const importPerson = async ()=>{
  const module = await import('./Person');
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
}

importPerson();

As you can see, JavaScript modules are very useful for organizing code. It allows us to export things that we want to expose to other modules, eliminating the need for global variables. In addition, exports and imports can be renamed to avoid conflict when importing multiple modules. Also, all the exported members can be imported all at once so that we get the whole module as an object instead of importing individual members. Finally, we can use export default if we only want to export one thing from our module.

Categories
React

How to Add Form Validation to Dynamic Forms

Formik and Yup can help to create form validation in React.

Dynamic forms are forms that are derived from some dynamic data. They are not hardcoded into the code. Usually, they are displayed by looping through some data and render forms from the data.

With Yup it is easy to loop through fields of an object and then add them to your validation schema, allowing you to validate form that does not have fixed fields.

An example Yup schema can be built like this:

let schemaObj = {
  to: yup.string().required("This field is required"),
  from: yup.string().required("This field is required"),
  subject: yup.string().required("This field is required")
};

fields.forEach(f => {
  schemaObj[f] = yup.string().required("This field is required");
});

let newSchema = yup.object(schemaObj);

We loop through fields to get the placeholders and add them to the schema object. That is all you need to add dynamic fields for validation with Yup.

In this article, we will build an app that lets users enter email templates. Then they can use the templates to send emails to different email addresses with the SendGrid API. Our app will consist of a back end and a front end. The front end will be built with React, and the back end will be built with Express.

SendGrid is a great service made by Twilio for sending emails. Rather than setting up your own email server for sending an email with your apps, we use SendGrid to do the hard work for us. It also decreases the chance of email ending up in spam since it is a known trustworthy service.

It also has very easy to use libraries for various platforms for sending emails. Node.js is one of the platforms that are supported.

To send emails with SendGrid, install the SendGrid SDK package by running npm i @sendgrid/mail . Then in your code, add const sgMail = require(‘@sendgrid/mail’); to import the installed package.

Then in your code, you send an email by:

sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
  to: email,
  from: 'email@example.com',
  subject: 'Example Email',
  text: `
    Dear user,    Here is your email.
  `,
  html: `
    <p>Dear user,</p>    <p>Here is your email.</p>
  `,
};
sgMail.send(msg);

where process.env.SENDGRID_API_KEY is the SendGrid’s API, which should be stored as an environment variable since it is a secret.

Back End

To start, we will make a project folder and within it, add a backend folder inside the project folder. We will use the Express Generator to generate the code for our project. To do this, run npx express-generator inside the backend folder. Then run npm i to install the packages listed in package.json .

Next, we install our own packages. We will use Sequelize as our ORM, Babel for using the latest JavaScript features, Dotenv for storing environment variables, SendGrid Nodejs for sending emails, CORS for enabling cross-domain requests with front end and SQLite3 for the database.

To install them run npm i @babel/cli @babel/core @babel/node @babel/preset-env @sendgrid/mail cors sendgrid-nodejs sequelize sqlite3 .

With those installed, we can start building the back end. First, we add .babelrc to enable Babel in our app to run the app with the latest JavaScript interpreter. To do this, add .babelrc to the backend folder and add:

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

Then in package.json , replace the existing code with the following in the scripts section:

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

This lets us run with the latest Babel Node runtime instead of the regular Node runtime so we can use the latest JavaScript features like import

Next, run Sequelize CLI to create the database boilerplate code and migration for creating the database. Run npx sequelize-cli init in the backend folder and you will get config.json . In config.json , change the existing code to:

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

Then we create our data migration. Run:

npx sequelize-cli model:create --name EmailTemplate --attributes name:string,type:string,template:text,subject,previewText:string

to create the EmailTemplates table in an SQLite database. The above command should also create the corresponding model for this table.

Next run npx sequelize-cli db:migrate to create the database.

Now we can move on to creating our routes. Create a file called email.js in the routes folder and add:

var express = require("express");
const models = require("../models");
const sgMail = require("@sendgrid/mail");
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
var router = express.Router();

router.get("/templates", async (req, res, next) => {
  const templates = await models.EmailTemplate.findAll();
  res.json(templates);
});

router.get("/template/:id", async (req, res, next) => {
  const id = req.params.id;
  const templates = await models.EmailTemplate.findAll({ where: { id } });
  res.json(templates[0]);
});

router.post("/template", async (req, res, next) => {
  try {
    const template = await models.EmailTemplate.create(req.body);
    res.json(template);
  } catch (ex) {
    res.json(ex);
  }
});

router.put("/template/:id", async (req, res, next) => {
  try {
    const id = req.params.id;
    const { name, description, template, subject } = req.body;
    const temp = await models.EmailTemplate.update(
      {
        name,
        description,
        template,
        subject
      },
      {
        where: { id }
      }
    );
    res.json(temp);
  } catch (ex) {
    res.json(ex);
  }
});

router.delete("/template/:id", async (req, res, next) => {
  try {
    const id = req.params.id;
    await models.EmailTemplate.destroy({ where: { id } });
    res.json({});
  } catch (ex) {
    res.json(ex);
  }
});

router.post("/send", async (req, res, next) => {
  try {
    const { template, variables, to, subject, from } = req.body;
    let html = template;
    Object.keys(variables).forEach(variable => {
      html = html.replace(`[[${variable}]]`, variables[variable]);
    });
    const msg = {
      to,
      from,
      subject,
      html
    };
    sgMail.send(msg);
    res.json({});
  } catch (ex) {
    res.json(ex);
  }
});

module.exports = router;

These are all the routes for saving our templates and send email with the template. The GET templates route get all the templates we saved. The templates/:id route gets the template by ID. We use findAll with where statement id = {id} to get the results and only get the first one to get by ID.

The POST template route create our template with the create function. The PUR template route uses the update function to update the entry found by looking up by ID. The second argument has our select condition. The DELETE template route deletes by looking up the entry by ID to delete with the destroy function.

The send route calls the SendGrid API to send the email with the variables declared in the email template filled in with the values set by the user. The request body has the variables field to send the variables with the values, where the key is the variable name and the value has the value.

Sequelize provides the create , findAll , update and destroy functions as part of the model.

In app.js , we replace the existing code with:

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

const indexRouter = require("./routes/index");
const emailRouter = require("./routes/email");

const app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());

app.use("/", indexRouter);
app.use("/email", emailRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

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

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

We enabled CORS by adding:

app.use(cors());

And we added our routes by adding:

const emailRouter = require("./routes/email");
app.use("/email", emailRouter);

This finishes the back end of our email app.

Front End

Next, we move on to the front end. We start a new React project in the project folder by running npx create-react-app frontend .

Then we need to install some packages. We need Bootstrap for styling, MobX for state management, Axios for making HTTP requests, Formik and Yup for form value handling and form validation respectively, and React Router for routing URLs to our pages.

To install the packages, run npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup .

After all the packages are installed, we can start building the app. First we replace the existing code of App.js with our code:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import TopBar from "./TopBar";
import EmailPage from "./EmailPage";
import { EmailTemplateStore } from "./store";
const history = createHistory();
const emailTemplateStore = new EmailTemplateStore();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} emailTemplateStore={emailTemplateStore} />
          )}
        />
        <Route
          path="/email/:id"
          exact
          component={props => (
            <EmailPage {...props} emailTemplateStore={emailTemplateStore} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

This is the entry component of our app and it contains the routes that we will add. The routes have the emailTemplateStore , which is a MobX store that we will create.

In App.css , replace the existing code with:

.page {
  padding: 20px;
}

to add some padding to our pages.

Next, we create a form for adding and editing our email templates. Create a file called EmailForm.js in the src folder and add:

import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik } from "formik";
import { addTemplate, getTemplates, editTemplate } from "./request";

const schema = yup.object({
  name: yup.string().required("Name is required"),
  template: yup.string().required("Template is required"),
  subject: yup.string().required("Subject is required")
});

function EmailForm({ emailTemplateStore, edit, onSave, template }) {
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    if (!edit) {
      await addTemplate(evt);
    } else {
      await editTemplate(evt);
    }
    getAllTemplates();
  };

  const getAllTemplates = async () => {
    const response = await getTemplates();
    emailTemplateStore.setTemplates(response.data);
    onSave();
  };

  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? template : {}}
      >
        {({
          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="description">
                <Form.Label>Description</Form.Label>
                <Form.Control
                  type="text"
                  name="description"
                  placeholder="Description"
                  value={values.description || ""}
                  onChange={handleChange}
                  isInvalid={touched.description && errors.description}
                />

              <Form.Control.Feedback type="invalid">
                  {errors.description}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="subject">
                <Form.Label>Subject</Form.Label>
                <Form.Control
                  type="text"
                  name="subject"
                  placeholder="Subject"
                  value={values.subject || ""}
                  onChange={handleChange}
                  isInvalid={touched.subject && errors.subject}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.subject}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="template">
                <Form.Label>
                  Template - Enter Template in HTML, Put Variables Between
                  Double Brackets.
                </Form.Label>
                <Form.Control
                  as="textarea"
                  rows="20"
                  name="template"
                  placeholder="Template"
                  value={values.template || ""}
                  onChange={handleChange}
                  isInvalid={touched.template && errors.template}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.template}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}

export default observer(EmailForm);

This is the form in which we enter the email template. We have the name and template field as required fields and descriptions as an optional field. We use Formik to automatically update the form values and populate them in the evt parameter of the handleSubmit function. This saves us a lot of work by eliminating the need for writingonChange handlers for each field ourselves.

For form validation, we define the schema object made with yup and pass it into the Formik component. The form validation happens automatically and errors are displayed as soon as invalid values are entered in the Form.Control.Feedback component.

The Form component is provided by React Boostrap. The edit prop will tell if we set the initialialValues in the Formik component. We only set it to the template prop if we edit is true because only then we have something to edit.

The handleSubmit function is called when the form is submitted, it has the values of all the form fields. We call schema.validate to validate the form values against the schema before submitting it. If they’re valid, then we call addTemplate or editTemplate depending if you want to add or edit the template. The edit will tell them apart. If it’s successful, we call getAllTemplates to get all the templates and put them in our store.

We wrap observer outside the EmailForm component to get the latest values from our MobX store as soon as it’s updated.

Next, we add a page for sending emails. Add a file called EmailPage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { getTemplate, sendEmail } from "./request";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Formik } from "formik";

function EmailPage({ match: { params } }) {
  const [template, setTemplate] = useState({});
  const [schema, setSchema] = useState(yup.object({}));
  const [variables, setVariables] = useState([]);
  const [initialized, setInitialized] = useState(false);

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    let data = { variables: {} };
    data.template = evt.template;
    variables.forEach(v => {
      const variable = v.replace("[[", "").replace("]]", "");
      data.variables[variable] = evt[variable];
    });
    data.to = evt.to;
    data.from = evt.from;
    data.subject = evt.subject;
    await sendEmail(data);
    alert("Email sent");
  };

  const getSingleTemplate = async () => {
    const response = await getTemplate(params.id);
    setTemplate(response.data);
    const placeholders = response.data.template.match(/[[(.*?)]]/g);
    setVariables(placeholders);
    let schemaObj = {
      to: yup.string().required("This field is required"),
      from: yup.string().required("This field is required"),
      subject: yup.string().required("This field is required")
    };
    placeholders.forEach(p => {
      p = p.replace("[[", "").replace("]]", "");
      schemaObj[p] = yup.string().required("This field is required");
    });
    let newSchema = yup.object(schemaObj);
    setSchema(newSchema);
    setInitialized(true);
  };

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

  return (
    <div className="page">
      <h1 className="text-center">Send Email</h1>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        enableReinitialize={true}
        initialValues={template}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            {variables.map((v, i) => {
              const variable = v.replace("[[", "").replace("]]", "");
              return (
                <Form.Row key={i}>
                  <Form.Group as={Col} md="12" controlId="name">
                    <Form.Label>Variable - {variable}</Form.Label>
                    <Form.Control
                      type="text"
                      name={variable}
                      value={values[variable] || ""}
                      onChange={handleChange}
                      isInvalid={touched[variable] && errors[variable]}
                    />
                    <Form.Control.Feedback type="invalid">
                      {errors[variable]}
                    </Form.Control.Feedback>
                  </Form.Group>
                </Form.Row>
              );
            })}

            <Form.Row>
              <Form.Group as={Col} md="12" controlId="from">
                <Form.Label>From Email</Form.Label>
                <Form.Control
                  type="text"
                  name="from"
                  placeholder="From Email"
                  value={values.from || ""}
                  onChange={handleChange}
                  isInvalid={touched.from && errors.from}
                />

               <Form.Control.Feedback type="invalid">
                  {errors.from}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>

            <Form.Row>
              <Form.Group as={Col} md="12" controlId="to">
                <Form.Label>To Email</Form.Label>
                <Form.Control
                  type="text"
                  name="to"
                  placeholder="To Email"
                  value={values.to || ""}
                  onChange={handleChange}
                  isInvalid={touched.to && errors.to}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.to}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>

            <Form.Row>
              <Form.Group as={Col} md="12" controlId="subject">
                <Form.Label>Subject</Form.Label>
                <Form.Control
                  type="text"
                  name="subject"
                  placeholder="Subject"
                  value={values.subject || ""}
                  onChange={handleChange}
                  isInvalid={touched.subject && errors.subject}
                />

               <Form.Control.Feedback type="invalid">
                  {errors.subject}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>

            <Form.Row>
              <Form.Group as={Col} md="12" controlId="template">
                <Form.Label>Template</Form.Label>
                <Form.Control
                  as="textarea"
                  rows="20"
                  name="template"
                  placeholder="Template"
                  value={values.template || ""}
                  onChange={handleChange}
                  isInvalid={touched.template && errors.template}
                  readOnly
                />

               <Form.Control.Feedback type="invalid">
                  {errors.template}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Send
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}

export default withRouter(EmailPage);

In this component, we get the email template by ID and we extract the variables from the template text in the getSingleTemplate function. We also create a validate schema with Yup with:

let schemaObj = {
  to: yup.string().required("This field is required"),
  from: yup.string().required("This field is required"),
  subject: yup.string().required("This field is required")
};
placeholders.forEach(p => {
  p = p.replace("[[", "").replace("]]", "");
  schemaObj[p] = yup.string().required("This field is required");
});
let newSchema = yup.object(schemaObj);

To build the Yup schema, we first add the static fields to , from and subject for validation. Then we loop through the keys of the variable field of the response.data object and build the Yup form validation schema dynamically by removing the brackets then using each placeholder entry as a key.

In the Formik component, we put the prop:

enableReinitialize={true}

so that we can populate the template text at the form field with name prop template .

In the Form component, we have:

{variables.map((v, i) => {
   const variable = v.replace("[[", "").replace("]]", "");
      return (
        <Form.Row key={i}>
          <Form.Group as={Col} md="12" controlId="name">
            <Form.Label>Variable - {variable}</Form.Label>
            <Form.Control
              type="text"
              name={variable}
              value={values[variable] || ""}
              onChange={handleChange}
              isInvalid={touched[variable] && errors[variable]}
          />
          <Form.Control.Feedback type="invalid">
              {errors[variable]}
          </Form.Control.Feedback>
        </Form.Group>
    </Form.Row>
    );
})}

to dynamically loop through the variables that we set in the getSingleTemplate function and render the form field with the form validation message. We set the name prop to the variable so that the right message will be displayed. A variable in the same as a placeholder with the brackets removed.

We need to wrap the withRouter function outside EmailPage so that we can get the match prop so that we get the ID of the email template from the URL.

Next, we build the home page, which will have a table for displaying the list of templates saved and have buttons in each table row for letting users send emails with the template or delete or edit the template. There will also be a button to let the user add an email template.

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import EmailForm from "./EmailForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getTemplates, deleteTemplate } from "./request";

function HomePage({ emailTemplateStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [template, setTemplate] = useState([]);

  const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };

  const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };

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

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

  const getAllTemplates = async () => {
    const response = await getTemplates();
    emailTemplateStore.setTemplates(response.data);
    setInitialized(true);
  };

  const editTemplate = template => {
    setTemplate(template);
    setOpenEditModal(true);
  };

  const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };

  const deleteSelectedTemplate = async id => {
    await deleteTemplate(id);
    getAllTemplates();
  };

  const sendEmail = template => {
    history.push(`/email/${template.id}`);
  };

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

  return (
    <div className="page">
      <h1 className="text-center">Templates</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Template</Button>
      </ButtonToolbar>

      <Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Template</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <EmailForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            emailTemplateStore={emailTemplateStore}
          />
        </Modal.Body>
      </Modal>

      <Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Template</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <EmailForm
            edit={true}
            template={template}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            emailTemplateStore={emailTemplateStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Subject</th>
            <th>Send Email</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {emailTemplateStore.templates.map(t => (
            <tr key={t.id}>
              <td>{t.name}</td>
              <td>{t.description}</td>
              <td>{t.subject}</td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={sendEmail.bind(this, t)}
                >
                  Send Email
                </Button>
              </td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={editTemplate.bind(this, t)}
                >
                  Edit
                </Button>
              </td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={deleteSelectedTemplate.bind(this, t.id)}
                >
                  Delete
                </Button>
              </td>
            </tr>
          ))}
        </tbody>
      </Table>
    </div>
  );
}

export default withRouter(observer(HomePage));

We have the Table provided by React Boostrap. Again, we wrap the withRouter function outside the HomePage component at the bottom so that we get the history object in our props, which let us call history.push to redirect to our email page.

We have the openAddTemplateModal, closeAddModal, cancelAddModal, cancelEditModal , and onSave function to open or close the add or edit modals. onSave is used by EmailForm component by passing it in as a prop to EmailForm . We have a getAllTemplates function to get the templates we saved. We use the useEffect ‘s callback function to get the templates on the first load by checking for the initialized variable, if it’s false then getAllTemplates load and set initialized to false so it won’t load again.

The Modal component is provided by React Bootstrap. In both the add and edit modals, we use the same EmailForm for saving templates. We can tell whether the user is adding or editing by using the edit prop.

Next, create a file called requests.js and add:

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

export const getTemplates = () => axios.get(`${APIURL}/email/templates`);

export const getTemplate = id => axios.get(`${APIURL}/email/template/${id}`);

export const addTemplate = data => axios.post(`${APIURL}/email/template`, data);

export const editTemplate = data =>
  axios.put(`${APIURL}/email/template/${data.id}`, data);

export const deleteTemplate = id =>
  axios.delete(`${APIURL}/email/template/${id}`);

export const sendEmail = data => axios.post(`${APIURL}/email/send`, data);

to let users make HTTP requests to our back end.

Then we create store.js in the src and put:

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

class EmailTemplateStore {
  templates = [];

setTemplates(templates) {
    this.templates = templates;
  }
}

EmailTemplateStore = decorate(EmailTemplateStore, {
  templates: observable,
  setTemplates: action
});

export { EmailTemplateStore };

to let us store the templates array in a central localization for easy access by all components. We pass an instance of this in our components via the emailTemplateStore prop of the components to call the setTemplates function to set the templates and access the templates by using emailTemplateStore.templates . That’s why we wrap observable function around our components. We need the latest values as they are updated here. Also, we designated templates as observable in the decorate function to allow templates field to have the latest value whenever it is accessed. setTemplates is designated as action so that we can call it to manipulate the store.

Next, create TopBar.js and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";

function TopBar({ location }) {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Email App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

to create a top bar by using the Navbar component provided by React Boostrap. We check the pathname to highlight the right links by setting the active prop.

Finally, in index.html , 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/
    -->
    <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>Email App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.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 add the Bootstrap CSS and change the title of the app.

Once all that is done go into the backend folder and run npm start to start the back end and go into the frontend folder and run the same command. Answer yes if you’re asked whether you want to run the app from a different port.