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.

Categories
React

How to Add a Date Picker to a React App

We use the react-big-calendar library in React.

For many applications, recording dates is an important feature. Having a calendar is often a handy feature to have. Fortunately, many developers have made calendar components that other developers can easily add to their apps.

Date pickers make the user experience of recording dates better by letting them pick the date from a calendar instead of typing in the date in the right format themselves. It brings a lot more convenience to the user.

In this article, we will make a simple calendar app where users can drag over a date range and add a calendar entry. Users can also click on an existing calendar entry and edit the entry. Existing entries can also be deleted. The form for adding and editing the calendar entry will have a date and time pickers to select the date and time.

React has many calendar widgets that we can add to our apps. One of them is React Big Calendar. It has a lot of features. It has a month, week, and daily calendar. Also, you can navigate easily to today or any other days with back and next buttons. You can also drag over a date range in the calendar to select the date range. With that, you can do any manipulation you want with the dates.

We will save the data on the back end in a JSON file.

We will use React to build our app. To start, we run:

npx create-react-app calendar-app

to create the project.

Next, we have to install a few packages. We will use Axios for HTTP requests to our back end, Bootstrap for styling, MobX for simple state management, React Big Calendar for our calendar component, React Datepicker for the date and time picker in our form, and React Router for routing.

To install them, we run:

npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom

With all the packages installed, we can start writing the code. First, we replace the existing code in App.js 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";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "react-datepicker/dist/react-datepicker.css";
const history = createHistory();

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

export default App;

We add the React Bootstrap top bar in here with a link to the home page. Also, we add the route for the home page in here with the MobX calendarStore passed in.

Also, we import the styles for the date picker and calendar here so that we can use them throughout the app.

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

.page {
  padding: 20px;
}

.form-control.react-datepicker-ignore-onclickoutside,
.react-datepicker-wrapper {
  width: 465px !important;
}

.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header,
.react-datepicker__day-name,
.react-datepicker__day,
[class^="react-datepicker__day--*"],
.react-datepicker__time-list-item {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
    "Droid Sans", "Helvetica Neue", sans-serif;
}

to add some padding to our page, change the width of the date picker input and change the font of the date picker.

Next, create a file called CalendarForm.js in the src folder and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import DatePicker from "react-datepicker";
import Button from "react-bootstrap/Button";
import {
  addCalendar,
  editCalendar,
  getCalendar,
  deleteCalendar
} from "./requests";
import { observer } from "mobx-react";

const buttonStyle = { marginRight: 10 };

