Home > Net >  Dynamically adding or removing items within a react-hook-forms form and registering the inputs?
Dynamically adding or removing items within a react-hook-forms form and registering the inputs?

Time:10-29

I'm trying to implement a rather complicated form that has and date picker and input which the user can add multiple of (or delete). That data then gets added to the overall form on submit. How do I get react-hook-forms to register that little dynamic faux form within the real form?

Here's the faux form inputs:

          <AddPackagesStyle>
              <Label htmlFor="addedPackages" label="Add Packages" />

              <DatePicker
                id="dateRange"
                selected={startDate}
                selectsRange
                startDate={startDate}
                endDate={endDate}
                placeholderText="select dates"
                onChange={onDateChange}
              />
              <PackageInput
                id="PackageSelect"
                placeholder="Select Package"
                type="text"
                value={name}
                // @ts-ignore
                onChange={(e) =>
                  // @ts-ignore
                  setName(e.target.value)
                }
              />
              <ButtonContainer>
                <button type="button" onClick={clearAll}>
                  Clear
                </button>
                <button
                  type="button"
                  // @ts-ignore
                  onClick={addPackage}
                >
                  Add
                </button>
              </ButtonContainer>
            </AddPackagesStyle>

These entries get added to an array in a useState hook:

 const [addedPackages, setAddedPackages] = useState<any[]>([])

Then this gets rendered in JSX as the add packages:

 <ContentSubscriptionWrapper>
              {addedPackages.length !== 0 &&
                addedPackages.map((addedPackage, idx) => (
                  // @ts-ignore
                  <>
                    <ContentSubscriptionColumn>
                      {addedPackage.name && addedPackage.name}
                    </ContentSubscriptionColumn>
                    <ContentSubscriptionColumn>
                      {addedPackage.startDate &&
                        addedPackage.startDate.toString()}
                    </ContentSubscriptionColumn>
                    <ContentSubscriptionColumn>
                      {addedPackage.endDate && addedPackage.endDate.toString()}
                    </ContentSubscriptionColumn>
                    <button type="button" onClick={() => removePackage(idx)}>
                      X
                    </button>
                  </>
                ))}
            </ContentSubscriptionWrapper>

So before the form gets submitted, the 'add packages' has to be set. Where do I add the {...register} object to add to the larger form object for submission?

const {
    control,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<any>()
  const onSubmit = (data: any) => {
    console.log(data)
  }

CodePudding user response:

I created a CodeSandbox trying to reproduce your use case and used Material UI to get it done quickly, but you should get the idea and can modify it with your own components.

Edit BasicDatePicker Material Demo (forked)

  • you should let RHF handle all the state of your form
  • use RHF's useFieldArray for managing (add, remove) your packages/subscriptions - there is no need to use watch here
  • use a separate useForm for your <AddPackage /> component, this has the benefit that you will have form validation for this sub form (in case it should be a requirement that all fields of <AddPackage /> need to be required) - i added validation in the demo to demonstrate this

AddPackage.tsx

export const AddSubscription: React.FC<AddSubscriptionProps> = ({ onAdd }) => {
  const {
    control,
    reset,
    handleSubmit,
    formState: { errors }
  } = useForm<Subscription>({
    defaultValues: { from: null, to: null, package: null }
  });

  const clear = () => reset();

  const add = handleSubmit((subscription: Subscription) => {
    onAdd(subscription);
    clear();
  });

  return (
    <Card variant="outlined">
      <LocalizationProvider dateAdapter={AdapterDateFns}>
        <Grid container spacing={1} p={2}>
          <Grid item container spacing={1} xs={12}>
            <Grid item xs={6}>
              <Controller
                name="from"
                control={control}
                rules={{ required: "Required" }}
                render={({ field }) => (
                  <DatePicker
                    {...field}
                    label="From"
                    renderInput={(params) => (
                      <TextField
                        {...params}
                        fullWidth
                        error={!!errors.from}
                        helperText={errors.from?.message}
                      />
                    )}
                  />
                )}
              />
            </Grid>
            <Grid item xs={6}>
              <Controller
                name="to"
                control={control}
                rules={{ required: "Required" }}
                render={({ field }) => (
                  <DatePicker
                    {...field}
                    label="To"
                    renderInput={(params) => (
                      <TextField
                        {...params}
                        fullWidth
                        error={!!errors.to}
                        helperText={errors.to?.message}
                      />
                    )}
                  />
                )}
              />
            </Grid>
          </Grid>
          <Grid item xs={12}>
            <Controller
              name="package"
              control={control}
              rules={{ required: "Required" }}
              render={({ field: { onChange, ...field } }) => (
                <Autocomplete
                  {...field}
                  options={packages}
                  onChange={(e, v) => onChange(v)}
                  renderInput={(params) => (
                    <TextField
                      {...params}
                      label="Package"
                      fullWidth
                      error={!!errors.package}
                      helperText={errors.package && "Required"}
                    />
                  )}
                />
              )}
            />
          </Grid>
          <Grid item xs={12}>
            <Stack spacing={1} direction="row" justifyContent="end">
              <Button variant="outlined" onClick={clear}>
                Clear
              </Button>
              <Button variant="contained" onClick={add} type="submit">
                Add
              </Button>
            </Stack>
          </Grid>
        </Grid>
      </LocalizationProvider>
    </Card>
  );
};

Form.tsx

export default function Form() {
  const { control, handleSubmit } = useForm<FormValues>({
    defaultValues: {
      seats: "",
      addOns: false
    }
  });
  const { fields, append, remove } = useFieldArray({
    control,
    name: "subscriptions"
  });

  const onSubmit = (data) => console.log("data", data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Grid container spacing={2}>
        <Grid item xs={12}>
          <Box display="flex" justifyContent="end" gap={1}>
            <Button variant="outlined">Cancel</Button>

            <Button variant="contained" type="submit">
              Save
            </Button>
          </Box>
        </Grid>

        <Grid item xs={12}>
          <Controller
            name="seats"
            control={control}
            render={({ field }) => (
              <TextField {...field} fullWidth label="Seats" />
            )}
          />
        </Grid>
        <Grid item xs={12}>
          <AddSubscription onAdd={append} />
        </Grid>
        <Grid item xs={12}>
          <List>
            {fields.map((field, index) => (
              <ListItem
                key={field.id}
                secondaryAction={
                  <IconButton
                    edge="end"
                    aria-label="delete"
                    onClick={() => remove(index)}
                  >
                    <DeleteIcon />
                  </IconButton>
                }
              >
                <ListItemText
                  primary={field.package.label}
                  secondary={
                    <span>
                      {formatDate(field.from)} - {formatDate(field.to)}
                    </span>
                  }
                />
              </ListItem>
            ))}
          </List>
        </Grid>

        <Grid item xs={12}>
          <Controller
            name="addOns"
            control={control}
            render={({ field: { value, onChange } }) => (
              <FormControlLabel
                control={<Checkbox checked={!!value} onChange={onChange} />}
                label="Add-ons"
              />
            )}
          />
        </Grid>
      </Grid>
    </form>
  );
}
  • Related