Formik: Dependent Dropdowns and Dynamic Fields

Created on 10 Jul 2018  路  17Comments  路  Source: formium/formik

Hello! I have read through many of the issues and online examples but I have not found an example of using dynamic fields.

I would like to add or remove form fields based on the value of one of the dropdowns. If the dropdown has "option A" selected, I would like to add two more dropdowns and populate them via an AJAX call. If "option B" is selected, I would like to add an input box and remove the dropdowns displayed for "option A".

I tried using a custom react component but it is not clear how to add or remove form elements based on the value of another form element. Thanks!

stale

Most helpful comment

@jaredpalmer Do you have any recommendations for dynamic and dependent fields?

All 17 comments

I am looking into the exact same thing right now. Still haven't found any answer.
I can't and shouldn't do setField inside render, otherwise that would be an option for me. But it's a major anti-pattern.

Okay @parlays I have solved the issue. It's not a pretty solution but it's a workaround.
I have a custom component which obviously calls setFieldValue, but it also calls a onValueChangeCallback to my container/page, which in turn updates the "options" prop to my form based on the option chosen.

To update the dynamic field's value, I am also calling setFieldValue on the onChange for my static field.

I'm sorry, I know my explanation is poor and hard to follow. My code and form are quite complex so it's difficult to give you a simple explanation.

But basically, what you want to do is to handle it through a callback I guess.

@SimpleCookie Can you give an example to show this?

@pavanmehta91 I'm sorry I don't have the possibility of creating an example for you right now.
But .. Say you have 2 dropdowns, parent A and dependant child B.

When you change A, you want to update the options of B accordingly.
What you need to do is, you need to first use setfieldValue to set value of A, and then you need to update the options of B, I did this using a callback. And then you also need to use setfieldValue to set the initial value of B.

Found this issue while trying to solve a similar problem. The pattern I've ended up using is to pass in the field and form as props to the child component, and use the componentDidUpdate lifecycle method to detect changes in the parent value. E.g.:

class ChildInput extends React.Component {
  componentDidUpdate(prevProps) {
    const { field, form: { values, setFieldValue } } = this.props;
    if (values.parent !== prevProps.form.values.parent) {
      setFieldValue(field.name, `some function of ${values.parent}`);
    }
  }

  render() {
    const { field } = this.props;
    return <input {...field}/>;
  }
}

...

<Field name="child">
  {({ field, form }) => (
    <ChildInput field={field} form={form}/>
  )}
</Field>

@jaredpalmer Do you have any recommendations for dynamic and dependent fields?

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

ProBot automatically closed this due to inactivity. Holler if this is a mistake, and we'll re-open it.

@pavanmehta91 I'm sorry I don't have the possibility of creating an example for you right now.
But .. Say you have 2 dropdowns, parent A and dependant child B.

When you change A, you want to update the options of B accordingly.
What you need to do is, you need to first use setfieldValue to set value of A, and then you need to update the options of B, I did this using a callback. And then you also need to use setfieldValue to set the initial value of B.

Could you please provide an example on codesandbox for the above case?

Any word on this? What's the most elegant way to solve?

I came across this looking for a way to set default fields in Formik's values when a conditional set of form fields is rendered. This is a simplified version of what my code looks like:

const ConditionalFields = (props) => {
  const { handleChange, setFieldValue, values } = props;

  useEffect(() => {
    // If my values are undefined, set them in Formik
    if (!values.myValue) {
      setFieldValue('myValue', 'default');
    }
  });

  // myValue is not defined so stop rendering, useEffect above will cause a re-render if necessary
  if (!values.myValue) return null;

  return (
    <div>
      <input type="text" value={values.myValue} onChange={handleChange} />
    </div>
  );
};

The premise of this solution is that you halt rendering when you encounter uninitialized fields, set field values in Formik's store, which then triggers a re-render. Not elegant, but could get you out of a bind.

@goldenshun Thanks for tip, I updated your solution a bit and here is my approach...

  • using connect helper to hook into form context instead of passing props explicitly
  • using getIn helper to support nested field name syntax
  • delegates rendering onto child Field component so it should be interchangeable

Component code:

import { connect, Field, getIn } from "formik";
import { any, object, string } from "prop-types";
import React, { useEffect } from "react";

const OptionalField = ({
  formik: { values, setFieldValue },
  name,
  defaultValue,
  ...props
}) => {
  useEffect(() => {
    // set value if not defined
    if (getIn(values, name) === undefined) {
      setFieldValue(name, defaultValue);
    }
  });

  const value = getIn(values, name);

  // stop rendering, effect above will rerender
  if (value === undefined) return null;

  return <Field name={name} {...props} />;
};

OptionalField.propTypes = {
  formik: object.isRequired,
  name: string.isRequired,
  defaultValue: any,
};

OptionalField.defaultProps = {
  defaultValue: null,
};

export default connect(OptionalField);

Example usage:

<OptionalField name="foo" defaultValue="bar" />

