0

I'm currently trying to create a dynamic select/input component where you can choose values from select options or type your own value inside an input field by selecting the "other" select option.

Right now I get stuck by updating the form data equally to the value of the selected option / input value. The Form Data Value always persist on the initial / default value.

App.js

...

export default function App() {
  const methods = useForm({});
  const { handleSubmit } = methods;

  const customSalutationOptions = [
    { title: "Not specified", value: "null" },
    { title: "Male", value: "male" },
    { title: "Female", value: "female" }
  ];

  const defaultValues = {
    salutation: "null"
  };

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <div className="App">
      <FormProvider {...methods}>
        <form onSubmit={handleSubmit(onSubmit)}>
          <SelectOrInput
            variant="outlined"
            name={`contactPerson[0].salutation`}
            defaultValue={defaultValues}
            selectOptions={customSalutationOptions}
          />
          <Button type="submit" color="primary" fullWidth variant="contained">
            Submit
          </Button>
        </form>
      </FormProvider>
    </div>
  );
}

components/SelectOrInput.tsx

...

type Props = {
  name: string;
  label: string;
  selectOptions: [{ title: string; value: string }];
  defaultValue: any;
  shouldUnregister: boolean;
  variant: "filled" | "outlined" | "standard";
};

export default function SelectOrInput({
  name,
  label,
  selectOptions,
  defaultValue,
  shouldUnregister,
  variant
}: Props) {
  const classes = useStyles();
  const { control } = useFormContext();
  const [showCustomInput, setShowCustomInput] = useState(false);
  const [value, setValue] = useState(selectOptions[0].value);

  const additionalInput = [{ title: "Other", value: "" }];

  const combindedOptions = selectOptions.concat(additionalInput);

  const handleInputSelectChange = (
    event: React.ChangeEvent<{ value: unknown }>
  ): void => {
    const value = event.target.value as string;
    if (value === "") {
      const newState = !showCustomInput;
      setShowCustomInput(newState);
      console.log(value);
      setValue(value);
    } else {
      setValue(value);
    }
  };

  const resetCustomInputToSelect = (event: React.MouseEvent<HTMLElement>) => {
    const newState = !showCustomInput;
    setValue(combindedOptions[0].value);
    setShowCustomInput(newState);
  };

  return (
    <>
      {showCustomInput ? (
        <FormControl className={classes.input}>
          <Controller
            name={name}
            control={control}
            shouldUnregister={shouldUnregister}
            render={({ field }) => (
              <TextField
                {...field}
                label={label}
                InputLabelProps={{ shrink: true }}
                variant={variant}
                placeholder="Other..."
                autoFocus
                type="text"
                onChange={handleInputSelectChange}
                value={value}
                InputProps={{
                  endAdornment: (
                    <InputAdornment position="end">
                      <IconButton
                        size="small"
                        onClick={resetCustomInputToSelect}
                        id="custominput-closebutton"
                      >
                        <CloseIcon fontSize="small" />
                      </IconButton>
                    </InputAdornment>
                  )
                }}
              ></TextField>
            )}
          />
        </FormControl>
      ) : (
        <FormControl className={classes.input} variant={variant}>
          <InputLabel id={`label-select-${label}`}>{label}</InputLabel>
          <Controller
            name={name}
            defaultValue={defaultValue}
            control={control}
            shouldUnregister={shouldUnregister}
            render={({ field }) => (
              <Select
                {...field}
                label={label}
                labelId={`label-select-${label}`}
                value={value}
                MenuProps={{
                  anchorOrigin: {
                    vertical: "bottom",
                    horizontal: "left"
                  },
                  getContentAnchorEl: null
                }}
                onChange={handleInputSelectChange}
              >
                {combindedOptions.map((option, index) => (
                  <MenuItem key={option.title} value={`${option.value}`}>
                    {option.title}
                  </MenuItem>
                ))}
              </Select>
            )}
          />
        </FormControl>
      )}
    </>
  );
}

...

To give a better example I provided a CSB:

Edit Dynamic Input / Select

K.Kröger
  • 97
  • 10

