Categories
React Answers

How to Initiate a Draft.js Editor with Content within a React App?

Sometimes, we want to initiate a Draft.js editor with content within a React app.

In this article, we’ll look at how to initiate a Draft.js editor with content within a React app.

Initiate a Draft.js Editor with Content within a React App

To initiate a Draft.js editor with content within a React app, we can use the ContentState.createFromText method to create the initial content for the text editor.

Then we can pass that into the EditorState.createWithContent method to set the content state as the editor’s initial content.

For instance, we can write:

import { ContentState, Editor, EditorState } from "draft-js";
import { useRef, useState } from "react";

const content = ContentState.createFromText("<b>hello world</b>");

export default function App() {
  const [editorState, setEditorState] = useState(() =>
    EditorState.createWithContent(content)
  );

  const editor = useRef(null);
  const focusEditor = () => {
    editor.current.focus();
  };

  return (
    <div
      style={{ border: "1px solid black", minHeight: "6em", cursor: "text" }}
      onClick={focusEditor}
    >
      <Editor
        ref={editor}
        editorState={editorState}
        onChange={setEditorState}
        placeholder="Write something!"
      />
    </div>
  );
}

We add:

const content = ContentState.createFromText("<b>hello world</b>");

to set “<b>hello world</b>” to create the initial content of the editor,

Then we have:

const [editorState, setEditorState] = useState(() =>
  EditorState.createWithContent(content)
);

to set the content as the initial state of the editorState , which has the initial value of the text editor which can be used by Draft.js’ Editor component.

Then we set the editorState prop to the editorState state as its value.

The onChange prop is set to the setEditorState function that sets binds the editorState to the input value of the editor.

Therefore, <b>hello world</b> is the content of the editor when it’s loaded initially.

Conclusion

To initiate a Draft.js editor with content within a React app, we can use the ContentState.createFromText method to create the initial content for the text editor.

Then we can pass that into the EditorState.createWithContent method to set the content state as the editor’s initial content.

Categories
React Answers

How to Fix a Checkbox not Emitting the Change Event in React?

Sometimes, we may encounter the issue where a checkbox not emitting the change event when we’re developing a React app.

In this article, we’ll look at how to fix the issue where a checkbox not emitting the change event when we’re developing a React app.

Fix a Checkbox not Emitting the Change Event in React

To fix the issue where a checkbox not emitting the change event when we’re developing a React app, we should get the checked value of the checkbox by setting the onChange prop to a function that gets the checked value of the checkbox.

Then we can set the checkbox value by setting the checkbox to a state that’s set to the checked value of the checkbox.

For instance, we can write:

import { useState } from "react";

export default function App() {
  const [active, setActive] = useState(false);
  return (
    <div className="App">
      <input
        type="checkbox"
        checked={active}
        onChange={(e) => setActive(e.target.checked)}
      />
    </div>
  );
}

to set the onChange prop to a function that calls the setActive function with e.target.checked to set the active state to the checked value of the checkbox.

Then we set the checked prop to the value of the active state to make the checkbox update with the checked value of the active state.

Conclusion

To fix the issue where a checkbox not emitting the change event when we’re developing a React app, we should get the checked value of the checkbox by setting the onChange prop to a function that gets the checked value of the checkbox.

Then we can set the checkbox value by setting the checkbox to a state that’s set to the checked value of the checkbox.

Categories
Express Nodejs React

How to Use SendGrid to Send Emails

SendGrid is a great service made by Twilio for sending emails. Rather than setting up your own email server for sending email with your apps, we use SendGrid to do the hard work for us. It also decrease 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 email by:

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

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

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

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 variavles, SendGrid Nodejs for sending emails, CORS for enabling cross domain requests with front end and SQLite3 for 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 a 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 which we enter the email template. We have the name and template field as required fields and description 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 they’re valid, then we call addTemplate or editTemplate depending if you want to add or edit 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 = {};
    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 and we extract the variables from the template text in the getSingleTemplate function. We also create a validate schema with Yup with:

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

We loop through the keys of the variable field of the response.data object and build the Yup form validation schema dynamically.

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.

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 display 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 user add a 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 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 use 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 out 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 in here. Also we designated templates as observable in the decorate function to allow templates field to have the latest value whenever it is accessed. setTemplates is designated as action so that we can call it to manipulate the store.

Next create TopBar.js and add:

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

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

export default withRouter(TopBar);

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

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Email App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and change the title of the app.

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

