1

I'm using react for the first time. I have hooked up a form with Formik and have all the validation stuff working but I am stuck on keeping form values if there was a problem.

The form collects data to add a record to the database. If the API response has data, the insert was successful and I show a success Alert. If the insert failed, data is empty because null was returned from the API and I show a danger Alert.

My question is how do I keep the form fields' values if there was an error? I want to close the danger Alert and still have the form fields populated so the user does not have to start over. I don't need to do this on success because the information the user entered got into the database.

I feel like this has something to do with the state of the form itself or the fields but I can't figure how to retain the values. I tried doing ...values in onSubmit but that didn't work (don't quite understand that spread thing). Any help would be appreciated.

This is what I have:

import React, { useState } from "react"
import * as Yup from "yup"
import { Formik, Form, Field } from "formik"
import "../../custom.css"
import * as apiService from "./../../services/apiService"
import DatePicker from "../../utils/DatePicker"
import "react-datepicker/dist/react-datepicker.css"
import Alert from "reactstrap/lib/Alert"
import { Button } from "reactstrap"
import "bootstrap/dist/css/bootstrap.min.css"

const CompanyMasterAdd = () => {
  const [showAlert, setShowAlert] = useState(false)
  const [apiResponse, setApiResponse] = useState(null)
  const urlRegex =
    /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\\+\\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\\+\\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\\+~%\\/.\w-_]*)?\??(?:[-\\+=&;%@.\w_]*)#?(?:[\w]*))?)/

  //Setup values for drop downs so we can validate against selection
  //This one is set here because we need to access statuses in the YUP validation
  const statuses = ["1", "2", "3", "4", "5"]
  const statusOptions = statuses.map((statusID, key) => (
    <option value={statusID} key={key}>
      {statusID}
    </option>
  ))

  //setup validation requirements and error messages for fields via Yup
  const companyMasterAddSchema = Yup.object().shape({
    companyId: Yup.number()
      //custom required message when required field not filled in
      .required("Company Id is required")
      //if it's not a number, display custom error message
      .typeError("Company Id must be a number")
      .positive()
      .integer(),
    companyName: Yup.string()
      .max(75, "Must be 75 characters or less")
      .required("Company Name is required"),
    databaseName: Yup.string()
      .required("Database Name is required")
      .max(100, "Must be 100 characters or less"),
    statusID: Yup.string().required("Status ID is required").oneOf(statuses),
    website: Yup.string().matches(urlRegex, "Website URL is not valid"),
  })

  //Setup values for drop downs so we can validate against selection
  const companyTypes = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

  const companyTypeOptions = companyTypes.map((companyTypeID, key) => (
    <option value={companyTypeID} key={key}>
      {companyTypeID}
    </option>
  ))

  //dismiss alert when button is closed
  const onDismiss = () => {
    setShowAlert(false)
  }
  return (
    <div id="companyMasterPageContainer">
      <h1>Add Company Master record</h1>
      <hr />
      {showAlert ? (
        <div>
          <Alert
            color={apiResponse && !apiResponse.data ? "danger" : "success"}
          >
            <h4 className="alert-heading">
              {apiResponse && !apiResponse.data ? "ERROR" : "SUCCESS"}
            </h4>
            <p>
              {apiResponse && !apiResponse.data
                ? 
                //TODO: keep form data populated if there is a failure
                //TODO: get the actual error from the API somehow
                "An error occurred. Check the ProcedureErrorLog table"
                : "Record added"}
            </p>
            <hr />
            <Button onClick={() => onDismiss()}>Dismiss</Button>
          </Alert>
        </div>
      ) : (
        // show form if showAlert is false
        <div id="companyMasterAddFormContainer">
          <h2>Form area</h2>
          <Formik
            initialValues={{
              companyId: "",
              companyName: "",
              website: "",
              fiscalYearEnd: null,
              taxIDNumber: "",
              companyTypeID: "",
              isKeyCompany: false,
              databaseName: "",
              statusID: "",
            }}
            validationSchema={companyMasterAddSchema}
            onSubmit={async (
              values,
              { resetForm, setSubmitting, isSubmitting }
            ) => {
              apiService.addCompanyMaster(values).then((response) => {
                setApiResponse(response)
                setShowAlert(true)
              })
            }}
          >
            {({ errors, touched, values, setFieldValue }) => (
              <Form id="companyMasterAddForm">
                <div>
                  <span className="requiredStar">
                    * indicates a required field
                  </span>
                </div>
                <div className="row">
                  <div className="formLabel">
                    <label
                      htmlFor="companyId"
                      className="companyMasterAddFormLabel"
                    >
                      Company ID <span className="requiredStar">*</span>
                    </label>
                  </div>
                  <div className="formInput">
                    <Field
                      value={values.companyId}
                      type="text"
                      id="companyId"
                      className="companyMasterAddFormField"
                      name="companyId"
                      placeholder="e.g., 1,2,3..."
                    />
                    {/* If this field has been touched, and it contains an error, display it  */}
                    {touched.companyId && errors.companyId && (
                      <div className="formError">{errors.companyId}</div>
                    )}
                  </div>
                </div>
                <div className="row">
                  <div className="formLabel">
                    <label
                      className="companyMasterAddFormLabel"
                      htmlFor="fiscalYearEnd"
                    >
                      Fiscal Year End
                    </label>
                  </div>
                  <div className="formInput">
                    <DatePicker
                      name="fiscalYearEnd"
                      value={values.fiscalYearEnd}
                      onChange={setFieldValue}
                      className="companyMasterAddFormField"
                      class="companyMasterAddFormField"
                    />
                    {/* If this field has been touched, and it contains an error, display it  */}
                    {touched.fiscalYearEnd && errors.fiscalYearEnd && (
                      <div className="formError">{errors.fiscalYearEnd}</div>
                    )}
                  </div>
                </div>
                <div className="row">
                  <button
                    type="submit"
                    style={{
                      textAlign: "center",
                      marginLeft: "30%",
                      marginRight: "30%",
                    }}
                  >
                    Submit
                  </button>
                </div>
              </Form>
            )}
          </Formik>
        </div>
      )}
    </div>
  )
}

export default CompanyMasterAdd

UPDATE - GOT IT WORKING
Separated the conditional logic for the alert and the form:

{
        showAlert && (
          <div>
            <Alert
              color={apiResponse && !apiResponse.data ? "danger" : "success"}
            >
              <h4 className="alert-heading">
                {apiResponse && !apiResponse.data ? "ERROR" : "SUCCESS"}
              </h4>
              <p>
                {apiResponse && !apiResponse.data
                  ? //TODO: keep form data populated if there is a failure
                    "An error occurred. Check the xyztable in the database"
                  : "Record added"}
              </p>
              <hr />
              <Button onClick={() => onDismiss()}>Dismiss</Button>
            </Alert>
          </div>
        )
        // show form if showAlert is false
      }
      <div
        id="companyMasterAddFormContainer"
        style={{ visibility: open ? "visible" : "hidden" }}
      >
        <h2>Form area</h2>

        <Formik
          initialValues={{

BONUS - HIDE FORM WHILE ALERT IS BEING DISPLAYED

  1. Added new state variable:
const [open, setOpen] = useState(true)
  1. Added visibility style in the div container for the form:
<div
        id="companyMasterAddFormContainer"
        style={{ visibility: open ? "visible" : "hidden" }}
      >
  1. Toggle the state of open in the onSubmit area of form:
onSubmit={async (
            values,
            { resetForm, setSubmitting, isSubmitting }
          ) => {
            setSubmitting(false)
            apiService.addCompanyMaster(values).then((response) => {
              setApiResponse(response)
              setShowAlert(true)
              setOpen(false)
              if (response && response.data) {
                resetForm()
              }
            })
  1. Toggle the state on the alert dismissal:
const onDismiss = () => {
    setShowAlert(false)
    setOpen(true)
  }
Neo
  • 115
  • 1
  • 8

1 Answers1

4

The Formik component is where the form state lives (eg. values, touched, errors). You are rendering Formik inside of a conditional. Each time the form is submitted you set showAlert to true, which removes the Formik component and its children from the DOM. Instead, you should conditionally render the alert, while always rendering the Formik component.

Then inside the onSubmit function, you can call the resetForm callback when you have a successful insert.

Stafford Rose
  • 769
  • 4
  • 9
  • 1
    Thank you @StaffordRose for the explanation. I got it working with your suggestion. The reason I was doing it this way was because I wanted to hide the form while the alert was being displayed. I guess it's not that big of a deal. – Neo Sep 09 '21 at 23:04
  • You can still hide the form visually. One way to do that is to position the alert as an overlay and give it a background that covers the form: https://stackoverflow.com/a/2941203/9922737 – Stafford Rose Sep 09 '21 at 23:14
  • Thanks for the suggestion @Stafford Rose. I finally figured it. I set me another state variable: `const [open, setOpen] = useState(true)` ; I added a visibility style in the div container for the form: `style={{ visibility: open ? "visible" : "hidden" }}`; In the onSubmit area I toggle the states `setOpen(false) if (response && response.data) { resetForm()`; and toggle it back in the alert dismissal: `setOpen(true)`. Thanks for your help! I was stuck. – Neo Sep 10 '21 at 00:10
  • That's a good solution! Happy I could help. – Stafford Rose Sep 10 '21 at 02:22