Mel
Mel

Reputation: 2715

React, Formik and implementing Repeatable Form Fields (as forms with multiple fields)

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:

  1. Have a parent form that has a button that says, add a data request;

  2. 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

  3. Have a button that says "remove this data request" so that a completed form element can be removed

  4. 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

Answers (1)

MiDas
MiDas

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

Related Questions