Yup / Formik validation using dynamic keys
Asked Answered
W

3

11

I am trying to validate a form with a dynamic amount of fields - i.e., data is returned from an API that determines how many rows are shown, and for each row there is a required field that needs the user to select an input for them to advance.

The package to be used is Yup alongside Formik. When looking on Yup tutorials, the object is usually built as follows:

let userSchema = object({
  name: string().required(),
});

Where keys like name are defined. However, my keys will need to be dynamic i.e. field1, field2 etc. as I do not know ahead of time how many of them there will be.

I want to loop through my object and pass a dynamic set of keys to the schema - basically however long the object is, will be how many keys I have.

let userSchema = object({
  [field1]: string().required(),
  [field2]: string().required(),
});

However, I am unsure of how to achieve this result. I can loop through my object and try to build a set of keys, rough e.g.

let myObject = {}
myKeyObject.forEach((key) => myObject[key] = string().required());

And then pass myKeyObject to object.shape, but this usually produces TS errors. Does anyone know of any utility within Yup for a dynamic form? Unless there is something I have missed, I don't see anything on the documentation that would make working with dynamic forms easier

Wallin answered 5/4, 2022 at 18:15 Comment(4)
What is the typescript error that is producedViscid
Are the keys field1 and field2 known at compile time? Otherwise there would really be no point in typing userSchema.Cherri
Please check the answer below, it may help you.Calculable
How to reset the form after submitting.Sweepstake
C
12

If you want dynamic fields you can add an array of fields (containing the name of field or key, label, initial value and the type of field) then generate a Schema from that array, here is an example:

import React, { Fragment } from 'react';
import { Field, Form, Formik } from 'formik';
import { string, object, number } from 'yup';

interface IField{
  name: string,
  label: string,
  initialValue: any,
  type: any
}

const fields: IField[] = [
  {
    name: 'firstName',
    label: 'Firstname',
    initialValue: '',
    type: string().required()
  },
  {
    name: 'lastName',
    label: 'Lastname',
    initialValue: '',
    type: string().required()
  },
  {
    name: 'email',
    label: 'Email',
    initialValue: '',
    type: string().required()
  },
  {
    name: 'password',
    label: 'Password',
    initialValue: '',
    type: string().required()
  },
  {
    name: 'age',
    label: 'Age',
    initialValue: 18,
    type: number()
  }
];

const initialValues = Object.fromEntries(fields.map((field)=>[field.name, field.initialValue]))

const SchemaObject = Object.fromEntries(fields.map((field)=>[field.name, field.type]))

const UserSchema = object().shape(SchemaObject);

const App = () => (
  <Fragment>
    <h1>User</h1>
    <Formik
      initialValues={initialValues}
      onSubmit={values =>
        console.log({values})
      }
      validationSchema={UserSchema}
      >
        {({ errors, touched }) => {
          return(
          <Form>
            <div>
               {fields.map(({label, name}, index) => (
                  <div key={index}>
                    <label style={{width: 100, display: 'inline-block'}}>{label}</label>
                    <Field name={name} />
                    {touched[name] && errors[name] && <div style={{color: 'red'}}>{errors[name]?.toString()}</div>}
                  </div>
                ))}
              <div>
                <button type="submit">Submit</button>
              </div>
            </div>
        </Form>
      );
      }}
    </Formik>
  </Fragment>
);

