Categories
JavaScript React

How to Make a Calendar App with 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.

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 day 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.

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 date and time pickers to select the date and time.

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 datepicker input and change the font of the datepicker.

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 in 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 use 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 back end and then saved into the store. In the useEffect callback, we set 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
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";
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/
    -->
    <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"
      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 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
JavaScript Nodejs React

How to Create Word Documents with Node.js

Microsoft Word is a very popular word processor. It is a popular format for making and exchanging documents. Therefore, a feature of a lot of apps is to create Word documents from other kinds of documents. Creating Word documents is easy in Node.js apps with third-party libraries. We can make an app from it easily ourselves.

In this article, we will make an app that lets users enter their document in a rich text editor and generate a Word document from it. We will use Express for back end and React for front end.

Back End

We will start with the back end. To start, we will create a project folder with the backend folder inside. Then in the backend folder run npx express-generator to create the Express app. Then run npm i to install the packages Next we install our own packages. We need Babel for run the app with the latest version of JavaScript, CORS for cross domain requests with front end, HTML-DOCX-JS for converting HTML strings to Word documents, Multer for file upload, Sequelize for ORM, and SQLite3 for our database.

We install all of these by running npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-docx-js sequelize sqlite3 multer .

After that we change the scripts section of package.json to have:

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

so that we run our app with Babel instead of the regular Node runtime.

Then create a .babelrc file in the backend folder and add:

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

to specify that we run our app with the latest version of JavaScript.

Next we add our database code. Run npx sequelize-cli init in the backend folder to create the Sequelize code.

We should have a config.js in the project now. In there, add:

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

to specify that we SQLite for our database.

Next create our model and migration by running:

npx sequelize-cli model:create --name Document --attributes name:string,document:text,documentPath:string

to create a Document model and Documents table.

We then run:

npx sequelize-cli db:migrate

to create the database.

Next we create our routes. Create a document.js file in the routes folder and add:

