Reputation: 2685
I am trying to follow the Formik documentation on using FieldArrays so that I can add repeatable form elements to my form.
I've also seen this Medium post setting out an example.
I'm slow to learn and can't join the dots between the documentation and the implementation.
I want to have a button in my main form that says: "Add a request for data".
If that button is selected, then a nested form setting out the data profile is displayed, along with "add another data request" and "remove" buttons.
I have made the nested form in another component in my application, but I'm struggling to figure out how to use the example from the medium post to incorporate the nested form (as a repeatable element - ie someone might want 5 data requests).
Are there any examples of how to implement this?
In my code, I have basically followed the medium post, but tried to link the Data Request form component inside the index
<button
type="button"
onClick={() => arrayHelpers.insert(index, <DataRequestForm />)}>
Add a data request
</button>
This is plainly incorrect, but I can't get a handle on how to do this.
Taking Nithin's answer, I've tried to modify the embedded form so that I can use react-select, as follows, but I'm getting an error which says:
TypeError: Cannot read property 'values' of undefined
import React from "react";
import { Formik, Form, Field, FieldArray, ErrorMessage, withFormik } from "formik";
import Select from "react-select";
import {
Button,
Col,
FormControl,
FormGroup,
FormLabel,
InputGroup,
Table,
Row,
Container
} from "react-bootstrap";
const initialValues = {
dataType: "",
title: "",
description: '',
source: '',
}
class DataRequests extends React.Component {
render() {
const dataTypes = [
{ value: "primary", label: "Primary (raw) data sought" },
{ value: "secondary", label: "Secondary data sought"},
{ value: "either", label: "Either primary or secondary data sought"},
{ value: "both", label: "Both primary and secondary data sought"}
]
return(
<Formik
initialValues={initialValues}
render={({
form,
remove,
push,
errors,
status,
touched,
setFieldValue,
setFieldTouched,
handleSubmit,
isSubmitting,
dirty,
values
}) => {
return (
<div>
{form.values.dataRequests.map((_notneeded, index) => {
return (
<div key={index}>
<Table responsive>
<tbody>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsTitle">Title</label>
<Field
name={`dataRequests.${index}.title`}
placeholder="Add a title"
className={"form-control"}
>
</Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsDescription">Description</label>
<Field
name={`dataRequests.${index}.description`}
component="textarea"
rows="10"
placeholder="Describe the data you're looking to use"
className={
"form-control"}
>
</Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsSource">Do you know who or what sort of entity may have this data?</label>
<Field
name={`dataRequests.${index}.source`}
component="textarea"
rows="10"
placeholder="Leave blank and skip ahead if you don't"
className={
"form-control"}
>
</Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataType">
Are you looking for primary (raw) data or secondary data?
</label>
<Select
key={`my_unique_select_keydataType`}
name={`dataRequests.${index}.source`}
className={
"react-select-container"
}
classNamePrefix="react-select"
value={values.dataTypes}
onChange={selectedOptions => {
// Setting field value - name of the field and values chosen.
setFieldValue("dataType", selectedOptions)}
}
onBlur={setFieldTouched}
options={dataTypes}
/>
</div>
</td>
</tr>
<tr>
<Button variant='outline-primary' size="sm" onClick={() => remove(index)}>
Remove
</Button>
</tr>
</tbody>
</Table>
</div>
);
})}
<Button
variant='primary' size="sm"
onClick={() => push({ requestField1: "", requestField2: "" })}
>
Add Data Request
</Button>
</div>
)
}
}
/>
);
};
};
export default DataRequests;
Upvotes: 3
Views: 5490
Reputation: 2685
For anyone looking to learn from this post, the answer to this question from nithin is clearly motivated by good intentions, but it isn't a correct deployment of formik. You can check out a code sandbox here: https://codesandbox.io/embed/goofy-glade-lx65p?fontsize=14 for the current attempt at solving this problem (still not fixed) but a better step toward a solution. Thanks just the same for the helpful intentions behind the answer to this question.
Upvotes: 2
Reputation: 3679
You cannot add nested forms inside a form element. Please refer the below post for mode details.
If you are looking to nest multiple fields with a nested structure, inside a main form, you can achieve it using FieldArrays
.
You can structure the form like.
{
firstName: "",
lastName: "",
dataRequests: []
}
Here firstName
and lastName
are top level form fields and dataRequests
can be an array where each element follows the structure
{
requestField1: "",
requestField2: ""
}
Since dataRequests
is an array, for rendering each item of FieldArray
you need a map function.
form.values.dataRequests.map( render function() )
And for each rendered item, the change handlers should target their index to update the correct item in FieldArray
.
<div key={index}>
<Field
name={`dataRequests.${index}.requestField1`}
placeholder="requestField1"
></Field>
<Field
name={`dataRequests.${index}.requestField2`}
placeholder="requestField2"
></Field>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
In the above snippet name={dataRequests.${index}.requestField1}
asks formik to update the key requestField1
of nth
element of dataRequests
array with the value of the input field.
Finally your <DataRequest />
component might look something like below.
import React from "react";
import { Field } from "formik";
export default ({ form, remove, push }) => {
return (
<div>
{form.values.dataRequests.map((_notneeded, index) => {
return (
<div key={index}>
<Field
name={`dataRequests.${index}.requestField1`}
placeholder="requestField1"
></Field>
<Field
name={`dataRequests.${index}.requestField2`}
placeholder="requestField2"
></Field>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
);
})}
<button
type="button"
onClick={() => push({ requestField1: "", requestField2: "" })}
>
Add Data Request
</button>
</div>
);
};
And using <FieldArray />
you can connect <DataRequest />
to the main form.
You can try out the below sample SO snippet
function DataRequests({ form, remove, push }){
return (
<div>
{form.values.dataRequests.map((_notneeded, index) => {
return (
<div key={index}>
<Formik.Field
name={`dataRequests.${index}.requestField1`}
placeholder="requestField1"
></Formik.Field>
<Formik.Field
name={`dataRequests.${index}.requestField2`}
placeholder="requestField2"
></Formik.Field>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
);
})}
<button
type="button"
onClick={() => push({ requestField1: "", requestField2: "" })}
>
Add Data Request
</button>
</div>
);
};
class Home extends React.Component {
initialValues = {
firstName: "",
lastName: "",
dataRequests: []
};
state = {};
render() {
return (
<div>
<Formik.Formik
initialValues={this.initialValues}
onSubmit={values => {
this.setState({ formData: values });
}}
>
{() => {
return (
<Formik.Form>
<div>
<Formik.Field
name="firstName"
placeholder="First Name"
></Formik.Field>
</div>
<div>
<Formik.Field
name="lastName"
placeholder="Last Name"
></Formik.Field>
</div>
<Formik.FieldArray name="dataRequests" component={DataRequests} />
<button type="submit">Submit</button>
</Formik.Form>
);
}}
</Formik.Formik>
{this.state.formData ? (
<code>{JSON.stringify(this.state.formData, null, 4)}</code>
) : null}
</div>
);
}
}
ReactDOM.render(<Home />, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/formik/dist/formik.umd.production.js"></script>
<div id="root"></div>
Upvotes: 7