3

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;
Mel
  • 3,699
  • 13
  • 71
  • 194

2 Answers2

7

You cannot add nested forms inside a form element. Please refer the below post for mode details.

Can you nest html forms?

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>
Nithin Thampi
  • 3,001
  • 1
  • 10
  • 9
  • Please can you explain what this line is doing (or show me where to find an explanation)? {form.values.dataRequests.map((_notneeded, index) => { – Mel Oct 14 '19 at 00:09
  • I'm trying to convert the dataRequests form into a stateful class so that I can use react-select on one of the form elements, but I'm getting stuck because my latest attempt can't find the definition of values (which is in my list of rendered functions) – Mel Oct 14 '19 at 00:10
  • I have pasted my current attempt at converting this embedded form to a class above. – Mel Oct 14 '19 at 00:14
  • @Mel I have updated my answer to add more details. Let me know if things are clearer now. Also I have created a REPL with your updated component. Hope this helps...https://repl.it/repls/RichQuickwittedConditional – Nithin Thampi Oct 14 '19 at 12:09
  • thank you so much for taking the time on this. I can't get the repl to run, but I tried your code and it sill throws an error (line 31), with the line: {parentForm.values.dataRequests.map((_notneeded, index) => { . The error message says: TypeError: Cannot read property 'map' of undefined I'm not sure what is supposed to be defined. I tried replacing camelCase on dataRequests with DataRequests, but that didn't solve the problem. Can you see what's left to get this working? – Mel Oct 14 '19 at 23:06
  • Is this line a placeholder? Am I supposed to replace it with something from the parent form? Maybe that's what's stopping the map function from operating? const {form: parentForm, ...parentProps} = this.props; – Mel Oct 14 '19 at 23:09
  • Also, when I comment out the map line and try to test the rest of the form, I get another TypeError that says 'remove is not a function' - pointing to this line: onClick={() => remove(index)}. Is there something more than needs to be done to use these functions (it's possible that values isn't working and that's stopping the map function from running?) – Mel Oct 15 '19 at 00:55
  • Where is 'parentForm' coming from? Is that a function in Formik somewhere? I can't find references to it in the Formik docs (but I am really struggling to learn to code (7 years and still trying to learn every day). What does this line do? const {form: parentForm, ...parentProps} = this.props; – Mel Oct 15 '19 at 01:08
  • Another question: what does this line (specifically, the number 4) from your repl mean?: {this.state.formData ? ( {JSON.stringify(this.state.formData, null, 4)} ) : null} – Mel Oct 15 '19 at 03:22
  • @Mel If REPL doesn't work, try this. https://stackblitz.com/edit/react-amh178?file=index.js – Nithin Thampi Oct 15 '19 at 04:18
  • That works. Thanks. I can't get it to work in my code. Can you point me to where parentForm is defined? Is there something somewhere that defines it as the form in which the DataRequests component is used? I'm wondering if that's blocking the form from working and throwing all of these errors? – Mel Oct 15 '19 at 04:21
  • Also wondering what the number 4 in the stringify statement contributes? Is it a count of the number of questions in the form? – Mel Oct 15 '19 at 04:22
  • index.js does not have a const defining parentForm. Is it somehow incorporated? – Mel Oct 15 '19 at 04:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/200877/discussion-between-nithin-and-mel). – Nithin Thampi Oct 15 '19 at 04:24
  • It has taken me almost a year to realise this, but you were right. I'm sorry for setting aside your work. I realised today that the issue I'm having isn't with the approach you've set out, it's with setting up a submit handler that can send the dataRequest to a firestore sub-collection of the main form. https://stackoverflow.com/questions/63422845/firebase-how-to-submit-form-data-to-different-collections Thank you for your help. – Mel Aug 15 '20 at 23:38
2

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.

Mel
  • 3,699
  • 13
  • 71
  • 194
  • What do you mean by, `it isn't a correct deployment of formik`? You could check the docs at https://jaredpalmer.com/formik/docs/api/fieldarray#component-reactreactnode. And what doesn't work in the solution posted by you? Looks like it already addresses the error you are receiving `TypeError: Cannot read property 'values' of undefined`. It's not clear what you are trying to achieve here. I would suggest you update the question/ add a comment on what you are trying to achieve so that others viewing this post / myself could give a better solution. – Nithin Thampi Oct 22 '19 at 07:28
  • Hi Nithin - you can see that the form of the implementation doesn't follow the docs in a number of areas. I've found a partial solution as I've set out in the code sandbox. When I figure out how it works fully, ill update this post to share the discovery. – Mel Oct 22 '19 at 10:58