var express = require("express");
const models = require("../models");
var multer = require("multer");
const fs = require("fs");
var router = express.Router();
const htmlDocx = require("html-docx-js");
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./files");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});
const upload = multer({
  storage
});
router.get("/", async (req, res, next) => {
  const documents = await models.Document.findAll();
  res.json(documents);
});
router.post("/", async (req, res, next) => {
  const document = await models.Document.create(req.body);
  res.json(document);
});
router.put("/:id", async (req, res, next) => {
  const id = req.params.id;
  const { name, document } = req.body;
  const doc = await models.Document.update(
    { name, document },
    { where: { id } }
  );
  res.json(doc);
});
router.delete("/:id", async (req, res, next) => {
  const id = req.params.id;
  await models.Document.destroy({ where: { id } });
  res.json({});
});
router.get("/generate/:id", async (req, res, next) => {
  const id = req.params.id;
  const documents = await models.Document.findAll({ where: { id } });
  const document = documents[0];
  const converted = htmlDocx.asBlob(document.document);
  const fileName = `${+new Date()}.docx`;
  const documentPath = `${__dirname}/../files/${fileName}`;
  await new Promise((resolve, reject) => {
    fs.writeFile(documentPath, converted, err => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
  const doc = await models.Document.update(
    { documentPath: fileName },
    { where: { id } }
  );
  res.json(doc);
});
router.post("/uploadImage", upload.single("upload"), async (req, res, next) => {
  res.json({
    uploaded: true,
    url: `${process.env.BASE_URL}/${req.file.filename}`
  });
});
module.exports = router;

We do the standard CRUD operations to the Documents table in the first 4 routes. We have GET for getting all the Documents , POST for create a Document from post parameters, PUT for updating Document by ID, DELETE for deleting a Document by looking it up by ID. We have the HTML in the document field for generating the Word document later.

The generate route is for generating the Word document. We get the ID from the URL and then use the HTML-DOCX-JS package to generate a Word document. We generate the Word document by converting the HTML document to a file stream object with the HTML-DOCX-JS package and then writing the stream to a file and saving the path to the file in the Document entry in with the ID in the URL parameter.

We also have a uploadImage route to let user upload images with CKEditor with the CKFinder plugin. The plugin expects uploaded and url in the response so we return those.

Then we need to add a files folder in the backend folder.

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

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");
var indexRouter = require("./routes/index");
var documentRouter = require("./routes/document");
var 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(express.static(path.join(__dirname, "files")));
app.use(cors());
app.use("/", indexRouter);
app.use("/document", documentRouter);
// 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 expose the file folder with:

app.use(express.static(path.join(__dirname, "files")));

and expose the document route with:

var documentRouter = require("./routes/document");  
app.use("/document", documentRouter);

Front End

Now the back end is done, we can move onto the front end. Create the React app by running Create React App. We run npx create-react-app frontend in the root folder of the project.

We then install our packages. We will use CKEditor for our rich text editor, Axios for making HTTP requests, Bootstrap for styling, MobX for simple state management, React Router for routing URLs to components, and Formik and Yup for form value handling and form validation respectively.

Install all the packages by running npm i @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-react axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup .

With the packages installed, we can get started. In App.js , we replace the existing code with:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import TopBar from "./TopBar";
import { DocumentStore } from "./store";
import "./App.css";
const history = createHistory();
const documentStore = new DocumentStore();
function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} documentStore={documentStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;

to add our top bar and route to the home page.

In App.css , we replace the existing code with:

.page {
  padding: 20px;
}
.content-invalid-feedback {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 80%;
  color: #dc3545;
}
nav.navbar {
  background-color: green !important;
}

to add some padding to our page and style the validation message for the Rich text editor, and change the color of the navbar.

Next we create the form for adding and editing documents. Create a DocumentForm.js in the src file 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, Field } from "formik";
import { addDocument, editDocument, getDocuments, APIURL } from "./request";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
const schema = yup.object({
  name: yup.string().required("Name is required")
});
function DocumentForm({ documentStore, edit, onSave, doc }) {
  const [content, setContent] = React.useState("");
  const [dirty, setDirty] = React.useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid || !content) {
      return;
    }
    const data = { ...evt, document: content };
    if (!edit) {
      await addDocument(data);
    } else {
      await editDocument(data);
    }
    getAllDocuments();
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    onSave();
  };
  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? doc : {}}
      >
        {({
          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.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="content">
                <Form.Label>Content</Form.Label>
                <CKEditor
                  editor={ClassicEditor}
                  data={content || ""}
                  onInit={editor => {
                    if (edit) {
                      setContent(doc.document);
                    }
                  }}
                  onChange={(event, editor) => {
                    const data = editor.getData();
                    setContent(data);
                    setDirty(true);
                  }}
                  config={{
                    ckfinder: {
                      uploadUrl:
                        `${APIURL}/document/uploadImage`
                    }
                  }}
                />
                <div className="content-invalid-feedback">
                  {dirty && !content ? "Content is required" : null}
                </div>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}
export default observer(DocumentForm);

We wrap our React Bootstrap Form inside the Formik component to get the form handling function from Formik which we use directly in the React Bootstrap form fields. We cannot do the same with CKEditor, so we write our own form handlers for the rich text editor. We set the data prop in the CKEditor to set the value of the input of the rich text editor. The onInit function is used with users try to edit an existing document since we have to set the data prop with the editor initializes by running setContent(doc.document); . The onChange prop is the handler function for setting content whenever it is updated so the data prop will have the latest value, which we will submit when the user clicks Save.

We use the CKFinder plugin to upload images. To make it work, we set the image upload URL to the URL of the upload route in our back end.

The form validation schema is provided by the Yup schema object which we create at the top of the code. We check if the name field is filled in.

The handleSubmit function is for handling submitting the data to back end. We check both the content and the evt object to check both fields since we cannot incorporate the Formik form handlers directly into the CKEditor component.

If everything is valid, then we add a new document or update it depending on on whether the edit prop is true or not.

Then when saving is successful, we call getAllDocuments to populate the latest documents into our MobX store by running documentStore.setDocuments(response.data); .

Next we make our home page by creating HomePage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import DocumentForm from "./DocumentForm";
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 { getDocuments, deleteDocument, generateDocument, APIURL } from "./request";
function HomePage({ documentStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [doc, setDoc] = useState([]);
  const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };
  const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };
  const cancelAddModal = () => {
    setOpenAddModal(false);
  };
  const cancelEditModal = () => {
    setOpenEditModal(false);
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    setInitialized(true);
  };
  const editDocument = d => {
    setDoc(d);
    setOpenEditModal(true);
  };
  const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };
  const deleteSingleDocument = async id => {
    await deleteDocument(id);
    getAllDocuments();
  };
  const generateSingleDocument = async id => {
    await generateDocument(id);
    alert("Document Generated");
    getAllDocuments();
  };
  useEffect(() => {
    if (!initialized) {
      getAllDocuments();
    }
  });
  return (
    <div className="page">
      <h1 className="text-center">Documents</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Document</Button>
      </ButtonToolbar>
      <Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            edit={true}
            doc={doc}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>Document</th>
            <th>Generate Document</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {documentStore.documents.map(d => {
            return (
              <tr key={d.id}>
                <td>{d.name}</td>
                <td>
                  <a href={`${APIURL}/${d.documentPath}`} target="_blank">
                    Open
                  </a>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={generateSingleDocument.bind(this, d.id)}
                  >
                    Generate Document
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editDocument.bind(this, d)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSingleDocument.bind(this, d.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}
export default withRouter(observer(HomePage));

We have a React Bootstrap table for listing the documents with buttons to edit, delete documents and generate Word document. Also, there is a Open link for opening the Word document in each row. We have a create button on top of the table.

When the page loads, we call getAllDocuments and populate them in the MobX store. We open and close the add and edit modals with the openAddTemplateModal, closeAddModal, cancelAddModal, cancelEditModal functions.

Next create request.js in the src folder and add:

export const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getDocuments = () => axios.get(`${APIURL}/document`);
export const addDocument = data => axios.post(`${APIURL}/document`, data);
export const editDocument = data => axios.put(`${APIURL}/document/${data.id}`, data);
export const deleteDocument = id => axios.delete(`${APIURL}/document/${id}`);
export const generateDocument = id => axios.get(`${APIURL}/document/generate/${id}`);

to add the functions to make requests to our routes in the back end.

Then we create our MobX store. Create store.js in the src folder and put:

import { observable, action, decorate } from "mobx";
class DocumentStore {
  documents = [];
setDocuments(documents) {
    this.documents = documents;
  }
}
DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});
export { DocumentStore };

We have the function setDocuments to put the photo data in the store, which we used in HomePage and DocumentForm and we instantiated it before exporting so that we only have to do it in one place.

This block:

DocumentStore = decorate(DocumentStore, {  
  documents: observable,  
  setDocuments: action  
});

designates the documents array in DocumentStore as the entity that can be watched by components for changes. The setDocuments function is designated as the function that can be used to set the documents array in the store.

Next we create the top bar by creating a TopBar.js file in the src folder 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">Word 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);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We only display it with the token present in local storage. We check the pathname to highlight the right links by setting the active prop.

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <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>Word 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.

After writing all that code, we can run our app. Before running anything, install nodemon by running npm i -g nodemon so that we don’t have to restart back end ourselves when files change.

Then run back end by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.

Categories
JavaScript React

Getting Started with React and JSX

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

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

Getting Started

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

We can run it by running:

npx create-react-app my-app

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

cd my-app  
npm start

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

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

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

Creating Our First React App

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

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

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

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

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

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

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

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

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

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

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

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

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

Another way to write the code above is to write:

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

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

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

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

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

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

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

Otherwise, we’ll get an error.

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

We can use the method as follows:

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

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

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

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

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

Embedding Expressions in JSX

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

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

Then we see Hello World on the screen again.

Something more useful world calling a function as follows:

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

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

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

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

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

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

Conclusion

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

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

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

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

We can embed JavaScript expressions in between curly braces.

Categories
JavaScript React

How to Add Browser Notifications to Your React App

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

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

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

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

npx create-react-app password-manager

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

We install them by running:

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

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

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

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

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

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

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

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

Next create HomePage.css and add:

.home-page {  
  padding: 20px;  
}

to add some padding to our page.

Then create HomePage.js and add:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next, we modify index.js to have:

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

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

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

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

Next, we create PasswordForm.js and add:

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

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

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

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

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

export default PasswordForm;

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

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

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

Next, create requests.js and add:

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

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

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

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

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

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

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

export { PasswordsStore };

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

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

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

to change the title and add the Bootstrap CSS.

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

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

json-server --watch db.json

In db.json, change the text to:

{  
  "passwords": [  
  ]  
}

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

Categories
JavaScript React TypeScript

How to Use the Optional Chaining Operator in Your React App Right Now

Optional chaining is a proposed feature that may be incorporated into the JavaScript specification.

The operator allows you to traverse through a nested object to get the value of variables without worrying if any of those will be undefined.

For example, without optional chaining, if you have the object below:

const person = {  
  name: 'Alice',  
  cat: {  
    name: 'Bob'  
  }  
};

If you want to get the cat’s name, you have to use the code below:

const catName = person.cat.name;

If cat is undefined or null in person, the JavaScript interpreter will throw an error. With the optional chaining operator, you can write:

const catName = person?.cat?.name;

If cat is undefined, the catName will be null.

It also works with keys of an object. Instead of const catName = person?.cat?.name;, we can write:

const catName = person?.['cat']?.['name'];

This syntax also works with functions. For example, you can write:

func?.('foo')

To call the function func with a string, where func may be undefined or null. If the function does not exist, it will not be run.

To further illustrate the example and to show you how to use it in a real application, we will build a React app that uses the NASA API, to get the latest asteroid data.

We will use the Create React App CLI program to build the app.

As optional chaining is just a proposed feature, it is not currently supported with the CLI, so we have to do some work ourselves by installing some packages and making some changes to the Babel configuration of the app to enable optional chaining.

To start, we run npx create-react-app nasa-app to create the project folder with the initial files.

Next, we install npm i -D @babel/plugin-proposal-optional-chaining customize-cra react-app-rewired to begin customizing Create React App to support the optional chaining syntax.

Next, we have to add new configuration files and edit existing ones to let our app run and build with the syntax.

First, we add a file called config-overrides.js and add the following:

const { useBabelRc, override, useEslintRc } = require("customize-cra");
module.exports = override(useBabelRc());

To let us use the .babelrc configuration file.

Then, we create the .babelrc file in the root folder of our project and add:

{  
    "plugins": [  
        [  
            "@babel/plugin-proposal-optional-chaining"  
        ],  
    ]  
}

This will add support for optional chaining syntax in our project.

Next, we have to switch to make our app run and build with react-app-rewired instead of the usual react-script program.

To do this, in the scripts section of package.json, we put:

"scripts": {  
    "start": "react-app-rewired start",  
    "build": "react-app-rewired build",  
    "test": "react-app-rewired test --env=jsdom",  
    "eject": "react-scripts eject"  
}

In here, we replaced the original scripts that use react-script to run and build our app, with react-app-rewired.

Now we can use the optional chaining syntax to build our app.

First, we have to install some packages.

Run npm i axios bootstrap react-bootstrap formik yup react-router-dom to install the axios HTTP client, React Bootstrap for styling, Formik and Yup for building forms and adding form validation, and React Router for routing URLs to the pages we build.

Now we can write some code. In App.js, we replace the existing code with:

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

function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/search" exact component={AsteroidsSearchPage} />  
      </Router>  
    </div>  
  );  
}export default App;

So that we get client-side routing to our pages. In App.css, we replace the existing code with:

.center {  
  text-align: center;  
}

Next, we start building new pages. Create a file called AsteroidSearchPage.js in the src folder and add:

import React, { useState, useEffect } from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import Card from "react-bootstrap/Card";  
import "./AsteroidsSearchPage.css";  
import { searchFeed } from "./requests";

const schema = yup.object({  
  startDate: yup  
    .string()  
    .required("Start date is required")  
    .matches(  
      /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/,  
      "Invalid start date"  
    ),  
  endDate: yup  
    .string()  
    .required("End date is required")  
    .matches(  
      /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|\[12]\d|3\[01]))/,  
      "Invalid end date"  
    ),  
});

