liam
liam

Reputation: 199

unable to setFieldValue in FieldArray using Formik

thanks for reading this.

I am trying to build a form using Formik. And it includes a FieldArray inside a FieldArray.

For some reason, setFieldValue works as I can console.log the correct e.target.name and e.target.value

The problem is upon submitting the form, all the values from the inputs are not where they supposed to be. The expected behavior is the values should be inside of exclusionGroups, not outside.

Would anyone be able to give me some insight ? I've been stuck on this the whole day and feels like my head is going to explode.

Expected:

exclusionGroups: [
  {
    exclusion: [
      {
        param: 'delayMin',
        operator: '<',
        value: 'test1',
      },
      {
        param: 'airlineCode',
        operator: '>',
        value: 'test2',
      },
    ],
  },
  {
    exclusion: [
      {
        param: 'flightNumber',
        operator: '=',
        value: 'test3',
      },
    ],
  },
],

Reality:

exclusionGroups: (2) [{…}, {…}]
group-1-operator-1: "<"
group-1-operator-2: ">"
group-1-param-1: "delayMin"
group-1-param-2: "airlineCode"
group-1-value-1: "test1"
group-1-value-2: "test2"
group-2-operator-1: "="
group-2-param-1: "flightNumber"
group-2-value-1: "test3"

My Code:

Index.tsx

type ExclusionRuleValues = {
  param: string;
  operator: string;
  value: string;
};

interface ExclusionGroupProps {
  exclusionRules?: ExclusionRuleValues[];
}

const Exclusion = ({ data, type }: any) => {
  const onSubmit = (values: any) => {
    console.log(values);
  };

  const initialValues = {
    exclusionGroups: [
      {
        exclusion: [
          {
            param: 'group1',
            operator: 'group1',
            value: 'group1',
          },
        ],
      },
      {
        exclusion: [
          {
            param: 'group2',
            operator: 'group2',
            value: 'group2',
          },
        ],
      },
    ],
  };

  const emptyGroup = {
    exclusion: [
      {
        param: '',
        operator: '',
        value: '',
      },
    ],
  };

  return (
    <React.Fragment>
      <Formik
        enableReinitialize
        onSubmit={onSubmit}
        initialValues={initialValues}
        render={(formProps) => {
          const { values, handleSubmit, submitForm } = formProps;

          const { exclusionGroups } = values;

          const setFieldValue = (e: ChangeEvent<HTMLInputElement>) => {
            return formProps.setFieldValue(e.target.name, e.target.value);
          };

          return (
            <React.Fragment>
              <Header>
                Model will return excluded message code if the following condition is true.
                <Button onClick={submitForm} color="primary">
                  Save Changes
                </Button>
              </Header>

              <Form onSubmit={handleSubmit}>
                <FieldArray
                  name="exclusionGroups"
                  render={(arrayHelper) => (
                    <React.Fragment>
                      {exclusionGroups.map((exclusionRulesGroup: any, index: number) => {
                        return (
                          <TargetFields
                            type={type}
                            group={index + 1}
                            key={`field-${index}`}
                            name={`${arrayHelper.name}.${index}`}
                            setFieldValue={setFieldValue}
                          />
                        );
                      })}
                      <AddNewGroupButton type="button" onClick={() => arrayHelper.push(emptyGroup)}>
                        + New Group
                      </AddNewGroupButton>
                    </React.Fragment>
                  )}
                />
              </Form>
            </React.Fragment>
          );
        }}
      />{' '}
    </React.Fragment>
  );
};

export default Exclusion;

TargetFields.tsx

interface TargetFieldsProps {
  group: number;
  name: string;
  type: string;
  data?: ExclusionRuleValues[];
  setFieldValue: (e: ChangeEvent<HTMLInputElement>) => void;
}