2 Answers2

1

You are storing value in it's own state of SelectOrInput component. You need to lift state up to parent component in order to get value in parent.

  1. Create state in parent component and initialize with default value and create function to change it's value
  const [inputValue, setInputValue] = useState(null);

  const onChange = (value) => {
    setInputValue(value);
  };
  1. Pass onChange function in SelectOrInput component and call onChange function whenever value is changed
<SelectOrInput
  ...
  onChange={onChange}
/>

// call onChange in handleInputSelectChange method

  const handleInputSelectChange = (
    event: React.ChangeEvent<{ value: unknown }>
  ): void => {
    const value = event.target.value as string;
    if (value === "") {
      const newState = !showCustomInput;
      setShowCustomInput(newState);

      setValue(value);
      onChange(value);  
    } else {
      setValue(value);
      onChange(value);
    }
  };

Working example: https://codesandbox.io/s/dynamic-input-select-wk2je

Priyank Kachhela
  • 2,517
  • 8
  • 16
  • Thanks you this looks perfect. Could you maybe for learning reasons try to explain to me, why it is wrong to store the value in its own state inside `SelectOrInput`. As far as I understood, lifting the state is used to share existing data across multiple components to to reflect the same changing data. But how does this correspond in my case ? – K.Kröger May 11 '21 at 12:26
  • It's totally fine to use own states of components but if you want some data in parent component from child component than you should define state in parent component and pass function to change that state in child component. And here you needed to use value from child component so that's the reason to store value in parent on change of value in child component. – Priyank Kachhela May 11 '21 at 12:31
  • One thought on this Approach though; Is this solution not redundant, because we now have and manage a state inside the `SelectOrInput ` as well as inside the `App.js` component ? If I'm not mistaking – K.Kröger May 11 '21 at 14:11
  • Yes, i did not remove the state from SelectOrInput component. If you want, you can replace it with value from parent component. – Priyank Kachhela May 11 '21 at 14:16
0

With the great help of @Priyank Kachhela, I was able to find out the answer.

By Lifting the State to it's closest common ancestor as well as removing any Controller Component inside the child component.

App.js

  1. Create state in parent component and initialize with default value and create function to change it's value
 const [inputValue, setInputValue] = useState("null");

 const onSubmit = (data) => {
    // Stringify Object to always see real value, not the value evaluated upon first expanding.
    // https://stackoverflow.com/questions/23429203/weird-behavior-with-objects-console-log
    console.log(JSON.stringify(data, 4));
  };

  const onChange = (value) => {
    setInputValue(value);
  };
  1. Wrap SelectOrInput with Controller and Pass onChange function, value as well as defaultValue to the Controller. Then use the render method and spread field on SelectOrInput Component.

<Controller
  name={`contactPerson[0].salutation`}
  defaultValue={defaultValues.salutation}
  onChange={onChange}
  value={inputValue}
  control={control}
  render={({ field }) => (
    <SelectOrInput
     {...field}
     variant="outlined"
     selectOptions={customSalutationOptions}
     />
  )}
/>

components/SelectOrInput.js

  1. Bubble / (Call) onChange Event Handler whenever value is changed from within the Child-(SelectOrInput) Component.
const handleInputSelectChange = (
    event: React.ChangeEvent<{ value: unknown }>
  ): void => {
    const value = event.target.value as string;
    if (value === "") {
      const newState = !showCustomInput;
      setShowCustomInput(newState);
      // Bubble / (Call) Event
      onChange(value);
    } else {
      onChange(value);
    }
  };

  const resetCustomInputToSelect = (event: React.MouseEvent<HTMLElement>) => {
    const newState = !showCustomInput;
    // Bubble / (Call) Event
    onChange("null");
    setShowCustomInput(newState);
  };
  1. Remove component internal Input State from the 'SelectOrInput'

Working Example

Edit Dynamic Input / Select (V.2.0)

Revisions captured inside Gist

https://gist.github.com/kkroeger93/1e4c0fe993f1745a34fb5717ee2ff545/revisions

K.Kröger
  • 97
  • 10