Replies: 10 comments 4 replies
-
I agree... |
Beta Was this translation helpful? Give feedback.
-
I came across this issue while using Remix. While we still use react-hook-form for client-side validation, you can't wrap it in the form component like It is super easy to remove the react-hook-form logic from Form components to keep the style and add it around wrappers of your own |
Beta Was this translation helpful? Give feedback.
-
I am using Remix also...do you have an example of removing this react-hook-form thing? |
Beta Was this translation helpful? Give feedback.
-
Sure it's pretty rough but it's working. Here I took the form elements and made this: import * as React from "react";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "~/lib/utils";
import { Label } from "~/components/ui/label";
import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
/**
* NOTE:
* These are based on the form components from the form.tsx except they have been stripped of all the React Hook Form logic.
* This allows them to be used with ease in Remix Forms and applied with React Hook Form in the way Remix wants
*/
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return <div ref={ref} className={cn("space-y-2", className)} {...props} />;
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
return <Label ref={ref} className={className} {...props} />;
});
FormLabel.displayName = "FormLabel";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
return (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> & {
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
}
>(({ className, children, error, ...props }, ref) => {
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export { FormItem, FormLabel, FormDescription, FormMessage }; Then it can be used in a form like so: We are still using React Hook Form but are using with remix form and getting a really nice client side validation too. Using https://github.com/Code-Forge-Net/remix-hook-form const schema = z.object({
name: z
.string({
required_error: "Please select an account",
})
.min(1),
type: z.enum(["business", "school", "home"]),
});
type FormData = z.infer<typeof schema>;
const resolver = zodResolver(schema);
export default function CreateAccount() {
const { userId } = useLoaderData<typeof loader>();
const { handleSubmit, formState, register } = useRemixForm({
mode: "onSubmit",
resolver,
submitData: { userId, key: "value" },
});
return (
<div className="flex items-center justify-center min-h-screen md:px-0 px-4">
<Card className="w-full md:w-96">
<CardHeader className="pb-3">
<CardTitle>Create Account</CardTitle>
<CardDescription>Set up a new account</CardDescription>
</CardHeader>
<CardContent className="grid gap-1">
<Form method="post" onSubmit={handleSubmit} className="space-y-6">
<FormItem>
<FormLabel>Name</FormLabel>
<Input
placeholder="Name of account"
type="text"
{...register("name")}
/>
<FormMessage error={formState.errors?.name} />
</FormItem>
<FormItem>
<FormLabel>Account Type</FormLabel>
<RadioGroup
{...register("type")}
className="grid grid-cols-3 gap-4"
>
<div>
<RadioGroupItem
value="business"
id="business"
className="peer sr-only"
/>
<Label
htmlFor="business"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
Business
</Label>
</div>
<div>
<RadioGroupItem
value="school"
id="school"
className="peer sr-only"
/>
<Label
htmlFor="school"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
School
</Label>
</div>
<div>
<RadioGroupItem
value="home"
id="home"
className="peer sr-only"
/>
<Label
htmlFor="home"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
Home
</Label>
</div>
</RadioGroup>
<FormMessage error={formState.errors?.type} />
</FormItem>
<Button
className="w-full"
type="submit"
disabled={!formState.isValid}
>
Create Account
</Button>
</Form>
</CardContent>
</Card>
</div>
);
} |
Beta Was this translation helpful? Give feedback.
-
Still using react-hook-form but in a remix way and you could use this approach to work with another form lib if you want and keep styling |
Beta Was this translation helpful? Give feedback.
-
I also found that There's also a recent issue requesting this. I am closing this discussion as we can follow the development across on the issue? |
Beta Was this translation helpful? Give feedback.
-
You can definetly still use useFormState, however it wont work with useFormStatus (React useFormStatus). function SignUpForm() {
const [serverFormState, formAction] = useFormState(signUp, initialState);
const {
register,
handleSubmit,
watch,
reset,
formState: { errors },
} = useForm<Inputs>({
resolver: zodResolver(SignUpSchema),
});
const onSubmit: SubmitHandler<Inputs> = async (data) => {
formAction(data);
};
return (
<form className='space-y-4' onSubmit={handleSubmit(onSubmit)}>
<div className='flex gap-4'>
<div>
<label
htmlFor='firstName'
className='mb-2 text-sm font-medium text-gray-900 dark:text-white'
>
First name
</label>
<input
type='firstName'
id='firstName'
className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
{...register('firstName')}
/>
<p className='text-sm text-red-500 min-h-unit-5'>
{errors.firstName?.message}
</p>
</div>
<div>
<label
htmlFor='lastName'
className='mb-2 text-sm font-medium text-gray-900 dark:text-white'
>
Last name
</label>
<input
type='lastName'
id='lastName'
className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
{...register('lastName')}
/>
<p className='text-sm text-red-500 min-h-unit-5'>
{errors.lastName?.message}
</p>
</div>
</div>
<div>
<label
htmlFor='email'
className='mb-2 text-sm font-medium text-gray-900 dark:text-white'
>
Your email
</label>
<input
type='email'
id='email'
className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder='name@email.com'
{...register('email')}
/>
<p className='text-sm text-red-500 min-h-unit-5'>
{errors.email?.message}
</p>
</div>
<div>
<label
htmlFor='password'
className='mb-2 text-sm font-medium text-gray-900 dark:text-white'
>
Password
</label>
<input
type='password'
id='password'
placeholder='••••••••'
className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
{...register('password')}
/>
<p className='text-sm text-red-500 min-h-unit-5'>
{errors.password?.message}
</p>
</div>
<div>
<label
htmlFor='confirm-password'
className='mb-2 text-sm font-medium text-gray-900 dark:text-white'
>
Confirm password
</label>
<input
type='password'
id='confirmPassword'
placeholder='••••••••'
className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
{...register('confirmPassword')}
/>
<p className='text-sm text-red-500 min-h-unit-5'>
{errors.confirmPassword?.message}
</p>
</div>
<div className='flex flex-col items-start'>
<div className='flex items-center h-5'>
<input
id='terms'
aria-describedby='terms'
type='checkbox'
className='w-4 h-4 border border-gray-300 bg-gray-50 focus:ring-3 dark:bg-gray-700 dark:border-gray-600 dark:ring-offset-gray-800'
{...register('terms')}
/>
<label
htmlFor='terms'
className='ml-3 text-sm font-light text-gray-500 dark:text-gray-300'
>
I accept the
<Link
className='font-medium text-blue-600 hover:underline'
href='/terms-and-conditions'
>
{' '}
Terms and Conditions
</Link>
</label>
</div>
<p className='text-sm text-red-500 min-h-unit-5'>
{errors.terms?.message}
</p>
</div>
{/* SERVER MESSAGE */}
<p className='font-medium text-sm text-red-500 min-h-unit-5'>
{serverFormState?.message}
</p>
<SubmitButton />
<p className='text-sm font-light text-gray-500 dark:text-gray-400 mt-5'>
Already have an account?
<Link
className='font-medium text-blue-600 hover:underline hover:cursor-pointer dark:text-primary-500'
href='/auth/login'
>
{' '}
Login here
</Link>
</p>
</form>
);
}
function SubmitButton() {
// WILL NOT WORK
const { pending } = useFormStatus();
return (
<Skeleton isLoaded={!pending}>
<button
aria-disabled={pending}
type='submit'
className='w-full text-white bg-zenmos-purple-900 hover:bg-zenmos-purple-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm py-2.5 text-center'
>
Create an account
</button>
</Skeleton>
);
} |
Beta Was this translation helpful? Give feedback.
-
Hi everyone, I'm also trying to use shadcn with uncontrolled components to make the most of the functionality of the new server actions. To start I tried to generalize the returning data coming from the server actions. Stealing a bit from sveltekit I tried to assign types to the responses returned.
native-action.ts And then add some helpers:
native-action.ts Once I decided on the types returned by the actions I created a wrapper for the server actions:
native-action.ts So that they can be used in this way (for example):
native-action.ts Ok all this can be used in any ui component library, you need to invoke the action and manage the return type accordingly For example:
user-form.tsx To integrate all this with shadcn I recreated as uncontrolled version of the basic components of the form that the library exposes:
native-form-components.ts Using these components I can create forms of this type, which are validated on the server side, the label and the error message come out in the case of a validation error
structure-form.tsx This is all a mega work in progress, I'm using it in a project I'm developing at the moment. |
Beta Was this translation helpful? Give feedback.
-
For remix use, combining Form's react-hook-form with remix-hook-form while minimizing the extend of the code necessery, comes down to utilization of RemixFormProvider and useRemixFormContext, both come from "remix-hook-form": import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { Controller, ControllerProps, FieldPath, FieldValues } from "react-hook-form";
import { RemixFormProvider, useRemixFormContext } from "remix-hook-form";
import { Label } from "~/components/ui/label";
import { cn } from "~/utils/css";
const Form = RemixFormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useRemixFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
ref={ref}
className={cn("space-y-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
}
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
);
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
}
);
FormMessage.displayName = "FormMessage";
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField }; By doing it this way, no extensive changes beyond just : ...
import { Controller, ControllerProps, FieldPath, FieldValues } from "react-hook-form";
import { RemixFormProvider, useRemixFormContext } from "remix-hook-form";
...
const Form = RemixFormProvider; // In remix it might be better to call it FormProvider as we already have Form from remix-run/react
...
const useFormField = () => {
...
const { getFieldState, formState } = useRemixFormContext();
... To use it, you can do this now: import { zodResolver } from "@hookform/resolvers/zod";
import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useFetcher, useNavigate } from "@remix-run/react";
import { getValidatedFormData, useRemixForm } from "remix-hook-form";
import { z } from "zod";
import { Button } from "~/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog";
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Form as FormProvider } from "~/components/ui/form";
import { Input } from "~/components/ui/input";
const createNewTeamSchema = z.object({
slug: z.string().min(2, "Account slug must be at least 2 characters."),
name: z.string().min(2, "Account name must be at least 2 characters."),
});
type CreateNewTeamSFormData = z.infer<typeof createNewTeamSchema>;
const resolver = zodResolver(createNewTeamSchema);
export async function action({ request }: ActionFunctionArgs) {
const {
errors,
data,
receivedValues: defaultValues,
} = await getValidatedFormData<CreateNewTeamSFormData>(request, resolver);
if (errors) {
console.error(errors);
// The keys "errors" and "defaultValue" are picked up automatically by useRemixForm
return json({ errors, defaultValues }, { status: 400 });
}
console.log("data:", JSON.stringify(data, null, 4));
return null;
}
export default function CreateNewTeam() {
const actionData = useActionData();
const navigate = useNavigate();
function onDismiss() {
navigate(-1);
}
const fetcher = useFetcher();
const methods = useRemixForm({
resolver,
fetcher,
defaultValues: {
slug: "",
name: "",
},
});
return (
<div>
<Dialog
aria-label="Create new team"
modal
onOpenChange={onDismiss}
open
>
<DialogContent>
<DialogHeader>
<DialogTitle>Create a new team</DialogTitle>
<DialogDescription>Create a team to collaborate with others.</DialogDescription>
</DialogHeader>
<FormProvider {...methods}>
<Form
className="space-y-6"
method="POST"
action="."
onSubmit={methods.handleSubmit}
>
<FormField
control={methods.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Team Name</FormLabel>
<FormControl>
<Input
placeholder="My Team"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={methods.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Unique identifier</FormLabel>
<FormControl>
<Input
placeholder="my-team"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
id="btn-create-new-team"
disabled={
!(methods.formState.isDirty && methods.formState.isValid) ||
methods.formState.isSubmitting ||
methods.formState.isValidating ||
methods.formState.isLoading
? true
: undefined
}
type="submit"
>
{methods.formState.isSubmitting || methods.formState.isValidating || methods.formState.isLoading
? "Creating..."
: "Create New Team"}
</Button>
</Form>
</FormProvider>
</DialogContent>
</Dialog>
</div>
);
} I hope this helps someone. |
Beta Was this translation helpful? Give feedback.
-
here is an adaptation for use with import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import {
FieldMetadata,
FormMetadata,
FormProvider,
getFormProps,
useField,
useInputControl
} from "@conform-to/react";
import {Form as RemixForm } from "@remix-run/react";
import {RemixFormProps} from "@remix-run/react/dist/components";
const Form = <T extends Record<string, unknown>,>({ form, children, ...props }: RemixFormProps & { form: FormMetadata<T> }) => {
return (
<FormProvider context={form.context}>
<RemixForm method="post" {...getFormProps(form)} {...props}>
{children}
</RemixForm>
</FormProvider>
);
}
const FormFieldContext = React.createContext<{name: string}>({} as {name: string});
const FormField = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & {field: FieldMetadata, render?: (control: ReturnType<typeof useInputControl>) => React.ReactNode}>(
({ className, render, children, field, ...props }, ref) => {
// @ts-ignore
const control = useInputControl(field);
return (
<FormFieldContext.Provider value={{ name: field.name }}>
<div
ref={ref}
className={cn("space-y-2", className)}
{...props} >
{ render ? render(control) : children }
</div>
</FormFieldContext.Provider>
);
});
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const [meta,] = useField(fieldContext.name);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
return {
formItemId: `${meta.id}-form-item`,
formDescriptionId: `${meta.id}-form-item-description`,
formMessageId: `${meta.id}-form-item-message`,
valid: meta.valid,
errors: meta.errors,
dirty: meta.dirty,
name: meta
};
};
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const field = useFormField();
return (
<Label
ref={ref}
className={cn(!field.valid && "text-destructive", className)}
htmlFor={field.formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const field = useFormField();
return (
<Slot
ref={ref}
id={field.formItemId}
aria-describedby={field.valid ? `${field.formDescriptionId}` : `${field.formDescriptionId} ${field.formMessageId}`}
aria-invalid={!field.valid}
{...props}
/>
);
}
);
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const field = useFormField();
return (
<p
ref={ref}
id={field.formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const field = useFormField();
const body = field.errors ? field.errors[0] : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={field.formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
}
);
FormMessage.displayName = "FormMessage";
export { Form, FormControl, FormDescription, FormField, FormLabel, FormMessage, useFormField }; usage import * as z from "zod";
import {ActionFunctionArgs} from "@remix-run/server-runtime";
import {Input} from "@/components/ui/input";
import { useActionData, } from "@remix-run/react";
import {Form, FormControl, FormField, FormLabel, FormMessage} from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {Button} from "@/components/ui/button";
import {parseWithZod} from "@conform-to/zod";
import {getInputProps, useForm} from "@conform-to/react";
const schema = z.object({
firstName: z.string(),
lastName: z.string(),
sex: z.enum(['male', 'female']),
});
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const submission = parseWithZod(formData, { schema });
// ...
}
export default function Profile() {
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
lastResult,
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldRevalidate: 'onInput',
shouldValidate: 'onBlur',
});
return <div className="space-y-6">
<Form method="post" className="space-y-6" form={form}>
<FormField field={fields.firstName}>
<FormLabel>First name</FormLabel>
<FormControl>
<Input
className={!fields.firstName.valid ? 'error' : ''}
{...getInputProps(fields.firstName, {type: 'text'})}
/>
</FormControl>
<FormMessage/>
</FormField>
<FormField field={fields.lastName}>
<FormLabel>Last name</FormLabel>
<FormControl>
<Input
className={!fields.lastName.valid ? 'error' : ''}
{...getInputProps(fields.lastName, {type: 'text'})}
/>
</FormControl>
<FormMessage/>
</FormField>
<FormField
field={fields.sex}
render={(control) => <>
<FormLabel>Sex</FormLabel>
<FormControl>
<Select
defaultValue={control.value as string}
onValueChange={control.change}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Choose sex" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="female">Male</SelectItem>
<SelectItem value="male">Female</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</>
}
/>
<Button type="submit">Save</Button>
</Form>
</div>;
} |
Beta Was this translation helpful? Give feedback.
-
I've been following the Forms and Mutations guide on the NextJS site (using NextJS14) to primarily not be dependent on
react-hook-form
(nothing against the library), using it the way it's prescribed in the examples.With the
'use client'
annotation while usingreact-hook-form
it prevents me from:metadata
on a page that contains a form (although I haven't tried abstracting the form away in a component and importing that in a page)canary
prevents usinguseFormState
anduseFormStatus
hooksMoving away from the
shadcnui
Forms will make me lose some nice functionality around displaying error states etc.I was wondering if there are any plans of supporting the functionality that the current templated forms do but using server rendered forms as demonstrated by the NextJS examples.
Node version: v21.1.0
OS: macOS 14
Framework: NextJS 14.0.1
Beta Was this translation helpful? Give feedback.
All reactions