I am trying to figure out how to use React with Formik's Field Arrays to embed repeatable form components in my form.
I have a system working (I think). It's a combination of suggestions sourced from 10+ sources, so I'm starting to think I am off the plantation because there must be an obvious way to set this up (and I just can't figure it out).
At the moment, my current approach is generating a warning that:
A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/docs/forms.html#controlled-components
My code sandbox is here: https://codesandbox.io/s/goofy-glade-lx65p?from-embed. Code from the sandbox is pasted at the bottom of this post for those who prefer not to use this tool.
You can see that the form renders and the repeatable embedded form can be added and removed. However, the embedded form has an error that says: this.props.formik.registerField is not a function. (In 'this.props.formik.registerField(this.props.name, this)', 'this.props.formik.registerField' is undefined)
I don't know what this means. I don't use something called registerField anywhere. Am I supposed to? I have seen this post that describes setting the initial state to get around this warning, but if I do that in the requests form, then I get a blank form displayed on the main form (I don't want that, I just want the link to embed one).
Can anyone see what I'm doing wrong with the state?
Form:
import React from "react";
import { Link } from "react-router-dom";
import {
Formik,
Form,
Field,
FieldArray,
ErrorMessage,
withFormik
} from "formik";
// import * as Yup from "yup";
import Select from "react-select";
// import { fsDB, firebase, settings } from "../../../firebase";
import Requests from "./Requests";
import {
Badge,
Button,
Col,
ComponentClass,
Feedback,
FormControl,
FormGroup,
FormLabel,
InputGroup,
Table,
Row,
Container
} from "react-bootstrap";
const style2 = {
paddingTop: "2em"
};
const initialValues = {
title: "",
Requests: [],
createdAt: ""
};
class MainForm 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,
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}
onSubmit={this.handleSubmit}
render={({
errors,
status,
touched,
setFieldValue,
setFieldTouched,
handleSubmit,
isSubmitting,
dirty,
values,
arrayHelers
}) => {
return (
<div>
<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>
{/*Method*/}
{/* <SoPTip /> */}
{/*Resources s*/}
<Table responsive>
<tbody>
<tr>
<td>
<label htmlFor="Requests">Request</label>
<FieldArray
name="Requests"
render={arrayHelpers => (
<React.Fragment>
{values.Requests.map((funding, i) => (
<Requests
setFieldValue={setFieldValue}
arrayHelpers={arrayHelpers}
values={values}
data={funding}
key={i}
index={i}
/>
))}
<div>
<Button
variant="link"
size="sm"
onClick={() =>
arrayHelpers.push({
title: "",
description: "",
source: "",
disclosure: "",
conflicts: ""
})
}
>
Add request
</Button>
</div>
</React.Fragment>
)}
/>
</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}
onSubmit={values => console.log(values)}
>
Save
</Button>
</div>
</Form>
</div>
);
}}
/>
);
}
}
export default MainForm;
Request repeatable form:
import React from "react";
import { Field } from "formik";
import Select from "react-select";
import { Table, Button } from "react-bootstrap";
const Currencies = [
{ value: "usd", label: "United States Dollars (USD)" },
{ value: "nzd", label: "New Zealnd Dollars (NZD)" },
{ value: "gbp", label: "Pounds Sterling (GBP)" },
{ value: "cad", label: "Canadian Dollars (CAD)" }
];
class Requests extends React.Component {
render() {
const {
index,
values,
setFieldValue,
setFieldTouched,
arrayHelpers,
remove
} = this.props;
return (
<div>
<Table responsive>
<tbody>
<tr>
<td>
<div className="form-group">
<label htmlFor="RequestsTitle">Title</label>
<Field
name={`Requests.${index}.title`}
placeholder="Add a title"
className="form-control"
/>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="RequestsSource">Source</label>
<Field
name={`Requests.${index}.source`}
component="textarea"
rows="10"
placeholder="Leave blank and skip ahead if you don't"
className="form-control"
/>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="Currency">Select your currency</label>
<Select
key={`my_unique_select_keyCurrency`}
name={`Requests.${index}.Currency`}
className={"react-select-container"}
classNamePrefix="react-select"
onChange={({ value: selectedOption }) => {
console.log(selectedOption);
setFieldValue(
`Requests.${index}.Currencies`,
selectedOption
);
}}
onBlur={setFieldTouched}
options={Currencies}
/>
</div>
</td>
<td className="forminsight">No insights</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="RequestsAmount">
What amount of funding are you seeking?
</label>
<Field
name={`Requests.${index}.amount`}
type="text"
placeholder="Enter a number without a currency symbol"
className={"form-control"}
/>
</div>
</td>
<td className="forminsight">No insights</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="RequestsDisclosure">Disclosure</label>
<Field
name={`Requests.${index}.disclosure`}
component="textarea"
rows="10"
placeholder="Consider."
className="form-control"
/>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="RequetsConflicts">Conflicts</label>
<Field
name={`Requests.${index}.conflicts`}
component="textarea"
rows="10"
placeholder="Take a look at the checklist. "
className="form-control"
/>
</div>
</td>
<td className="forminsight">No insights</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="RequestsBudget">
Do you have a budget for how this funding will be deployed?
</label>
<Field
name={`Requests.${index}.budget`}
component="textarea"
rows="10"
placeholder="Leave blank if you don't."
className="form-control"
/>
</div>
</td>
<td className="forminsight">No insights</td>
</tr>
</tbody>
</Table>
<Button
variant="outline-secondary"
size="sm"
onClick={() => arrayHelpers.remove(index)}
>
Remove this request
</Button>
</div>
);
}
}
export default Requests;