2

I'm using react-select and react-final-form for conditional dropdowns, where options for the second select are provided by a <PickOptions/> component based on the value of the first select (thanks to this SO answer).

Here is the component:

/** Changes options and clears field B when field A changes */
const PickOptions = ({ a, b, optionsMap, children }) => {
  const aField = useField(a, { subscription: { value: 1 } });
  const bField = useField(b, { subscription: {} });
  const aValue = aField.input.value.value;
  const changeB = bField.input.onChange;
  const [options, setOptions] = React.useState(optionsMap[aValue]);

  React.useEffect(() => {
    changeB(undefined); // clear B
    setOptions(optionsMap[aValue]);
  }, [aValue, changeB, optionsMap]);
  return children(options || []);
};

It clears the second select when the value of the first one changes by changeB(undefined). I've also set the second select to the first option in an array by passing initialValue. As I need to initialize the values from the state, I ended up with the following code:

initialValue={
  this.state.data.options[index] &&
  this.state.data.options[index].secondOption
    ? this.state.data.options[index]
        .secondOption
    : options.filter(
        option => option.type === "option"
      )[0]
}

But it doesn't work. Initial values from the state are not being passed to the fields rendered by <PickOptions/>. If I delete changeB(undefined) from the component, the values are passed but then the input value of the second select is not updated, when the value of the first select changes (even though the options have been updated). Here is the link to my codesandbox.

How can I fix it?

jupiteror
  • 1,085
  • 3
  • 24
  • 50

2 Answers2

1

I was able to get this to work by taking everything that is mapped by the fields.map() section and wrapping it in it's own component to ensure that each of them have separate states. Then I just put the changeB(undefined) function in the return call of the useEffect hook to clear the secondary selects after the user selects a different option for the first select like so:

React.useEffect(() => {
  setOptions(optionsMap[aValue]);

  return function cleanup() {
    changeB(undefined) // clear B
  };
}, [aValue, changeB, optionsMap]);

You can see how it works in this sandbox: React Final Form - Clear Secondary Selects.

To change the secondary select fields, you will need to pass an extra prop to PickOptions for the type of option the array corresponds to. I also subscribe and keep track of the previous bValue to check if it exists in the current bValueSet array. If it exists, we leave it alone, otherwise we update it with the first value in its corresponding optionType array.

  // subscibe to keep track of previous bValue
  const bFieldSubscription = useField(b, { subscription: { value: 1 } })
  const bValue = bFieldSubscription.input.value.value

  React.useEffect(() => {
    setOptions(optionsMap[aValue]);

    if (optionsMap[aValue]) {
      // set of bValues defined in array
      const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);

      // if the previous bValue does not exist in the current bValueSet then changeB
      if (!bValueSet.some(x => x.value === bValue)) {
        changeB(bValueSet[0]); // change B
      }
    }

  }, [aValue, changeB, optionsMap]);

Here is the sandbox for that method: React Final Form - Update Secondary Selects.

I also changed your class component into a functional because it was easier for me to see and test what was going on but it this method should also work with your class component.

F. Serna
  • 239
  • 2
  • 13
  • Thank you very much for your reply! I will try it out. But I also need to have the second select set to the first option in an array after the change of the first select. This should happen only when the field is changed manually by the user, not when the form is initialized from the state. See the code for `initialValue` in my example above. – jupiteror Nov 15 '19 at 20:33
  • @jupiteror I've modified my answer to address what I think your desired outcome is. Check the second sandbox link. – F. Serna Nov 15 '19 at 21:16
  • Thank you! But the values in the second example are now not initialized( That's the problem, I'm trying to solve. – jupiteror Nov 16 '19 at 06:13
  • @jupiteror I'm sorry about that I must not have saved it right, try the second example again the values should be initialized and change when you move the first option. – F. Serna Nov 16 '19 at 07:11
  • Now there are two more problems. First, the app crashes when you try to add a new option. Second, the value in the second option is always set to the first option in the array and not the one in the state. The second option in the second field should be Two B, but it is Two A. – jupiteror Nov 16 '19 at 07:28
  • The way it should work: field B has a value in the state when the form is initialized → the value from the state is applied. Doesn't have → a first option in an array is applied. A user changes the value of field A → the first option in the array is applied. – jupiteror Nov 16 '19 at 07:30
  • Ok so I fixed the bug with the app crashing when adding a new option and fixed it so that it loads the initial state, look at the updated `PickOptions` component. So I see where I misunderstood before and would value anymore feedback if I'm still not seeing the problem correctly @jupiteror. – F. Serna Nov 16 '19 at 08:23
  • We are almost there But there is one more problem. When you add a new option and change Field A, the app is still crashing. – jupiteror Nov 16 '19 at 08:29
  • @jupiteror I was not able to reproduce your problem until I opened the sandbox in an incognito window so that was probably my fault again for not saving the files correctly. I've updated it and should hopefully be good to go now. – F. Serna Nov 16 '19 at 08:39
  • Now it works, thank you very much! I will try to move this to my project, and if I have any problems I will let you know. I really appreciate your help! – jupiteror Nov 16 '19 at 09:41
  • Based on your answer I ended up with a slightly different code. Please see my answer below. As in my project the values of options in different sets might be the same. So it's better to rely on a fact, if aFiled has been updated by the user or not. – jupiteror Nov 17 '19 at 14:12
0

Based on the previous answer I ended up with the following code in my component:

// subscibe to keep track of aField has been changed
const aFieldSubscription = useField(a, { subscription: { dirty: 1 } });

React.useEffect(() => {
  setOptions(optionsMap[aValue]);

  if (optionsMap[aValue]) {
    // set of bValues defined in array
    const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);

    if (aFieldSubscription.meta.dirty) {
      changeB(bValueSet[0]); // change B
    }
  }
}, [aValue, changeB, optionsMap]);

This way it checks whether the aField has been changed by the user, and if it's true it sets the value of the bField to the first option in an array.

jupiteror
  • 1,085
  • 3
  • 24
  • 50