Skip to content

Commit

Permalink
feat: forms (#197)
Browse files Browse the repository at this point in the history
Cherry picked from #132

---------

Co-authored-by: Antriksh Kumar <[email protected]>
  • Loading branch information
2 people authored and KambojRajan committed Jun 8, 2024
1 parent 1e8d769 commit dba0263
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 5 deletions.
172 changes: 172 additions & 0 deletions components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
import { Controller, FormProvider, useFormContext } from 'react-hook-form';

import { Label } from '~/components/ui';
import { cn } from '~/lib/utils';

interface 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 } = 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,
};
};

interface 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> & {
disabled?: boolean;
required?: boolean;
}
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();

return (
<Label
ref={ref}
className={cn(error && 'text-primary-500', 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-[0.8rem] text-neutral-500', 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-[0.8rem] font-medium text-primary-500', className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = 'FormMessage';

export {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
FormProvider,
useFormField,
};
33 changes: 29 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.556.0",
"@formatjs/intl-localematcher": "^0.5.4",
"@hookform/resolvers": "^3.6.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
Expand All @@ -45,11 +46,12 @@
"next-auth": "^4.24.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.5",
"react-icons": "^5.0.1",
"tailwind-merge": "^2.2.1",
"typesense": "^1.8.2",
"usehooks-ts": "^3.0.1",
"zod": "^3.23.4"
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",
Expand Down

0 comments on commit dba0263

Please sign in to comment.