I am having a form which is having 3 forms within and each child form will have its separate submit button and the parent will have its own submit button. I want to show the form one by one like initially only 1st form will be shown then on clicking continue 1st and 2nd form will be displayed and then after again clicking on continue 1st,2nd,3rd form will be displayed. All 3 forms are on the same page in 3 different components.
The code structure is like this: Codebox link
Also am facing an error
<form> cannot appear as a descendant of <form>
Because form within form, is there a way to achieve the same without this is error
You need to use a state to control it: https://codesandbox.io/s/elated-dawn-zlh5x?file=/src/App.js
export default function App() {
const [formNumber, setFormNumber] = useState(0);
const initialValues = { name: "", email: "", number: "" };
const onSubmit = async (values, { setSubmitting }) => {
console.log(" Final Values", values);
};
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
{({ isSubmitting, values, setFieldValue }) => (
<Form className="space-y-7">
{formNumber >= 0 && <Form1 initialValues={initialValues} />}
{formNumber >= 1 && <Form2 initialValues={initialValues} />}
{formNumber >= 2 && <Form3 initialValues={initialValues} />}
<div className="flex flex-row-reverse">
<button type="button" onClick={() => setFormNumber(formNumber + 1)}>
Continue
</button>
</div>
</Form>
)}
</Formik>
);
}
I came up something like this, we can keep the count of form number and add one every time we click continue. But there needs to be validation before continue button enabled and plus, you need to handle the last step extra.
export default function App() {
const [formNumber, setFormNumber] = useState(0)
const initialValues = { name: "", email: "", number: "" };
const onSubmit = async (values, { setSubmitting }) => {
console.log(" Final Values", values);
};
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
{({ isSubmitting, values, setFieldValue }) => (
<Form className="space-y-7">
{formNumber === 0 && <Form1 initialValues={initialValues} />}
{formNumber === 1 && <Form2 initialValues={initialValues} />}
{formNumber === 2 && <Form3 initialValues={initialValues} />}
<div className="flex flex-row-reverse">
<button onClick={()=> setFormNumber(formNumber + 1)} type="button">Continue</button>
</div>
</Form>
)}
</Formik>
);
}
Instead of using Form components, you can make it a single form with 3 inputs. That would be easier I believe.
You should use useState to save the index of current form, then create a function to change current index and pass it as a function to the child components!
use this:
import "./styles.css";
import React,{useState} from "react";
import { Formik, Form } from "formik";
import Form1 from "./Form1";
import Form2 from "./Form2";
import Form3 from "./Form3";
export default function App() {
const initialValues = { name: "", email: "", number: "" };
const onSubmit = async (values, { setSubmitting }) => {
console.log(" Final Values", values);
};
const [index, setIndex] = useState(0);
const nextIndex = ()=>{
setIndex(index+1);
}
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
{({ isSubmitting, values, setFieldValue }) => (
<Form className="space-y-7">
{
index==0?<Form1 initialValues={initialValues} handleIndex={nextIndex}/>:
index==1?<Form2 initialValues={initialValues} handleIndex={nextIndex}/>:
<Form3 initialValues={initialValues} handleIndex={nextIndex}/>
}
<div className="flex flex-row-reverse">
<button type="button">Continue</button>
</div>
</Form>
)}
</Formik>
);
}
now, in each child use handleIndex function to go to the next form!
Related
I have a Formik form that is using a progressive stepper and have multiple fields across different components, thus requiring the need to store the values in React Context. However none of the field values are being passed, so when I click submit, all values are empty strings and the validation fails. You can see on each Formik Field i am setting the value as {state.[field]}, which comes from the Context, so I believe something is going wrong here. Can anyone see what I'm doing wrong?
Thanks a lot
Here is my parent component
const AddSongPage = () => {
const { state, dispatch } = useUploadFormContext();
const initialValues = {
name: "",
};
const { mutate: handleCreateTrack } = useCreateSyncTrackMutation(
gqlClient,
{}
);
const handleSubmit = (values: any) => {
handleCreateTrack(
{
...values,
},
{
onSuccess() {
console.log("Track added succesfully");
},
}
);
};
const validate = Yup.object({
name: Yup.string().required("Song name is required"),
description: Yup.string().optional(),
});
return (
<Layout headerBg="brand.blue">
<Formik
onSubmit={(values) => handleSubmit(values)}
initialValues={initialValues}
validationSchema={validate}
>
<Form>
<Box> {state.activeStep === 1 && <Step1 />}</Box>
<Box> {state.activeStep === 2 && <Step2 />}</Box>
<Box> {state.activeStep === 3 && <Step3 />}</Box>
<Button type={"submit"}>Submit</Button>
</Form>
</Formik>
</Layout>
);
};
Here is step 1
const Step1 = () => {
const { state, dispatch } = useUploadFormContext();
const onInputChange = (e: FormEvent<HTMLInputElement>) => {
const inputName = e.currentTarget.name;
dispatch({
type: "SET_UPLOAD_FORM",
payload: {
[inputName]: e.currentTarget.value,
},
});
};
return (
<Stack spacing={4}>
<Field name={"name"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.name}
/>
</FormControl>
)}
</Field>
<Field name={"description"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.description}
/>
</FormControl>
)}
</Field>
</Stack>
);
};
export default Step1;
I have a form with a fields array. At the bottom of the form there is a button that deletes all list items (in fact, it does form.restart).
When deleting fields, the validation of each field is triggered, but the value of the field is undefined. This breaks the validation logic.
In theory, this validation should not exist at all.
How to get rid of this validation on form.restart?
Code:
(https://codesandbox.io/s/festive-water-6u3t69?file=/src/App.js:0-1477)
import React from "react";
import { Form, Field } from "react-final-form";
import { FieldArray } from "react-final-form-arrays";
import arrayMutators from "final-form-arrays";
import "./styles.css";
export default function App() {
const [values] = React.useState({ items: [] });
const renderForm = ({ form }) => {
return (
<div>
<FieldArray name={"items"}>
{({ fields }) => (
<div>
{fields.map((name, index) => (
<div key={name}>
<Field
name={`${name}.title`}
validate={(value) => console.log(value)}
key={name}
>
{({ input }) => (
<input
value={input.value}
onChange={(event) => input.onChange(event.target.value)}
/>
)}
</Field>
</div>
))}
<button onClick={() => fields.push({ title: "111" })}>
add item
</button>
<button onClick={() => form.restart({ items: [] })}>reset</button>
</div>
)}
</FieldArray>
</div>
);
};
return (
<div className="App">
<Form
onSubmit={() => {}}
initialValues={values}
render={renderForm}
mutators={{ ...arrayMutators }}
/>
</div>
);
}
Thank you.
I have the following code which you can find here:
https://stackblitz.com/edit/react-d2fadr?file=src%2FApp.js
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
<ErrorMessage name={fieldName} />
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
I want to know if is it any way to render the ErrorMessage element automatically?
The error message should be shown somewhere around the input text.
If you know how, you can fork the StackBlitz above with your suggestion.
Thanks!
Don't really know why ErroMessage is not rendering before you submit your form once but you can replace the line <ErrorMessage name={fieldName} /> by {formik.errors[fieldName]} to make it works
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
{formik.errors[fieldName]}
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
the issue is validation schema. When I changed 6
return /^[0-9a-f]{6}$/.test(value);
to 3
return /^[0-9a-f]{3}$/.test(value);
and submitted with the initial value, ErrorMessage component is rendered
To reach your goal, I changed your code as below:
Since Formik's default component is Input, I deleted your TextInput component as there was nothing special in your component and handleChange function.
<Field name="hexColor" placeholder="Hex Color" onChange={(e) => handleChange(e.target.value)}/>
<ErrorMessage name="hexColor" />
As in mentioned in this answer, I changed your submit button condition to determine whether the button is disabled or not:
<Button type="submit" disabled={Object.keys(errors).length}>
Submit
</Button>
You can view my entire solution here.
Edit
If you want to keep your component, you should pass props as you might be missing something important, e.g. onChange,onBlur etc.
const TextInput = ({ field, ...props }) => {
return (
<input
{...field} {...props}
// ... your custom things
/>
);
};
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
onChange={(e) => handleChange(e.target.value)}
/>
Solution 2
I'm working on React based on NextJS code.
If I manually enter values in form fields and submit the form, I get all the values in the component function.
But if I dynamically display values in a form field like value={query.campaignid}, the component does not collect any value.
import { Button, Input } from "#chakra-ui/react";
import { Field, Form, Formik } from "formik";
import { useRouter } from "next/router";
export default function FormikExample() {
const { query } = useRouter();
return (
<>
<Formik
initialValues={
{
// campaignid: " ",
}
}
onSubmit={(values) => {
setTimeout(() => {
console.log(values);
});
}}
>
{(props) => (
<Form>
<Field name="campaignid">
<Input value={query.campaignid} />
</Field>
<Button id="submited" isLoading={props.isSubmitting} type="submit">
Submit
</Button>
</Form>
)}
</Formik>
</>
);
}
Resolved!
I passed the URL query to initialValues and added enableReinitialize={true} below:
...
initialValues={{
campaignId: query.campaignid,
}}
enableReinitialize={true}
...
In the input value I passed those props:
<Field name="campaignid">
<Input value={props.values.campaignId} />
</Field>
I have a dynamic array of form fields, whose values are fetched via REST API. On the page, there is also a dropdown, that, when changed, shows a different array of fields. I fetch all of these fields/values during the componentDidMount life cycle hook and filter the list to show the relevant data.
The Formik docs mention FieldArrays as a means to handle an array of fields. However, their example shows a static list of objects as its initialValues -- but I don't see how dynamically generated lists. In fact, since I'm fetching initialValues via AJAX, it's initially an empty array -- so nothing is rendered even after getting the data.
This is simplified version of my code:
const MyComponent = class extends Component {
componentDidMount() {
// data structure: [{Name: '', Id: '', Foo: '', Bar: ''}, ...]
axios
.get('/user')
.then((res) => {
this.setState({
userData: res.data
});
});
}
render() {
return (
<div>
<Formik
initialValues={{
users: this.state.userData
}}
render={({values}) => (
<Form>
<FieldArray
name="users"
render={arrayHelpers => (
<ul>
{
values.users.map((user, index) => {
return (
<li key={user.Id}>
<div>{user.Name}</div>
<Field name={`user[${index}].Foo`} type="text" defaultValue={user.Foo} />
<Field name={`user[${index}].Bar`} type="text" defaultValue={user.Bar} />
</li>);
}
}
</ul>
)}
/>
</Form>
)}
/>
</div>
);
}
}
You can do this via setting enableReinitialize true. According to doc it will do this:
Default is false. Control whether Formik should reset the form if initialValues changes (using deep equality).
I created complete codesanbox where your incoming data is async and when you push the data its also async. check this:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const InviteFriends = () => {
const [initialValues, setInitialValues] = React.useState({
friends: []
});
useEffect(() => {
const initialValues = {
friends: [
{
name: "",
email: ""
}
]
};
const timer = setTimeout(() => {
setInitialValues(initialValues);
}, 1000);
return () => {
timer && clearTimeout(timer);
};
}, []);
return (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{({ values }) => (
<Form>
<FieldArray name="friends">
{({ insert, remove, push }) => (
<div>
{console.log("Values", values, initialValues)}
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>
Email
</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={async () => {
await new Promise((r) =>
setTimeout(() => {
push({ name: "", email: "" });
r();
}, 500)
);
}}
>
Add Friend
</button>
</div>
)}
</FieldArray>
<button type="submit">Invite</button>
</Form>
)}
</Formik>
</div>
);
};
ReactDOM.render(<InviteFriends />, document.getElementById("root"));
Here is the demo: https://codesandbox.io/s/react-formik-async-l2cc5?file=/index.js