Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependent Dropdowns and Dynamic Fields #751

Closed
najicham opened this issue Jul 10, 2018 · 18 comments
Closed

Dependent Dropdowns and Dynamic Fields #751

najicham opened this issue Jul 10, 2018 · 18 comments
Labels

Comments

@najicham
Copy link

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!

@SimpleCookie
Copy link

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.

@SimpleCookie
Copy link

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.

@pavanmehta91
Copy link

@SimpleCookie Can you give an example to show this?

@SimpleCookie
Copy link

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

@lwilliams-oats
Copy link

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>

@Chris-Boe
Copy link

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

@stale
Copy link

stale bot commented Dec 10, 2018

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.

@stale stale bot added the stale label Dec 10, 2018
@stale
Copy link

stale bot commented Dec 17, 2018

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

@stale stale bot closed this as completed Dec 17, 2018
@ajitfawade
Copy link

ajitfawade commented Apr 9, 2019

@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?

@dzg
Copy link

dzg commented Apr 16, 2019

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

@seanconnollydev
Copy link

seanconnollydev commented Apr 25, 2019

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.

@malyzeli
Copy link

@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!

@seanconnollydev
Copy link

Super cool @malyzeli! I was unaware of the connect and getIn helpers ❤️

@malyzeli
Copy link

malyzeli commented May 17, 2019

@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! :-)

@voliva
Copy link

voliva commented Jun 5, 2019

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

@ltfschoen
Copy link

ltfschoen commented Jan 9, 2020

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}
        ...
    />
}

@Maskedman99
Copy link

Found a solution here
https://stackoverflow.com/questions/60517777/how-can-i-create-connected-dependent-select-elements-in-formik

The code sandbox in the solution
https://codesandbox.io/embed/formik-example-3jfxh?fontsize=14&hidenavigation=1&theme=dark

@richAtreides
Copy link

Is name the field to which it is connected? trying to piece this together. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests