'Formik Validation in a Field Array

I have a formik form like this:-

import Drawer from "components/atoms/Drawer";
/* import Input from "components/atoms/Input";
import InputGroup from "components/atoms/InputGroup";
import Label from "components/atoms/Label"; */
import Scrim from "components/atoms/Scrim";
import DrawerBody from "components/molecules/DrawerBody";
import { Field, FieldArray, Form, FormikErrors, FormikProps, withFormik } from "formik";
import { ITrackedPage } from "hocs/withAppDynamics";
import * as React from "react";
import { Box, Flex, Text } from "rebass";
import * as AmenitiesActions from "store/amenities/actions";
import { IAmenity, IAmenityRanking } from "store/amenities/models";
import DrawerHeader from "./DrawerHeader";
// import ErrorMessage from 'components/atoms/ErrorMessage';




interface IAmenitiesDrawerProps {
    drawerOpen: boolean;
    onDrawerClose: () => void;
    tenantAssessmentId: string;
    actions: typeof AmenitiesActions;
    maxRank?: number;
  }



interface IAmenitiesDrawerValues {
    amenitieslist: IAmenity[];
}

const InnerForm: React.FC<
IAmenitiesDrawerProps & ITrackedPage & FormikProps<IAmenitiesDrawerValues>
> = ({
  errors,
  drawerOpen,
  onDrawerClose,
  handleChange,
  values,
  setValues,
  isValid,
  tenantAssessmentId,
  sendAnalyticsData,
  actions
}) => {

  const handleDrawerClose = () => {
    onDrawerClose();
  };

  return (
    <>
      <Scrim isOpen={drawerOpen} onClose={handleDrawerClose} />
      <Drawer isOpen={drawerOpen} direction="right" drawerWidth="700px">
        <DrawerHeader handleCloseDrawer={handleDrawerClose} />
        <DrawerBody p={5}>
          <Flex mb={4}>
            <Box flex={1}>
              <Text fontWeight="light" fontSize={4} mt={3} mb={4}>
                Add custom amenities
              </Text>
            </Box>
          </Flex>
          <Form>
          <FieldArray
              name="amenitieslist"
              render={arrayHelpers => (
                <div>
                {// values.amenitieslist && values.amenitieslist.length > 0 ? (
                  values.amenitieslist.map((amenity, index) => (
                    <div key={index}>
                      <Field name={`amenitieslist.${index}.name`} />
                      <button
                        type="button"
                        onClick={() => arrayHelpers.remove(index)} // remove a amenity from the list
                      >
                        -
                      </button>
                      {errors.amenitieslist}
                    </div>
                  ))}
                  <button type="button" onClick={() => arrayHelpers.push({ id: "", name: "", imageUrl: '', description: '', tenantAssessmentId })}>
                    {/* show this when user has removed all amenities from the list */}
                    Add a Amenity
                  </button>
              </div>
            )}
            />
            <div>
                <button type="submit">Submit</button>
              </div>
    </Form>
        </DrawerBody>
      </Drawer>
    </>
  );
};

export const AmenitiesDrawer = withFormik<IAmenitiesDrawerProps, IAmenitiesDrawerValues>({
  enableReinitialize: true,
  handleSubmit: (values, {props}) => {
    const seed: number = props.maxRank? props.maxRank : 0;
    const amenityRankings: IAmenityRanking[] = values.amenitieslist.map((a, index)=>({
        amenityId: 1,
        rank: index + 1 + seed,
        amenityName: a.name,
        customAmenity: true
      }));
      console.log(amenityRankings);
      console.log(props.actions.customSaveAmenities);
      console.log(props.tenantAssessmentId);
      props.actions.customSaveAmenities(props.tenantAssessmentId, amenityRankings);
  },
  mapPropsToValues: ({tenantAssessmentId}) => ({
      amenitieslist:[{id: 0, name: '', imageUrl: '', description: '', tenantAssessmentId}]
  }),
  validate: values => {
    const errors: FormikErrors<{ validAmenity: string }> = {};
    console.log('In the Validate method');
    const { amenitieslist } = values;

    const amenityValid = amenitieslist[0].name.length < 28;
    if (!amenityValid) {
        console.log('Amenity is not valid');
      errors.validAmenity = "Amenity needs to be atmost 28 characters";
      console.log(errors);
    }

    return errors;
  }
})(InnerForm);

As you all can see I have a text input. I want to throw a error message below the text field when the length is more than 28 characters long.

How is this possible ? Please help me with this.



Solution 1:[1]

I find the most convenient way to validate Formik forms is using yup as recommended in their documentation. You can define a validation schema and pass it as a prop to the main Formik component (or HOC as it appears you're using) and remove your custom validation function:

validationSchema: yup.object().shape({
  amenitieslist: yup.array()
    .of(yup.object().shape({
      name: yup.string().max(28, "Max 28 chars")
      // Rest of your amenities object properties
    }))
})

And then in your FieldArray:

<FieldArray
    name="amenitieslist"
    render={arrayHelpers => (
        <div>
          { values.amenitieslist && values.amenitieslist.length > 0 ? (
            values.amenitieslist.map((amenity, index) => (
                <div key={index}>
                  <Field name={`amenitieslist[${index}].name`} />
                  <button
                      type="button"
                      onClick={() => arrayHelpers.remove(index)} // remove a amenity from the list
                  >
                    -
                  </button>
                  {errors.amenitieslist[index].name}
                </div>
            ))}
          <button type="button" onClick={() => arrayHelpers.push({ id: "", name: "", imageUrl: '', description: '', tenantAssessmentId })}>
            {/* show this when user has removed all amenities from the list */}
            Add a Amenity
          </button>
        </div>
    )}
/>

I just changed the accessors you were using to designate the field name (to use an index for an array element, you have to use bracket notation) and where to find the errors, yup should generate them automatically. Tough to know for sure I'm not missing anything without testing it, hope this helps!

Solution 2:[2]

I wasn't using yup so the other solutions weren't useful for me. What I did was is have another value that can represent your error. I loop through my fieldarray and assign an error.


    values.payments.forEach((element) => {
      if (Number(element.isBTC) === 1 && !values.btcCompanyId)  {
        errors.btcCompany =
          "For this payment account, BTC Company must be selected";
      }
    });

You can make something like this for yourself. I sometimes have an _action value given to the last button and have errors show there. You'll figure it out. Not the right solution if you want the error to display for each fieldarray.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Muhammad Mubashirullah Durrani