7
votes

I am trying to create a React component to abstract away creating an Input group for my form. All inputs have the same layout - a Label, with the Input underneath and if errors/info text is present these are displayed under the Input.

Previously I was handling my own form state/handlers. Now I am experimenting with formik (with Yup validation) and have run into a problem dynamically accessing the error and touched fields when I have nested information.

Here is my input group component:

import React from 'react';
import { FormGroup, Label, Input, FormFeedback, FormText } from 'reactstrap';
import { Field, ErrorMessage } from 'formik';

const InputGroup = ({ name, label, type, info, required }) => {
  return (
    <FormGroup>
      <Label htmlFor={name}>{label}{required && '*'}</Label>
      <Field name={name}>
        {({field, form}) => (
          <Input {...field} id={name} type={
                 invalid={form.errors[name] && form.touched[name]} //problem here
          />
        )}
      </Field>
      {info && <FormText color="muted">{info}</FormText>}
      <ErrorMessage name={name}>
          {msg => <FormFeedback>{msg}</FormFeedback>}
      </ErrorMessage>
    </FormGroup>
  )
}

InputGroup.propTypes = {
  label: PropTypes.string,
  name: PropTypes.string.isRequired,
  type: PropTypes.string,
  info: PropTypes.string,
  required: PropTypes.bool
};

InputGroup.defaultProps = {
  type: 'text',
  required: false
};

As I am using bootstrap ([email protected]), the <FormFeedback> element requires the accompanying <Input> to be labelled with an invalid tag. In the above I dynamically assign invalid=true/false if the corresponding field on formik's form.errors object exists (ie. an error exists) and form.touched object is true (ie. the user has touched the input).

This works fine when formik is setup with a flat initialValues (eg. below), as the invalid={form.errors[name] && form.touched[name]} evaluates to (for eg.) invalid={form.errors[firstName] && form.touched[firstName]}

initialValues = {
  firstName: '',
  lastName: '',
  email: '',
  password: ''
}

However, when formik is setup with a nested initialValues (eg. below), the invalid={form.errors[name] && form.touched[name]} evaluates to invalid={form.errors[name.first] && form.touched[name.first]}. Ultimately, this will always evaluate to false, hence the input is always invalid=false, thusthe input is never marked with the error styling nor the error message displayed.

initialValues = {
  name: {
    first: '',
    last: ''
  },
  email: '',
  password: ''
}

How can I go about setting up my InputGroup component so that I can dynamically access the required fields on formik's error and touched objects regardless of whether it is flat or nested?

2

2 Answers

16
votes

Formik has a function getIn() that can extract a value from an object by a path (e.g. a path being something like name.first).

<Field name={name}>
  {({ field, form }) => (
    <Input
      {...field}
      id={name}
      invalid={getIn(form.errors, name) && getIn(form.touched, name)}
    />
  )}
</Field>

See an example here on CodeSandbox.

0
votes

Formik also supports meta argument from the Field component, there is information specified for the exact field (value, touched, error).

const CustomFormikInput = (props) => {   
    return <Field name={props.name}>
       {({ field, meta }) => {
            console.log(JSON.stringify(meta)); // Log meta output
            const errorMsg = meta.touched ? meta.error : undefined;
            return <div>
                     <input {...field} {...props} />
                     {errorMsg}
                   </div>;
        }}
    </Field>;
}