Reputation: 153
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
Upvotes: 11
Views: 17906
Reputation: 482
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;
Upvotes: 12
Reputation: 814
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
});
}
...
Upvotes: -1
Reputation: 11
**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;
Upvotes: 1