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.
- 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 usewatch
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>
);
}