<OptionalField name="a.b" defaultValue={1} />

<OptionalField name="list" defaultValue={[1, 2, 3]} />

<OptionalField name="set[0]" defaultValue={true} />

<OptionalField name="set[1].value" defaultValue={{ a: 1, b: 2 }} />

Enjoy!

Super cool @malyzeli! I was unaware of the connect and getIn helpers 鉂わ笍

@goldenshun Yep, they are not documented well.. I found it by accident while browsing through the source code! :-D

@jaredpalmer Can you add page to API Reference listing available helpers?
Maybe it's sufficient mentioning just getIn, since it's useful in situations like this. I see other helpers being used internally, though I'm not sure if they are reusable for some other purpose...
I believe you know better, so please give us some hints and help us build upon that!

Also what about using GitHub wiki for sharing various community snippets like this OptionalField, or for example Checkbox/Radio components, which are discussed in other issue threads? Then publish link on the website so people could find custom components, examples, tutorials and tips easier.
Formik ecosystem will grow! :-)

I've made a solution inspired on @SimpleCookie: I created a component called FieldSpy:

import { Field, FieldProps } from 'formik';
import * as React from 'react';
import { useRef } from 'react';

interface FieldSpyProps {
    name: string;
    onChange: (value: any) => void;
}

const empty = Symbol('empty');
export const FieldSpy = ({ name, onChange }: FieldSpyProps) => {
    const value = useRef<any>(empty);

    return (
        <Field name={name}>
            {({ field }: FieldProps) => {
                if (value.current !== empty && value.current !== field.value) {
                    onChange(field.value);
                }
                value.current = field.value;

                return null;
            }}
        </Field>
    );
};

So now, whenever I want to "listen" to changes on a particular field in my form container, I can just:

<Formik initialValues={defaultValue} onSubmit={onSubmit}>
    <Form>
        <TextField name="username" />
        <FieldSpy name="username" onChange={onUsernameChange} />
    </Form>
</Formik>

What's your opinion on this? Does this look right? I'm quite new to Formik and I haven't fully tried this, but it seems like it should cover most of the use cases.
The only problem though is that I can't make the call trigger on blur....

I'd like to share how I solved a similar situation.

I had two selection box fields A and B using Formik.
Both A and B call a method that is passed to prop getOptions and list data that is retrieved
I conditionally rendered B only when the user selected an option in A (via onNameSelect) that returned
a value and stored it in this.state.object.nameID.
Each time I conditionally rendered B, in onNameSelect I reset selection box B so no value was chosen
(since each different Name may have a different list of Profiles available).
And each time a different option in A is chosen (which causes B to be conditionally be rendered),
it runs setFieldValue={() => setFieldValue("object.profileID", "0", false)}, which
appears to re-render selection box B, and triggers a call to getProfileOptions and returns a
list of profiles in the selection box B that correspond to the current this.state.object.nameID.

...
getNameOptions = (search, callbackFunc) => {
    NameStore.list((999), resp => {
        const options = resp.result.map((n, i) => { return { label: n.name, value: n.id } });
        callbackFunc(options);
    });
}

getProfileOptions = (search, callbackFunc) => {
    // Only fetch Profiles associated with the Name that the user must have chosen first.
    if (this.state.object === undefined || this.state.object.nameID === undefined) {
        callbackFunc([]);
        return;
    }

    ProfileStore.list(this.state.object.nameID, resp => {
        const options = resp.result.map((p, i) => { return { label: p.name, value: p.id } });
        this.setState({ loading: false });
        callbackFunc(options);
    });
}

onNameSelect = (inputValue) => {
    if (!this.state.object.nameID !== inputValue.id)) {
        let object = this.state.object;
        // Reset profileID when nameID changes
        object.profileID = null;
        object.nameID = v.value;
        this.setState({
            object,
        });
    }
}

onProfileSelect = (inputValue) => {
    if (this.state.object.profileID !== inputValue.id)) {
        let object = this.state.object;
        object.profileID = v.value;
        this.setState({
            object
        });
    }
}

...

<Field
    id="nameID"
    name="object.nameID"
    type="text"
    value={values.object.nameID}
    onChange={this.onNameChange}
    getOptions={this.getNameOptions}
    // Hack: trigger Profile ID list to refresh
    // whenever the Name ID changes
    setFieldValue={() => setFieldValue("object.profileID", "0", false)}
    ...
/>

{values.object.nameID &&
    <Field
        id="profileID"
        name="object.profileID"
        type="text"
        value={values.object.profileID}
        onChange={this.onProfileSelect}
        getOptions={this.getProfileOptions}
        ...
    />
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jaredpalmer picture jaredpalmer  路  3Comments

jordantrainor picture jordantrainor  路  3Comments

Jungwoo-An picture Jungwoo-An  路  3Comments

PeerHartmann picture PeerHartmann  路  3Comments

najisawas picture najisawas  路  3Comments