const TargetFields = ({ group, data, name, type, setFieldValue }: TargetFieldsProps) => {
  const emptyTarget = {
    param: '',
    operator: '',
    value: '',
  };

  return (
    <React.Fragment>
      <Field name={name}>
        {(fieldProps: any) => (
          <React.Fragment>
            <ExclusionGroupHeader>
              <b>Group {group}</b>
            </ExclusionGroupHeader>

            <Wrapper>
              <FieldArray
                name={`${fieldProps.field.name}.exclusion`}
                key={`exclusion-${group}`}
                render={(targetHelper) => (
                  <React.Fragment>
                    {fieldProps.field.value.exclusion.map(
                      (target: ExclusionRuleValues, key: number) => {
                        const { param, operator, value } = target;
                        return (
                          <ExclusionRuleGroup key={`group-${key}`}>
                            <ExclusionRuleHeader>
                              <b>Target {key + 1}</b>
                              <DeleteButton type="button" onClick={() => targetHelper.remove(key)}>
                                remove
                              </DeleteButton>
                            </ExclusionRuleHeader>
                            <StyledRow>
                              <CCol sm="4">
                                <Select
                                  onChange={setFieldValue}
                                  // value={param}
                                  label="Params"
                                  name={`group-${group}-param-${key + 1}`}
                                  options={
                                    type === 'input' ? InputExclusionParams : OutputExclusionParams
                                  }
                                  placeholder="Operator"
                                />
                              </CCol>
                              <CCol sm="4">
                                <Select
                                  onChange={setFieldValue}
                                  // value={operator}
                                  label="Operator"
                                  name={`group-${group}-operator-${key + 1}`}
                                  options={SelectOptions}
                                  placeholder="Operator"
                                />
                              </CCol>
                              <CCol sm="4">
                                <Input
                                  onChange={setFieldValue}
                                  // value={value}
                                  label="Value"
                                  name={`group-${group}-value-${key + 1}`}
                                  type="text"
                                  placeholder="Value"
                                />
                              </CCol>
                            </StyledRow>
                          </ExclusionRuleGroup>
                        );
                      }
                    )}
                    <AddNewRuleButton type="button" onClick={() => targetHelper.push(emptyTarget)}>
                      + New Rule
                    </AddNewRuleButton>
                  </React.Fragment>
                )}
              />
            </Wrapper>
          </React.Fragment>
        )}
      </Field>
    </React.Fragment>
  );
};

export default TargetFields;

Upvotes: 4

Views: 11266

Answers (1)

Vencovsky
Vencovsky

Reputation: 31693

The problem is when you pass the name property to your form's inputs.

In the component TargetFields you are passing the name like name={`group-${group}-operator-${key + 1}`} so formik think you want to store that value in the property that will result from that string, e.g. group-1-operator-1 and that is why it's going of the object you want, but going in a property with that name.

If you want to use nested objects / arrays, you need to concatenate the name with object with want using . or [${index}], just like you did in here

<TargetFields
    type={type}
    group={index + 1}
    key={`field-${index}`}
    name={`${arrayHelper.name}.${index}`}  // You use the . with the name and the index. 
    setFieldValue={setFieldValue}          // It could also be using [] instead of . like this => `${arrayHelper.name}[${index}]`
/>

and like here

<FieldArray
    name={`${fieldProps.field.name}.exclusion`} // You use the . with the name and the property of the object you want
    key={`exclusion-${group}`}
    render={(targetHelper) => ( ... )
/>

So to solve you problem, you need to change the following.

<StyledRow>
  <CCol sm="4">
    <Select
      onChange={setFieldValue}
      // value={param}
      label="Params"
      name={`${targetHelper}[${key}].param`}
      options={
        type === 'input' ? InputExclusionParams : OutputExclusionParams
      }
      placeholder="Operator"
    />
  </CCol>
  <CCol sm="4">
    <Select
      onChange={setFieldValue}
      // value={operator}
      label="Operator"
      name={`${targetHelper}[${key}].operator`}
      options={SelectOptions}
      placeholder="Operator"
    />
  </CCol>
  <CCol sm="4">
    <Input
      onChange={setFieldValue}
      // value={value}
      label="Value"
      name={`${targetHelper}[${key}].value`}
      type="text"
      placeholder="Value"
    />
  </CCol>
</StyledRow>

And just a guess, you didn't code all that by your self right? Because in one place you are doing exactly what you need to do to solve your problem. If you don't know how that name thing works with FieldArray, I recommend you reading this part of the formik docs. Know how this works is very important for using nested objects / arrays.

Upvotes: 4

Related Questions