function AsteroidsSearchPage() {  
  const [feed, setFeed] = useState({});  
  const [error, setError] = useState("");
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    try {  
      const response = await searchFeed(evt);  
      setFeed(response.data.near_earth_objects);  
    } catch (ex) {  
      alert(ex?.response?.data?.error_message);  
    }  
  };

  return (  
    <div className="AsteroidsSearchPage">  
      <h1 className="center">Search Asteroids</h1>  
      <Formik validationSchema={schema} onSubmit={handleSubmit}>  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors,  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="startDate">  
                <Form.Label>Start Date</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="startDate"  
                  placeholder="YYYY-MM-DD"  
                  value={values.startDate || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.startDate && errors.startDate}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.startDate}  
                </Form.Control.Feedback>  
              </Form.Group>  
              <Form.Group as={Col} md="12" controlId="endDate">  
                <Form.Label>End Date</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="endDate"  
                  placeholder="YYYY-MM-DD"  
                  value={values.endDate || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.startDate && errors.endDate}  
                /><Form.Control.Feedback type="invalid">  
                  {errors.endDate}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Search  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      {Object.keys(feed)?.map(key => {  
        return (  
          <Card style={{ width: "90vw", margin: "0 auto" }}>  
            <Card.Body>  
              <Card.Title>{key}</Card.Title>  
              {feed?.[key]?.length > 0  
                ? feed?.[key]?.map(f => {  
                    return (  
                      <div style={{ paddingBottom: "10px" }}>  
                        {f?.close_approach_data?.length > 0 ? (  
                          <div>  
                            <b>  
                              Close Approach Date:  
                              {f?.close_approach_data?.map(d => {  
                                return <p>{d?.close_approach_date_full}</p>;  
                              })}  
                            </b>  
                          </div>  
                        ) : null}  
                        <div>  
                          Minimum Estimated Diameter:{" "}  
                          {  
                            f?.estimated_diameter?.kilometers  
                              ?.estimated_diameter_min  
                          }{" "}  
                          km  
                        </div>  
                        <div>  
                          Maximum Estimated Diameter:{" "}  
                          {  
                            f?.estimated_diameter?.kilometers  
                              ?.estimated_diameter_max  
                          }{" "}  
                          km  
                          <br />  
                        </div>  
                      </div>  
                    );  
                  })  
                : null}  
            </Card.Body>  
          </Card>  
        );  
      })}  
    </div>  
  );  
}