Categories
React

How to Upload Files with React and Node.js

File upload is a common operation for web applications. In Node.js, with the Express web framework and the Multer library, adding file upload feature to your app is very easy.

To add file upload feature to your app, first you need an input field with type file. By default this type of input renders itself as a button which is difficult to style. The user clicks the dialog and if you attach an onChange handler to it, then you can access the file object by using JavaScript. The onChange handler takes a parameter which is the event object. The object has the file under the target.files property. Once you have that, you can add it to the FormData object with the append function to attach it to the form submission.

In this article, we will make a photo manager application that lets users enter a name, description, and upload a photo with the text.

We will use React for the frontend and Express with Multer for backend.

Backend: Node.js for File Upload

We start by building the backend. We create the project folder and a backend folder inside the project folder. Then go into the backend folder and run npx express-generator to generate the code files for our back end app.

Next, we run npm i to install the packages for our backend provided by the generator. We also need Babel to be able to use the import syntax, the CORS package for cross-domain communication, Multer for file upload with Express, Sequelize as our ORM, and SQLite3 as the database. We will use SQLite for simplicity.

To install all the packages, run npm i @babel/cli @babel/core @babel/node @babel/preset-env cors multer sequelize sqlite3 Then create a file called .babelrc in the root of the backend folder and add:

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

Then in the scripts section of package.json, we add:

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

This lets us run our app with Babel instead of regular Node runtime. We should also install nodemon to watch for file changes and restart the app. Install it globally by running npm i -g nodemon.

Next we run npx sequelize-cli init in the backend folder to create the Sequelize ORM code to let us make and run migrations.

After that finishes, we should have a config.js created. In there, replace the existing code with:

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

This specifies SQLite as our database.

Next we create a migration with the model:

npx sequelize-cli model:create --name Photo --attributes name:string,description:string,photoPath:string

Notice that we no spaces between commas after the attributes flag.

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

After that, we create the routes for manipulating photos. In the routes folder, create photos.js and add:

var express = require("express");
var multer = require("multer");
var router = express.Router();
const models = require("../models");
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./uploads");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});

const upload = multer({
  storage
});

/* GET users listing. */
router.get("/", async (req, res, next) => {
  const photos = await models.Photo.findAll();
  res.json(photos);
});

router.post("/add", upload.single("photo"), async (req, res, next) => {
  try {
    const path = req.file.path;
    const { name, description } = req.body;
    const entry = await models.Photo.create({
      name,
      description,
      photoPath: path
    });
    res.json(entry);
  } catch (ex) {
    res.status(400).send({ error: ex });
  }
});

router.put("/edit", upload.single("photo"), async (req, res, next) => {
  try {
    const path = req.file && req.file.path;
    const { id, name, description } = req.body;
    let params = {};
    if (path) {
      params = {
        name,
        description,
        photoPath: path
      };
    } else {
      params = {
        name,
        description
      };
    }
    const photo = await models.Photo.update(params, {
      where: {
        id
      }
    });
    res.json(photo);
  } catch (ex) {
    res.status(400).send({ error: ex });
  }
});

router.delete("/delete/:id", async (req, res, next) => {
  const { id } = req.params;
  await models.Photo.destroy({
    where: {
      id
    }
  });
  res.json({ deleted: id });
});

module.exports = router;

These are all the routes for manipulating photos. The models file is created by Sequelize CLI, which contains the model objects which we manipulate to save our data to the Photos table. We have a GET route to get the photos with findAll, a POST route for saving a Photo with create, a PUT route that updates the model with update, and a DELETE route that deletes a Photo with destroy.

To add file upload, we use the multer package which we installed earlier. It is very simple to add. We just specify the file name and folder to upload the file to, like in this block of code:

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./uploads");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});

const upload = multer({
  storage
});

We include the multer middleware with the routes we want to access files with like so:

upload.single("photo")

This specifies that we let the frontend upload a file in the FormData with the photo field.

Then we can access the file path of the saved file in the routes by using req.file.path to save the path to our database.

We need to create an uploads folder in the backend folder to save the files.

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

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 photosRouter = require("./routes/photos");

var app = express();

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

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, "uploads")));

app.use("/", indexRouter);
app.use("/photos", photosRouter);

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

In this file, we added app.use(cors()); to enable frontend cross-domain requests to backend. We expose the photos routes to the frontend with the following code:

`var photosRouter = require(“./routes/photos”);
`app.use("/photos", photosRouter);