export default App;
Calculable answered 17/5, 2022 at 13:8 Comment(9)
This just blew my mindInjudicious
If you see that answer is useful can you click the up button, thanks.Calculable
How to reset the form after submitting?Sweepstake
There is a callback func in Formik onSubmit that allow us to reset form: onSubmit={(values, { resetForm }) => { //Submission logic resetForm(); }}Calculable
Already tried that, but the values not clearing.Sweepstake
@Aflahvp check the enableReinitialize prop formik.org/docs/api/formik#enablereinitialize-booleanAuramine
@Auramine i tried that one and doesn't work.Sweepstake
what if i add new field dynamically after the first render? like on user click or somthingKeeping
You can make fields stateful, I suggest to use useReducer to have better control on the fields. Check this link: codesandbox.io/s/formik-dynamic-fields-zqngjkCalculable
C
1
**Today i was working on too my much forms so i was trying to make it more dynamic**

**Do you mean like this**    
**My Validation schema generator**

import testFormModel from './testFormModel';
    import * as yup from 'yup';
    
    const { formField } = testFormModel;
    
    const [firstName] = formField;
    
    const dynamicValidationGenerator = formField => {
      //dynamic required validation for required field
      const validateObj = {};
      formField.map(field => {
        field.required &&
          Object.assign(validateObj, {
            [field.name]: yup
              .string()
              .required(`${field.errorText.requiredErrorMsg}`),
          });
      });
      return validateObj;
    };
    
    //for manual validation + dynamic validation
    export default yup.object().shape({
      ...dynamicValidationGenerator(formField),
    
      [firstName.name]: yup.string().min(5),
    });

**my form model**
export default {
  formId: 'testForm',
  formField: [
    {
      name: 'firstName',
      label: 'First Name',
      required: true,
      errorText: {
        requiredErrorMsg: 'Required message',
      },
    },
    {
      name: 'lastName',
      label: 'Last Name',
      required: true,
      errorText: {
        requiredErrorMsg: 'Required message',
      },
    },
    { name: 'email', label: 'Email' },
    { name: 'age', label: 'Age' },
    { name: 'gender', label: 'Gender' },
  ],
};

**Initial form field value generator**
const initialFormValueGenerator = formField => {
  const initialValues = {};
  formField.map(el => Object.assign(initialValues, { [el.name]: '' }));
  return initialValues;
};
export default initialFormValueGenerator;

**Input field**
import React from 'react';
import { useField } from 'formik';

function InputField(props) {
  const { errorText, ...rest } = props;
  const [field] = useField(props);

  return (
    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
      <label>{props.label}</label>
      {props?.required && <span style={{ color: 'red' }}>*</span>}
      <input
        type='text'
        onChange={value => console.log(value)}
        name={props.name}
        {...field}
        {...rest}
      />
    </div>
  );
}

export default InputField;

**Form field html **
import React from 'react';
import InputField from '../FormField/InputField';

function AddressForm(props) {
  const { formField } = props;
  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        gap: 20,
        padding: 20,
      }}
    >
      {formField.map(field => {
        return (
          <div key={field.name}>
            <InputField {...field} />
          </div>
        );
      })}
    </div>
  );
}

export default AddressForm;

**App.js**
import { Formik, Form } from 'formik';
import React from 'react';
import AddressForm from './Features/Form/AddressForm';
import testFormModel from './Features/FormModel/testFormModel';
import validationSchema from './Features/FormModel/validationSchema';
import initialFormValueGenerator from './Features/Form/formInitialValues';
function App() {
  const { formId, formField } = testFormModel;

  const _handleSubmit = value => {
    console.log('submitted', value);
  };
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <div
        style={{
          width: '50%',
          border: '1px solid black',
          display: 'flex',
          flexDirection: 'column',
          marginTop: 20,
          padding: 20,
          backgroundColor: 'orange',
        }}
      >
        <Formik
          initialValues={initialFormValueGenerator(formField)}
          validationSchema={validationSchema}
          onSubmit={_handleSubmit}
        >
          {() => (
            <Form id={formId}>
              <AddressForm formField={formField} />
              <div>
                <button type='submit'>Submit</button>
              </div>
            </Form>
          )}
        </Formik>
      </div>
    </div>
  );
}

export default App;
Coastline answered 20/7, 2022 at 18:10 Comment(0)
H
-1

The reduce implementation can be changed to adapt it to your needs.

...
const FormValidation = Yup.object();


const DynamicForm = (props) => {

  const [obj, setObj] = useState(props.editObj);

  const validations = props.fields?.reduce((acc, curr) => {
    acc[curr.name] = Yup.string().required("Required");
    return acc;
  }, {});

  const formik = useFormik({
    initialValues: obj || {},
    onSubmit: props.onSubmit,
    validationSchema: FormValidation.shape(validations),
    enableReinitialize: true,
    validateOnChange: false
  });
}
...
Herzl answered 24/2, 2023 at 5:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.