export default AsteroidsSearchPage;

This is where we add a form to search for asteroid data from the NASA API by date range. Both the start and end date fields should be in YYYY-MM-DD format and we changed our form validation to match that in the schema object.

Once validation is done, by calling the schema.validate function, we search. The response has many nested objects, so we use the optional chaining syntax everywhere in the result cards.

We loop through close_approach_data array, and we don’t assume it’s always defined or has a greater than zero length, the same goes for the call to the map function. We do not assume that the map function is always defined.

We also use the optional chaining syntax for f?.estimated_diameter?.kilometers ?.estimated_diameter_min and f?.estimated_diameter?.kilometers ?.estimated_diameter_max.

The more levels of nesting there are, the less likely it is that you can traverse the object tree successfully without the optional chaining syntax, as there is more chance of nested objects being undefined.

Also, note that the optional chaining syntax works for returned results like Object.keys(feed)

In AsteroidSearchPage.css, which we should create in the src folder, we put:

.AsteroidsSearchPage{  
  margin: 0 auto;  
  width: 90vw;  
}

To add some margins to the page.

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

import React, { useState, useEffect } from "react";  
import { browse } from "./requests";  
import Card from "react-bootstrap/Card";  
import "./HomePage.css";

function HomePage() {  
  const [initialized, setIntialized] = useState(false);  
  const [feed, setFeed] = useState([]);
  const browserFeed = async () => {  
    const response = await browse();  
    setFeed(response.data.near_earth_objects);  
    setIntialized(true);  
  };

  useEffect(() => {  
    if (!initialized) {  
      browserFeed();  
    }  
  });  
  return (  
    <div className="HomePage">  
      <h1 className='center'>Asteroids Close to Earth</h1>  
      <br />  
      {feed?.map(f => {  
        return (  
          <Card style={{ width: "90vw", margin: "0 auto" }}>  
            <Card.Body>  
              <Card.Title>{f?.name}</Card.Title>  
              <div>  
                {f?.close_approach_data?.length > 0 ? (  
                  <div>  
                    Close Approach Date:  
                    {f?.close_approach_data?.map(d => {  
                      return <p>{d?.close_approach_date_full}</p>;  
                    })}  
                  </div>  
                ) : null}  
                <p>  
                  Minimum Estimated Diameter:{" "}  
                  {f?.estimated_diameter?.kilometers?.estimated_diameter_min} km  
                </p>  
                <p>  
                  Maximum Estimated Diameter:{" "}  
                  {f?.estimated_diameter?.kilometers?.estimated_diameter_max} km  
                </p>  
              </div>  
            </Card.Body>  
          </Card>  
        );  
      })}  
    </div>  
  );  
}

