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 protected]',
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.