And we add a static path route to access the files:

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

Now that back end is done, we can move on to frontend.

Frontend: File Upload with React

We will use React to build the frontend with MobX for simple state management. To create the skeleton code, we run npx create-react-app frontend.

Next we have to install some packages. We will install MobX, Bootstrap for styling, React Router for routing, Formik and Yup for form value handling and form validation respectively, and Axios for making HTTP requests.

To do this run npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup to install all the packages.

With all the packages installed, we can start writing code. To start, we replace the existing code in App.js with:

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

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

export default App;

This adds a top bar which we will create next, and we define the routes so that we see the home page and address generator page when we go to the defined URLs.

Next create HomePage.js in the src for our home page. In it, we will display the table of entries, which has the name, description, the photo, and buttons to open the add / edit photo forms, and delete the entries.

In the file, we add:

import React, { useState, useEffect } from "react";
import * as yup from "yup";
import "./HomePage.css";
import Modal from "react-bootstrap/Modal";
import PhotoForm from "./PhotoForm";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getPhotos, deletePhoto, APIURL } from "./requests";

function HomePage({ photosStore }) {
  const [show, setShow] = useState(false);
  const [showEdit, setShowEdit] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedPhoto, setSelectedPhoto] = useState({});
  const handleClose = () => setShow(false);
  const handleShow = () => setShow(true);

  const handleEditClose = () => setShowEdit(false);
  const handleEditShow = photo => {
    setSelectedPhoto(photo);
    setShowEdit(true);
  };

  const getAllPhotos = async () => {
    const response = await getPhotos();
    photosStore.setPhotos(response.data);
  };

  const deletePhotoById = async id => {
    await deletePhoto(id);
    await getAllPhotos();
  };

  const onSave = () => {
    setShow(false);
    setShowEdit(false);
  };

  useEffect(() => {
    if (!initialized) {
      getAllPhotos();
      setInitialized(true);
    }
  });

  return (
    <div className="home-page">
      <h1>Photos</h1>
      <Button variant="primary" onClick={handleShow}>
        Add Photo
      </Button>

      <Table striped bordered hover style={{ marginTop: 10 }}>
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Photo</th>
            <th></th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {photosStore.photos.map((p, i) => {
            const splitPath = p.photoPath.split("");
            const path = splitPath[splitPath.length - 1];
            return (
              <tr key={i}>
                <td>{p.name}</td>
                <td>{p.description}</td>
                <td>
                  <img src={`${APIURL}/${path}`} style={{ width: 200 }} />
                </td>
                <td>
                  <Button onClick={handleEditShow.bind(this, p)}>Edit</Button>
                </td>
                <td>
                  <Button onClick={deletePhotoById.bind(this, p.id)}>
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>

      <Modal show={show} onHide={handleClose}>
        <Modal.Header closeButton>
          <Modal.Title>Add Photo</Modal.Title>
        </Modal.Header>

        <Modal.Body>
          <PhotoForm
            edit={false}
            photosStore={photosStore}
            onSave={onSave.bind(this)}
          />
        </Modal.Body>
      </Modal>

     <Modal show={showEdit} onHide={handleEditClose}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Photo</Modal.Title>
        </Modal.Header>

        <Modal.Body>
          <PhotoForm
            edit={true}
            photosStore={photosStore}
            selectedPhoto={selectedPhoto}
            onSave={onSave.bind(this)}
          />
        </Modal.Body>
      </Modal>
    </div>
  );
}

export default observer(HomePage);

The Table is provided by React Bootstrap. We just display every entry in its own row. The entries are provided by our MobX store which we will create. The modals contain the PhotoForm which has all the inputs for manipulating photos. We will use it for both add and edit so we need to pass in the edit prop to distinguish between add and edit. We also pass in a onSave function so that we can close the modals. If there is an entry selected for edit, we also pass in selectedPhoto prop, which contains the entry that the user is editing.

Next we build the photo form. Create a file called PhotoForm.js in the src folder and add:

import React from "react";
import { useEffect, useState } 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 "./PhotoForm.css";
import { getPhotos, addPhoto, editPhoto } from "./requests";
import { observer } from "mobx-react";

const schema = yup.object({
  name: yup.string().required("Name is required"),
  description: yup.string().required("Description is required")
});

function PhotoForm({ photosStore, edit, selectedPhoto, onSave }) {
  const fileUpload = React.createRef();
  const [photo, setPhoto] = useState(null);
  const [fileName, setFileName] = useState("");
  const getAllPhotos = async () => {
    const response = await getPhotos();
    photosStore.setPhotos(response.data);
  };

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      let bodyFormData = new FormData();
      if (!edit) {
        bodyFormData.set("name", evt.name);
        bodyFormData.set("description", evt.description);
        bodyFormData.append("photo", photo);
        await addPhoto(bodyFormData);
      } else {
        bodyFormData.set("id", selectedPhoto.id);
        bodyFormData.set("name", evt.name);
        bodyFormData.set("description", evt.description);
        if (photo) {
          bodyFormData.append("photo", photo);
        }
        await editPhoto(bodyFormData);
      }
    } catch (error) {
      alert("Upload must be an image");
    }

  await getAllPhotos();
    onSave();
  };

  const setFile = evt => {
    setPhoto(evt.target.files[0]);
    setFileName(evt.target.files[0].name);
  };

  const openUploadDialog = () => {
    fileUpload.current.click();
  };

  return (
    <div>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? selectedPhoto : {}}
      >
        {({
          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.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="photo">
                <input
                  type="file"
                  ref={fileUpload}
                  name="photo"
                  style={{ display: "none" }}
                  onChange={setFile}
                />
                <div className="file-box">
                  <Button type="button" onClick={openUploadDialog}>
                    Upload Photo
                  </Button>
                  <span style={{ paddingLeft: "10px", marginTop: "5px" }}>
                    {fileName}
                  </span>
                </div>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Save
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}

export default observer(PhotoForm);

In this file, we have the file input for getting the photo file and the form fields for name and description. To get the file, we pass the setFile function into the onChange prop of the file input, where we get the file object by using ev.target.files[0]. The [0] means that we only want the first file.

We wrapped the React Boostrap form inside the Formik component to automatically handle form values, which will be available in the parameter of the handleSubmit function. In that function, we validate the text inputs with the schema.validation function. Then we add the text data and file to the FormData object. After everything is added, we submit the FormData object to our back end via HTTP.

After that, we call getAllPhotos to get the latest data and set the new data in our MobX store. Then we call onSave function, which is passed in from HomePage so that we can close the dialog box after everything is done.

Next create PhotoForm.css and add:

.file-box {
  display: flex;
}

Then create requests.js in the src folder and add:

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

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

export const addPhoto = data =>
  axios({
    method: "post",
    url: `${APIURL}/photos/add`,
    data,
    config: { headers: { "Content-Type": "multipart/form-data" } }
  });

export const editPhoto = data =>
  axios({
    method: "put",
    url: `${APIURL}/photos/edit`,
    data,
    config: { headers: { "Content-Type": "multipart/form-data" } }
  });

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

We need these functions to send the requests to the backend. Note that we have config: { headers: { “Content-Type”: “multipart/form-data” } } in the post so that we send form data instead of the default JSON to the back end. Form data can include files but JSON cannot.

Next we create the MobX Store. Create a file called store.js and add:

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

class PhotosStore {
  photos = [];

  setPhotos(photos) {
    this.photos = photos;
  }
}

PhotosStore = decorate(PhotosStore, {
  photos: observable,
  setPhotos: action
});

const photosStore = new PhotosStore();

export { photosStore };

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

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 }) {
  const { pathname } = location;

  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Photo 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>
      </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.

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/
    -->
    <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>Photo 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>

This adds the Bootstrap CSS file in the link tag and change the title of the app.

After writing all that code, we can run our app. First start the backend 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
React Answers

How to Add the CSS display: none Style within a Conditional Expression with React?

Sometimes, we want to add the CSS display: none style within a conditional expression with React.

In this article, we’ll look at how to add the CSS display: none style within a conditional expression with React.

Add the CSS display: none Style within a Conditional Expression with React

To add the CSS display: none style within a conditional expression with React, we can pass in an object into the style prop of an element.

For instance, we can write:

import React, { useState } from "react";

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow((s) => !s)}>toggle</button>
      <div style={{ display: show ? "block" : "none" }}>hello</div>
    </div>
  );
}

We have the show state that we create with the useState hook.

Then we set the onClick prop of the button to a function that calls setShow to toggle show between true and false .

And then we can set the display CSS property of the div with a ternary expression.

If show is true , then set it to 'block' .

Otherwise, we set it to 'none' .

Conclusion

To add the CSS display: none style within a conditional expression with React, we can pass in an object into the style prop of an element.