function CalendarForm({ calendarStore, calendarEvent, onCancel, edit }) {
  const [start, setStart] = React.useState(null);
  const [end, setEnd] = React.useState(null);
  const [title, setTitle] = React.useState("");
  const [id, setId] = React.useState(null);

  React.useEffect(() => {
    setTitle(calendarEvent.title);
    setStart(calendarEvent.start);
    setEnd(calendarEvent.end);
    setId(calendarEvent.id);
  }, [
    calendarEvent.title,
    calendarEvent.start,
    calendarEvent.end,
    calendarEvent.id
  ]);

  const handleSubmit = async ev => {
    ev.preventDefault();
    if (!title || !start || !end) {
      return;
    }

    if (+start > +end) {
      alert("Start date must be earlier than end date");
      return;
    }
    const data = { id, title, start, end };
    if (!edit) {
      await addCalendar(data);
    } else {
      await editCalendar(data);
    }
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
  const handleStartChange = date => setStart(date);
  const handleEndChange = date => setEnd(date);
  const handleTitleChange = ev => setTitle(ev.target.value);

  const deleteCalendarEvent = async () => {
    await deleteCalendar(calendarEvent.id);
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };

  return (
    <Form noValidate onSubmit={handleSubmit}>
      <Form.Row>
        <Form.Group as={Col} md="12" controlId="title">
          <Form.Label>Title</Form.Label>
          <Form.Control
            type="text"
            name="title"
            placeholder="Title"
            value={title || ""}
            onChange={handleTitleChange}
            isInvalid={!title}
          />
          <Form.Control.Feedback type="invalid">{!title}  </Form.Control.Feedback>
        </Form.Group>
      </Form.Row>

      <Form.Row>
        <Form.Group as={Col} md="12" controlId="start">
          <Form.Label>Start</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={start}
            onChange={handleStartChange}
          />
        </Form.Group>
      </Form.Row>

      <Form.Row>
        <Form.Group as={Col} md="12" controlId="end">
          <Form.Label>End</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={end}
            onChange={handleEndChange}
          />
        </Form.Group>
      </Form.Row>
      <Button type="submit" style={buttonStyle}>
        Save
      </Button>
      <Button type="button" style={buttonStyle} onClick={deleteCalendarEvent}>
        Delete
      </Button>
      <Button type="button" onClick={onCancel}>
        Cancel
      </Button>
    </Form>
  );
}

export default observer(CalendarForm);

This is the form for adding and editing the calendar entries. We add the React Bootstrap form here by adding the Form component. The Form.Control is also from the same library. We use it for the title text input.

The other 2 fields are the start and end dates. We use React Datepicker in here to let users select the start and end dates of a calendar entry. In addition, we enable the time picker to let users pick the time.

There are change handlers in each field to update the values in the state so users can see what they entered and let them submit the data later. The change handlers are handleStartChange , handleEndChange and handleTitleChange . We set the states with the setter functions generated by the useState hooks.

We use the useEffect callback to set the fields in the calendarEvent prop to the states. We pass all the fields we want to set to the array in the second argument of the useEffect function so that the states will be updated whenever the latest value of the calendarEvent prop is passed in.

In the handleSubmit function, which is called when the form Save button is clicked. we have to call ev.preventDefault so that we can use Ajax to submit our form data.

If data validation passes, then we submit the data and get the latest and store them in our calendarStore MobX store.

We wrap observer outside the CalendarForm component so that we always get the latest values from calendarStore .

Next, we create our home page. Create a HomePage.js file in the src folder and add:

import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import Modal from "react-bootstrap/Modal";
import CalendarForm from "./CalendarForm";
import { observer } from "mobx-react";
import { getCalendar } from "./requests";

const localizer = momentLocalizer(moment);

function HomePage({ calendarStore }) {
  const [showAddModal, setShowAddModal] = React.useState(false);
  const [showEditModal, setShowEditModal] = React.useState(false);
  const [calendarEvent, setCalendarEvent] = React.useState({});
  const [initialized, setInitialized] = React.useState(false);

  const hideModals = () => {
    setShowAddModal(false);
    setShowEditModal(false);
  };

  const getCalendarEvents = async () => {
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    setInitialized(true);
  };

  const handleSelect = (event, e) => {
    const { start, end } = event;
    const data = { title: "", start, end, allDay: false };
    setShowAddModal(true);
    setShowEditModal(false);
    setCalendarEvent(data);
  };

  const handleSelectEvent = (event, e) => {
    setShowAddModal(false);
    setShowEditModal(true);
    let { id, title, start, end, allDay } = event;
    start = new Date(start);
    end = new Date(end);
    const data = { id, title, start, end, allDay };
    setCalendarEvent(data);
  };

  React.useEffect(() => {
    if (!initialized) {
      getCalendarEvents();
    }
  });

  return (
    <div className="page">
      <Modal show={showAddModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Add Calendar Event</Modal.Title>
        </Modal.Header>

        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={false}
          />
        </Modal.Body>
      </Modal>

      <Modal show={showEditModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Calendar Event</Modal.Title>
        </Modal.Header>

        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={true}
          />
        </Modal.Body>
      </Modal>
      <Calendar
        localizer={localizer}
        events={calendarStore.calendarEvents}
        startAccessor="start"
        endAccessor="end"
        selectable={true}
        style={{ height: "70vh" }}
        onSelectSlot={handleSelect}
        onSelectEvent={handleSelectEvent}
      />
    </div>
  );
}

export default observer(HomePage);

We get the calendar entries and populate them in the calendar here. The entries are retrieved from the back end and then saved into the store. In the useEffect callback, we set to get the items when the page loads. We only do it when initialized is false so we won’t be reloading the data every time the page renders.

To open the modal for adding calendar entries, we set the onSelectSlot prop with our handler so that we can call setShowAddModal and setCalendarEvent to set open the modal and set the dates before opening the add calendar event modal.

Similarly, we set the onSelectEvent modal with the handleSelectEvent handler function so that we open the edit modal and set the calendar event data of the existing entry.

Each Modal have the CalendarForm component inside. We pass in the function for closing the modals into the form so that we can close them from the form. Also, we pass in the calendarStore and calendarEvent so that they can be manipulated in the CalendarForm.

We wrap observer outside the CalendarForm component so that we always get the latest values from calendarStore .

Next in index.js , we replace the existing code with:

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

ReactDOM.render(
  <App calendarStore={calendarStore} />,
  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();

so that we can pass in the MobX calendarStore into the root App component.

Next, create a requests.js file in the src folder and add:

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

export const getCalendar = () => axios.get(`${APIURL}/calendar`);

export const addCalendar = data => axios.post(`${APIURL}/calendar`, data);

export const editCalendar = data =>
  axios.put(`${APIURL}/calendar/${data.id}`, data);

export const deleteCalendar = id => axios.delete(`${APIURL}/calendar/${id}`);

These are the functions for making the HTTP calls to manipulate the calendar entries.

Next createstore.js in the src folder and add:

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

class CalendarStore {
  calendarEvents = [];

setCalendarEvents(calendarEvents) {
    this.calendarEvents = calendarEvents;
  }
}

CalendarStore = decorate(CalendarStore, {
  calendarEvents: observable,
  setCalendarEvents: action
});

export { CalendarStore };

to save the items in the store for access by all of our components.

Next 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/](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>Calendar App</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 add the Bootstrap CSS and rename the title.

Now all the hard work is done. All we have to do is use the JSON Server NPM package located at https://github.com/typicode/json-server for our back end.

Install it by running:

npm i -g json-server

Then run it by running:

json-server --watch db.json

In db.json , replace the existing content with:

{
  "calendar": []
}

Next we run our app by running npm start in our app’s project folder and when the program asks you to run in a different port, select yes.

Categories
React Answers

How to Add Scroll to Top Feature in Your Vue.js App

If a page has a long list, then it is convenient for users if the page has an element that scrolls to somewhere on the page with one click. In plain JavaScript, there is the window.scrollTo and element.scrollTo functions which take the x, y coordinate of the screen as parameters, which isn’t too practical for most cases. There’s also the scrollIntoView function available for DOM element objects. You can call it to scroll to the element that’s calling this function.

With Vue.js, we can do this easily with the Vue-ScrollTo directive located at https://github.com/rigor789/vue-scrollTo. It allows us to scroll to an element identified by ID of an element and also add animation to the scrolling. It makes implementing this feature.

In this article, we will build a recipe app that has tool tips to guide users on how to add recipes into a form. Users can enter the name of their dish, the ingredients, the steps and upload a photo. In the entry, there will be a ‘Scroll to Top’ button to let the user scroll back to the top automatically by clicking the button. We will build the app with Vue.js.

We start building the app by running the Vue CLI. We run it by entering:

npx @vue/cli create recipe-app

Then select ‘Manually select features’. Next, we select Babel, Vue Router, Vuex, and CSS Preprocessor in the list. After that, we install a few packages. We will install Axios for making HTTP requests to our back end. BootstrapVue for styling, V-Tooltip for the tooltips, Vue-ScrollTo for scrolling and Vee-Validate for form validation. We install the packages by running npm i axios bootstrap-vue v-tooltip vee-validate vue-scrollto .

Now we move on to creating the components. Create a file called RecipeForm.vue in the components folder and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group
        label="Name"
        v-tooltip="{
          content: 'Enter Your Recipe Name Here',
          classes: ['info'],
          targetClasses: ['it-has-a-tooltip'],
        }"
      >
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group
        label="Ingredients"
        v-tooltip="{
          content: 'Enter Your Recipe Description Here',
          classes: ['info'],
          targetClasses: ['it-has-a-tooltip'],
        }"
      >
        <ValidationProvider name="ingredients" rules="required" v-slot="{ errors }">
          <b-form-textarea
            :state="errors.length == 0"
            v-model="form.ingredients"
            required
            placeholder="Ingredients"
            name="ingredients"
            rows="8"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">Ingredients is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group
        label="Recipe"
        v-tooltip="{
          content: 'Enter Your Recipe Here',
          classes: ['info'],
          targetClasses: ['it-has-a-tooltip'],
        }"
      >
        <ValidationProvider name="recipe" rules="required" v-slot="{ errors }">
          <b-form-textarea
            :state="errors.length == 0"
            v-model="form.recipe"
            required
            placeholder="Recipe"
            name="recipe"
            rows="15"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">Recipe is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="Photo">
        <input type="file" style="display: none" ref="file" @change="onChangeFileUpload($event)" />
        <b-button
          @click="$refs.file.click()"
          v-tooltip="{
            content: 'Upload Photo of Your Dish Here',
            classes: ['info'],
            targetClasses: ['it-has-a-tooltip'],
          }"
        >Upload Photo</b-button>
      </b-form-group>

      <img ref="photo" :src="form.photo" class="photo" />

      <br />

       <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "RecipeForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    recipe: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid || !this.form.photo) {
        return;
      }

      if (this.edit) {
        await this.editRecipe(this.form);
      } else {
        await this.addRecipe(this.form);
      }
      const { data } = await this.getRecipes();
      this.$store.commit("setRecipes", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    },
    onChangeFileUpload($event) {
      const file = $event.target.files[0];
      const reader = new FileReader();
      reader.onload = () => {
        this.$refs.photo.src = reader.result;
        this.form.photo = reader.result;
      };
      reader.readAsDataURL(file);
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    recipe: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

<style>
.photo {
  width: 100%;
  margin-bottom: 10px;
}
</style>

In this file, we have a form to let users enter their recipe. We have text inputs and a file upload file to let users upload a photo. We use Vee-Validate to validate our inputs. We use the ValidationObserver component to watch for the validity of the form inside the component and ValidationProvider to check for the validation rule of the inputted value of the input inside the component. Inside the ValidationProvider , we have our BootstrapVue input for the text input fields.

Each form field has a tooltip with additional instructions. The v-tooltip directive is provided by the V-Tooltip library. We set the content of the tooltip and the classes here, and we can set other options like delay in displaying, the position and the background color of the tooltip. A full list of options is available at https://github.com/Akryum/v-tooltip.

The photo upload works by letting users open the file upload dialog with the Upload Photo button. The button would click on the hidden file input when the Upload Photo button is clicked. After the user selects a file, then the onChangeFileUpload function is called. In this function, we have the FileReader object which sets the src attribute of the img tag to show the uploaded image, and also the this.form.photo field. readAsDataUrl reads the image into a string so we can submit it without extra effort.

This form is also used for editing recipes, so we have a watch block to watch for the recipe prop, which we will pass into this component when there is something to be edited.

Next we create a mixins folder and add requestsMixin.js into the mixins folder. In the file, we add:

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

export const requestsMixin = {
  methods: {
    getRecipes() {
      return axios.get(`${APIURL}/recipes`);
    },

    addRecipe(data) {
      return axios.post(`${APIURL}/recipes`, data);
    },

    editRecipe(data) {
      return axios.put(`${APIURL}/recipes/${data.id}`, data);
    },

    deleteRecipe(id) {
      return axios.delete(`${APIURL}/recipes/${id}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to get and save our data.

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

<template>
  <div class="page" id='top'>
    <h1 class="text-center">Recipes</h1>
    <b-button-toolbar class="button-toolbar">
      <b-button @click="openAddModal()" variant="primary">Add Recipe</b-button>
    </b-button-toolbar>

    <b-card
      v-for="r in recipes"
      :key="r.id"
      :title="r.name"
      :img-src="r.photo"
      img-alt="Image"
      img-top
      tag="article"
      class="recipe-card"
      img-bottom
    >
      <b-card-text>
        <h1>Ingredients</h1>
        <div class="wrap">{{r.ingredients}}</div>
      </b-card-text>

      <b-card-text>
        <h1>Recipe</h1>
        <div class="wrap">{{r.recipe}}</div>
      </b-card-text>

      <b-button
        href="#"
        v-scroll-to="{
          el: '#top',
          container: 'body',
          duration: 500,
          easing: 'linear',
          offset: -200,
          force: true,
          cancelable: true,
          x: false,
          y: true
        }"
        variant="primary"
      >Scroll to Top</b-button>

      <b-button @click="openEditModal(r)" variant="primary">Edit</b-button>

      <b-button @click="deleteOneRecipe(r.id)" variant="danger">Delete</b-button>
    </b-card>

    <b-modal id="add-modal" title="Add Recipe" hide-footer>
      <RecipeForm @saved="closeModal()" @cancelled="closeModal()" :edit="false" />
    </b-modal>

    <b-modal id="edit-modal" title="Edit Recipe" hide-footer>
      <RecipeForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :recipe="selectedRecipe"
      />
    </b-modal>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    RecipeForm
  },
  mixins: [requestsMixin],
  computed: {
    recipes() {
      return this.$store.state.recipes;
    }
  },
  beforeMount() {
    this.getAllRecipes();
  },
  data() {
    return {
      selectedRecipe: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(recipe) {
      this.$bvModal.show("edit-modal");
      this.selectedRecipe = recipe;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedRecipe = {};
    },
    async deleteOneRecipe(id) {
      await this.deleteRecipe(id);
      this.getAllRecipes();
    },
    async getAllRecipes() {
      const { data } = await this.getRecipes();
      this.$store.commit("setRecipes", data);
    }
  }
};
</script>

<style scoped>
.recipe-card {
  width: 95vw;
  margin: 0 auto;
  max-width: 700px;
}

.wrap {
  white-space: pre-wrap;
}
</style>

In this file, we have a list of BootstrapVue cards to display a list of recipe entries and let users open and close the add and edit modals. We have buttons on each card to let users edit or delete each entry. Each card has an image of the recipe at the bottom which was uploaded when the recipe is entered. For scrolling to top functionality, we used the v-scroll-to directive provided by the V-ScrollTo library. To make scrolling smooth, we set the easing property to linear . Also, we set the duration of the scroll to 500 milliseconds. el is the selector of the element we want to scroll to. Settingforce to true means that scrolling will be performed, even if the scroll target is already in view. cancelable is true means that the user can cancel scrolling. x set to false means that we don’t want to scroll horizontally, and y set to true means we want to scroll vertically. container is the selector for the container element that will be scrolled. offset is the offset in the number of pixels when scrolling. The full list of options is at https://github.com/rigor789/vue-scrollTo.

In the scripts section, we have the beforeMount hook to get all the password entries during page load with the getRecipes function we wrote in our mixin. When the Edit button is clicked, the selectedRecipe variable is set, and we pass it to the RecipeForm for editing.

To delete a recipe, we call deleteRecipe in our mixin to make the request to the back end.

The CSS in the wrap class is for rendering line break characters as line breaks.

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 to="/">Recipes 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;
  margin: 0 auto;
  max-width: 700px;
}

button {
  margin-right: 10px !important;
}

.button-toolbar {
  margin-bottom: 10px;
}

.tooltip {
  display: block !important;
  z-index: 10000;

.tooltip-inner {
    background: black;
    color: white;
    border-radius: 16px;
    padding: 5px 10px 4px;
  }

.tooltip-arrow {
    width: 0;
    height: 0;
    border-style: solid;
    position: absolute;
    margin: 5px;
    border-color: black;
  }

&[x-placement^="top"] {
    margin-bottom: 5px;

.tooltip-arrow {
      border-width: 5px 5px 0 5px;
      border-left-color: transparent !important;
      border-right-color: transparent !important;
      border-bottom-color: transparent !important;
      bottom: -5px;
      left: calc(50% - 5px);
      margin-top: 0;
      margin-bottom: 0;
    }
  }

&[x-placement^="bottom"] {
    margin-top: 5px;

.tooltip-arrow {
      border-width: 0 5px 5px 5px;
      border-left-color: transparent !important;
      border-right-color: transparent !important;
      border-top-color: transparent !important;
      top: -5px;
      left: calc(50% - 5px);
      margin-top: 0;
      margin-bottom: 0;
    }
  }

&[x-placement^="right"] {
    margin-left: 5px;

.tooltip-arrow {
      border-width: 5px 5px 5px 0;
      border-left-color: transparent !important;
      border-top-color: transparent !important;
      border-bottom-color: transparent !important;
      left: -5px;
      top: calc(50% - 5px);
      margin-left: 0;
      margin-right: 0;
    }
  }

&[x-placement^="left"] {
    margin-right: 5px;

.tooltip-arrow {
      border-width: 5px 0 5px 5px;
      border-top-color: transparent !important;
      border-right-color: transparent !important;
      border-bottom-color: transparent !important;
      right: -5px;
      top: calc(50% - 5px);
      margin-left: 0;
      margin-right: 0;
    }
  }

&[aria-hidden="true"] {
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.15s, visibility 0.15s;
  }

&[aria-hidden="false"] {
    visibility: visible;
    opacity: 1;
    transition: opacity 0.15s;
  }
}
</style>

to add a Bootstrap navigation bar to the top of our pages, and a router-view to display the routes we define. Also, we have the V-Tooltip styles in the style section. This style section isn’t scoped so the styles will apply globally. In the .page selector, we add some padding to our pages and set max-width to 700px so that the cards won’t be too wide. We also added some margins to our buttons.

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 BootstrapVue from "bootstrap-vue";
import VTooltip from "v-tooltip";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.use(VTooltip);

Vue.config.productionTip = false;

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

We added all the libraries we need here, including BootstrapVue JavaScript and CSS, Vee-Validate components along with the validation rules, and the V-Tooltip directive we used in the components.

In router.js we 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
    }
  ]
});

to include the home page in our routes so users can see the page.

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

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

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    recipes: []
  },
  mutations: {
    setRecipes(state, payload) {
      state.recipes = payload;
    }
  },
  actions: {}
});

to add our recipes state to the store so we can observer it in the computed block of RecipeFormand HomePage components. We have the setRecipes function to update the passwords state and we use it in the components by call this.$store.commit(“setRecipes”, response.data); like we did in RecipeForm .

Finally, in index.html , we 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>Recipe App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-tooltip-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.

After all the hard work, we can start our app by running npm run serve .

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:

{
  "`recipes`": [
]
}

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