I’m trying to submit an array for a field called services
using reack-hook-form but it’s returning as a separate value when submitted. { customerId: '', title: '', message: '', serviceAt: '', 'services.0.id': '1', amount: '5.25' }
This works if I change it to use the onSubmit
method but I want to use the actions method so the form can be used with JS.
"use client";
import { CaretSortIcon, CheckIcon, PlusIcon } from "@radix-ui/react-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { useReducer, useState } from "react";
import { Controller, useFieldArray } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "app/components/ui/button";
import { Calendar } from "app/components/ui/calendar";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "app/components/ui/command";
import { FullScreenModal } from "app/components/ui/full-screen-modal";
import { InputField } from "app/components/ui/input";
import { Label } from "app/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "app/components/ui/popover";
import { Separator } from "app/components/ui/separator";
import { Textarea } from "app/components/ui/textarea";
import ServiceField from "../../components/ServiceField";
import createCustomerByName from "app/actions/mutations/createCustomerByName";
import { createProposalMutation } from "app/actions/mutations/createProposal";
import getCustomersQuery from "app/actions/queries/getCustomers";
import { useSlyderzForm } from "app/hooks/useSlyderzForm";
import { cn, formatNumberToCurrency } from "app/lib/utils";
import { ProposalServicesReducer, initialState } from "reducers/proposal-services-reducer";
export const createProposalSchema = z.object({
// title: z.string(),
// amount: z.string(),
// description: z.string().optional(),
// customerId: z.string(),
// serviceAt: z.string(),
services: z.array(z.any()),
});
interface ProposalModalProps {
closeModal: () => void;
}
function ProposalModalCreateForm(props: ProposalModalProps) {
const [open, setOpen] = useState<boolean>(false);
const [newCustomerName, setNewCustomerName] = useState<string>("");
const queryClient = useQueryClient()
const [state, dispatch] = useReducer(ProposalServicesReducer, initialState)
const { data: customers = [] } = useQuery({
queryKey: ["customers"],
queryFn: () => getCustomersQuery(),
});
const createCustomer = useMutation({
mutationFn: createCustomerByName,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["customers"] });
setOpen(false)
},
});
const createProposal = useMutation({
mutationFn: createProposalMutation,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["proposals"] });
},
});
const { control, register, setValue, handleSubmit, formState: { errors } } = useSlyderzForm(createProposalSchema, {
title: "",
services: [{ id: 1 }]
});
const { append, fields } = useFieldArray({
control,
name: "services"
})
const handleCreateCustomer = async () => {
if (newCustomerName) {
const res = await createCustomer.mutateAsync(newCustomerName);
if (!res.success) {
toast.error(res.error?.message);
}
return
}
toast.error("Customer name can't be empty");
};
const handleFormSubmit = async (input: FormData) => {
const res = await createProposal.mutateAsync(input);
if (!res.success) {
toast.error(res.error?.message);
return
}
toast.success("Proposal successfully created");
props.closeModal();
};
const onSub = async (data: any) => {
console.log(data)
await createProposalMutation(data)
}
return (
<FullScreenModal title="Create Proposal" formId="create-proposal-form">
<div>
<form
id="create-proposal-form"
action={handleFormSubmit}
// onSubmit={proposalForm.handleSubmit(onSub)}
className="space-y-8 overflow-auto"
>
<Controller
control={control}
name="customerId"
render={({ field }) => (
<div className="space-y-2 flex flex-col">
<input hidden name="customerId" value={field.value} />
<Label>Assign a customer to proposal</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className={cn(
"justify-between",
!field.value && "text-muted-foreground",
)}
>
{field.value
? customers.find(
(customer) => customer.id === field.value,
)?.firstName
: "Select customer"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 popover-content-width-full">
<Command>
<CommandInput
placeholder="Search customers..."
onKeyUp={(e: any) => setNewCustomerName(e.target.value)}
required
/>
<CommandList>
<CommandEmpty>
<Button
variant="ghost"
className="w-full flex justify-start"
onClick={handleCreateCustomer}
>
<PlusIcon className={cn("h-5 w-5 font-bold pr-2")} />
<strong>
Create new customer: {newCustomerName}
</strong>
</Button>
</CommandEmpty>
<CommandGroup>
{customers.map((customer) => (
<CommandItem
{...field}
key={customer.id}
onSelect={() => {
setValue("customerId", customer.id);
setOpen(false);
}}
className="capitalize"
>
{customer?.firstName}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
customer.id === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}
/>
<Separator />
<InputField
id="title"
label="Title"
placeholder="Proposal title"
{...register("title")}
/>
<Controller
control={control}
name="message"
render={({ field }) => (
<div className="space-y-2">
<Label>Message</Label>
<Textarea
rows={3}
placeholder="Thank you for choosing our services."
{...field}
/>
</div>
)}
/>
<Controller
control={control}
name="serviceAt"
render={({ field }) => (
<div className="space-y-2">
<input hidden readOnly name="serviceAt" value={field.value} />
<Label>Service Date</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground",
)}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
// disabled={(date) => disableDaysOff(date)}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
)}
/>
<Separator />
<h6 className="text-lg font-semibold leading-none tracking-tight">
Services
</h6>
{/* <ServiceField
control={control}
register={register}
dispatch={dispatch}
/> */}
<button type="button" onClick={() => append({ id: 1 })}>
add service
</button>
{fields.map((f, i) => (
<input key={f.id} {...register(`services.${i}.id` as const)} />
))}
{/* Proposal amount - hidden field */}
<input
{...register("amount" as const)}
hidden
readOnly
defaultValue={String(state.amount + 5.25)}
/>
</form>
<section className="pt-10 w-full text-end gap-4 grid">
<p>
<strong>Subtotal:</strong> {formatNumberToCurrency(state.amount)}
</p>
<p>
<strong>Taxes:</strong> $5.25
</p>
<p>
<strong>Total:</strong> {formatNumberToCurrency(state.amount + 5.25)}
</p>
</section>
</div>
</FullScreenModal>
);
}
export default ProposalModalCreateForm;