Reputation: 2715
I am trying to figure out how to use react with formik to integrate repeatable form fields in my form. The formik documentation provides an outline of how Field Arrays work. I have read it a million times and am not clever enough to understand it.
I have also read this medium post which includes an example and which I have tried to follow in learning how to make a repeatable form element.
I previously asked this question and tried to work with the advice in it, but have run into the errors shown in this stackblitz. I can't figure out how to resolve them.
Someone on discord provided this code sandbox example to show how repeatable fields are supposed to work. I am struggling to adapt that example to use my form in its context. I can't get the remove element working at all, before I try to embed a separate component with multiple form fields instead of a single form element called title.
I am trying to:
Have a parent form that has a button that says, add a data request;
When that button is clicked, display another form (with multiple questions, currently setup as a separate class component) that asks questions for that data requests
Have a button that says "remove this data request" so that a completed form element can be removed
Repeat steps 1-3 above so that there can be multiple data requests.
The closest this has come to getting to working is in the code sandbox above, but I'm struggling to replace the single title form element with the DataRequests.jsx form component that has several form fields in it.
Can anyone see how to do that?
To set out the code in this post, my main form has:
import React from "react";
import ReactDOM from "react-dom";
import DataRequests from "./DataRequests";
// import { fsDB, firebase, settings } from "../../../firebase";
import { Formik, Form, Field, FieldArray, ErrorMessage, withFormik } from "formik";
// import * as Yup from "yup";
import Select from "react-select";
import {
Badge,
Button,
Col,
ComponentClass,
Feedback,
FormControl,
FormGroup,
FormLabel,
InputGroup,
Table,
Row,
Container
} from "react-bootstrap";
const style2 = {
paddingTop: "2em"
}
const initialValues = {
title: "",
DataRequests: [],
FundingRequests: [],
createdAt: ''
}
class ProjectForm extends React.Component {
state = {
options: [],
}
async componentDidMount() {
// const fsDB = firebase.firestore(); // Don't worry about this line if it comes from your config.
let options = [];
// await fsDB.collection("abs_for_codes").get().then(function (querySnapshot) {
// querySnapshot.forEach(function(doc) {
// console.log(doc.id, ' => ', doc.data());
// options.push({
// value: doc.data().title.replace(/( )/g, ''),
// label: doc.data().title + ' - ABS ' + doc.id
// });
// });
// });
this.setState({
options
});
}
handleSubmit = (formState, { resetForm }) => {
// Now, you're getting form state here!
const payload = {
...formState,
// resourceOffers: formState.resourceOffers.map(t => t.value),
// ethicsIssue: formState.ethicsIssue.map(t => t.value),
// disclosureStatus: formState.disclosureStatus.value,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
}
console.log("formvalues", payload);
fsDB
.collection("project")
.add(payload)
.then(docRef => {
console.log("docRef>>>", docRef);
resetForm(initialValues);
})
.catch(error => {
console.error("Error adding document: ", error);
});
};
render() {
const { options } = this.state;
return(
<Formik
initialValues={initialValues}
// validationSchema={Yup.object().shape({
// title: Yup.string().required("Give your proposal a title")
// })}
onSubmit={this.handleSubmit}
render={({
errors,
status,
touched,
setFieldValue,
setFieldTouched,
handleSubmit,
isSubmitting,
dirty,
values,
arrayHelpers
}) => {
return (
<div>
<div className="formbox">
<Form>
<Table responsive>
<thead>
<tr>
<th>#</th>
<th>Element</th>
<th>Insights</th>
</tr>
</thead>
</Table>
{/*General*/}
<h5 className="formheading">general</h5>
<Table responsive>
<tbody>
<tr>
<td>1</td>
<td>
<div className="form-group">
<label htmlFor="title">Title</label>
<Field
name="title"
type="text"
className={
"form-control" +
(errors.title && touched.title ? " is-invalid" : "")
}
/>
<ErrorMessage
name="title"
component="div"
className="invalid-feedback"
/>
</div>
</td>
<td className="forminsight">No insights</td>
</tr>
</tbody>
</Table>
{/*Resources s*/}
<h5 className="formheading">resources</h5>
<Table responsive>
<tbody>
</tbody>
</Table>
<h6 className="formheading">Repeatable data form</h6>
<Table responsive>
<tbody>
<tr>
<td>2
</td>
<td>
<label htmlFor="DataRequests">Add a Data Request</label>
<Table responsive>
<tbody>
<tr>
<td>12
</td>
<td><label htmlFor="DataRequests">Add a Funding Request</label>
{/* <FieldArray name="fundingRequests" component={FundingRequests} /> */}
<tr>
<td>
<div>
<FieldArray
name="dataRequests"
component={DataRequests}
render={arrayHelpers => (
<React.Fragment>
{values.repeatable.map((r, i) => (
<div>
<label for={`repeatable.${i}.title`}>Title for {i}</label>
<Field name={`repeatable.${i}.title`} />
<Button
variant="outline-primary"
size="sm"
onClick={() => arrayHelpers.remove({ title: "" })}
>
Remove this Request
</Button>
</div>
))}
<div>
<button
type="button"
onClick={() => arrayHelpers.push({ title: "" })}
>
Add repeatable thing
</button>
</div>
</React.Fragment>
)}
/>
</div>
</td>
</tr>
</td>
<td className="forminsight">No insights</td>
</tr>
</tbody>
</Table>
<div className="form-group">
<Button
variant="outline-primary"
type="submit"
id="ProjectId"
onClick={handleSubmit}
disabled={!dirty || isSubmitting}
>
Save
</Button>
</div>
</Form>
</div>
</div>
);
}}
/>
);
}
}
export default ProjectForm;
ReactDOM.render(<ProjectForm />, document.getElementById("root"));
In the above code, I keep the single form item (for title) from the code sandbox example to show the gist of what I want to do - that line item should be replaced with the form components in the request form, which are as follows:
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: "",
disclosure: ""
};
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" }
];
class DataRequests extends React.Component {
render() {
// Get the parent form props. This is where you should set the form values.
const {form: parentForm, ...parentProps} = this.props;
return (
<Formik
initialValues={initialValues}
render={({ values, form, push, remove, index, setFieldTouched }) => {
return (
<div>
{/* if i uncomment this line, i get loads of type errors saying that values is not defined
{parentForm.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"
onChange={e => {
parentForm.setFieldValue(
`dataRequests.${index}.title`,
e.target.value
);
}}
></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"
onChange={e => {
parentForm.setFieldValue(
`dataRequests.${index}.description`,
e.target.value
);
}}
></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}.dataType`}
className={"react-select-container"}
classNamePrefix="react-select"
value={values.dataTypes}
onChange={({ value: selectedOption }) => {
console.log(selectedOption);
// Setting field value - name of the field and values chosen.
parentForm.setFieldValue(
`dataRequests.${index}.dataType`,
selectedOption
);
}}
onBlur={setFieldTouched}
options={dataTypes}
/>
</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"
onChange={e => {
parentForm.setFieldValue(
`dataRequests.${index}.source`,
e.target.value
);
}}
></Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsDisclosure">
Do you anticipate disclosing this data?
</label>
<Field
name={`dataRequests.${index}.disclosure`}
component="textarea"
rows="10"
placeholder="Describe uses which may involve disclosure"
className="form-control"
onChange={e => {
parentForm.setFieldValue(
`dataRequests.${index}.disclosure`,
e.target.value
);
}}
></Field>
</div>
</td>
</tr>
<tr>
<Button
variant="link"
size="sm"
onClick={() => parentProps.remove(index)}
>
Remove this Data Request
</Button>
</tr>
</tbody>
</Table>
</div>
{/* );}) } */}
<Button
variant="outline-primary"
size="sm"
onClick={() => parentProps.push(initialValues)}
>
Add Data Request
</Button>
</div>
);
}}
/>
);
}
}
export default DataRequests;
So, as you can see, it's all a big jumbled mess. This is the product of attempting to solve this EVERY day since May of this year. I'm clearly not clever but I've run out of ideas for where to look next to try to learn.
I tried to amend the Formik code to use a number of form fields inside the repeatable FieldArray as follows. In doing so, I removed the 'repeatable' statement from the map function, but this causes an error that I don't know how to debug.
const initialValues = {
title: "",
type: "",
identifier: "",
proposedUse: "",
relatedRights: ""
};
<Formik
initialValues={initialValues}
render={({ values }) => (
<Form>
<FieldArray
render={arrayHelpers => (
<React.Fragment>
{values.map((r, i) => (
<div>
<label for={`repeatable.${i}.title`}>Title</label>
<Field name={`repeatable.${i}.title`} />
<label for={`repeatable.${i}.classification`}>Classification</label>
<Field name={`repeatable.${i}.classification`} />
<label for={`repeatable.${i}.identifier`}>Identifier</label>
<Field name={`repeatable.${i}.identifier`} />
<label for={`repeatable.${i}.proposedUse`}>Proposed Use</label>
<Field name={`repeatable.${i}.proposedUse`} />
<label for={`repeatable.${i}.relatedRights`}>Related Rights</label>
<Field name={`repeatable.${i}.relatedRights`} />
<div>
<button
type="button"
onClick={() => arrayHelpers.remove({ values })}
>
Remove repeatable thing
</button>
</div>
</div>
))}
<div>
<button
type="button"
onClick={() => arrayHelpers.push({ values })}
>
Add repeatable thing
</button>
</div>
</React.Fragment>
)}
/>
</Form>
)}
/>
Any advice on how to solve this problem would be gratefully received.
Upvotes: 2
Views: 8600
Reputation: 374
This is how your base file will look like. Import your DataRequests component into the base formik file here.
import React from 'react'
import { Form, Formik, FieldArray } from 'formik'
import DataRequests from './components/DataRequests'
function App() {
return (
<Formik
onSubmit={values => console.log(values)}
initialValues={{ dataRequests: [] }}
render={({ values, setFieldValue }) => (
<Form>
<FieldArray
name='dataRequests'
render={arrayHelpers => (
<>
{values.dataRequests.map((data, i) => (
<DataRequests
setFieldValue={setFieldValue}
arrayHelpers={arrayHelpers}
values={values}
data={data}
key={i}
index={i}
/>
))}
<div>
<button
type='button'
onClick={() =>
arrayHelpers.push({
dataType: '',
title: '',
description: '',
source: '',
disclosure: ''
})
}
>
Add data request
</button>
</div>
</>
)}
/>
<button type='submit'>Submit</button>
</Form>
)}
/>
)
}
export default App
This is how you will want to structure your DataRequests component
import React from 'react'
import { Field } from 'formik'
import Select from 'react-select'
import { Table, Button } from 'react-bootstrap'
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' }
]
class DataRequests extends React.Component {
render() {
const { index, values, setFieldValue, arrayHelpers } = this.props
return (
<div>
<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='dataType'>
Are you looking for primary (raw) data or secondary data?
</label>
<Select
key={`my_unique_select_keydataType`}
name={`dataRequests.${index}.dataType`}
className={'react-select-container'}
classNamePrefix='react-select'
value={values.dataTypes}
onChange={({ value: selectedOption }) => {
console.log(selectedOption)
setFieldValue(
`dataRequests.${index}.dataType`,
selectedOption
)
}}
options={dataTypes}
/>
</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='dataRequestsDisclosure'>
Do you anticipate disclosing this data?
</label>
<Field
name={`dataRequests.${index}.disclosure`}
component='textarea'
rows='10'
placeholder='Describe uses which may involve disclosure'
className='form-control'
></Field>
</div>
</td>
</tr>
</tbody>
</Table>
<Button
variant='outline-primary'
size='sm'
onClick={() => arrayHelpers.remove(index)}
>
Remove this data request
</Button>
</div>
)
}
}
export default DataRequests
Created a codesandbox here
Upvotes: 3