export default HomePage;

This page is very similar to the AsteroidSearchPage component with the use of the optional chaining syntax.

Next, create HomePage.css in the src folder and add:

.HomePage{  
  text-align: left;  
}

To align our text to the left.

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

const APIURL = "https://api.nasa.gov/neo/rest/v1]";  
const axios = require("axios");

export const searchFeed = data =>  
  axios.get(  
    `${APIURL}/feed?start_date=${data.startDate}&end_date=${data.endDate}&api_key=${process.env.REACT_APP_APIKEY}`  
  );

export const browse = () =>  
  axios.get(`${APIURL}/neo/browse?api_key=${process.env.REACT_APP_APIKEY}`);

To add the functions for making the HTTP requests to the NASA API for getting asteroid data and searching for them.

process.env.REACT_APP_APIKEY has the API key when you add the API key to the .env file of your project with REACT_APP_APIKEY as the key. Register for an API key at NASA.

Finally, we add TopBar.js to the src folder 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 }) {  
  const { pathname } = location;  
  return (  
    <Navbar bg="primary" expand="lg" variant="dark">  
      <Navbar.Brand href="#home">NASA App</Navbar.Brand>  
      <Navbar.Toggle aria-controls="basic-navbar-nav" />  
      <Navbar.Collapse id="basic-navbar-nav">  
        <Nav className="mr-auto">  
          <Nav.Link href="/" active={pathname == "/"}>  
            Home  
          </Nav.Link>  
          <Nav.Link href="/search" active={pathname.includes("/search")}>  
            Search  
          </Nav.Link>  
        </Nav>  
      </Navbar.Collapse>  
    </Navbar>  
  );  
}

export default withRouter(TopBar);

This is the navigation bar at the top of each page in our app. We set the active prop by checking the current URL of the page so we get highlights in our links.