A container for collecting and validating user input.
"use client";
import { createContext, forwardRef, useContext, useId } from "react";
import { ark } from "@ark-ui/react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { Label } from "../core/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = 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>
);
};
FormField.displayName = "FormField";
const useFormField = () => {
const fieldContext = useContext(FormFieldContext);
const itemContext = useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
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 = createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={"space-y-2 " + className} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = forwardRef<
HTMLLabelElement,
React.DetailedHTMLProps<
React.LabelHTMLAttributes<HTMLElement>,
HTMLLabelElement
>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
aria-invalid={error !== undefined}
htmlFor={formItemId}
className={className}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = forwardRef<
React.ElementRef<typeof ark.slot>,
React.ComponentPropsWithoutRef<typeof ark.slot>
>((props, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<ark.slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormFieldHelperText = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={"text-base-500 text-sm leading-[1] " + className}
{...props}
/>
);
});
FormFieldHelperText.displayName = "FormFieldHelperText";
const FormFieldErrorMessage = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const message = error ? String(error?.message) : children;
if (!message) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={"text-sm font-medium text-red-500 " + className}
{...props}
>
{message}
</p>
);
});
FormFieldErrorMessage.displayName = "FormFieldErrorMessage";
export {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormFieldHelperText,
useFormField,
FormFieldErrorMessage,
};
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Form,
FormControl,
FormFieldHelperText,
FormField,
FormItem,
FormLabel,
FormFieldErrorMessage,
} from "../core/form";
import { Input } from "../core/input";
import { Button } from "../core/button";
const loginFormSchema = z.object({
email: z.string().email(),
password: z.string().min(20).max(100),
});
export const FormExample = () => {
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = (values: z.infer<typeof loginFormSchema>) => {
console.log(values);
};
return (
<div className="w-full max-w-96">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="mb-5 space-y-3">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormFieldHelperText>
This is your public display name.
</FormFieldHelperText>
<FormFieldErrorMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormFieldErrorMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" fullWidth colorScheme="primary">
Log in
</Button>
</form>
</Form>
</div>
);
};