0
votes

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

2

2 Answers

1
votes

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

0
votes

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://stackguides.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