Skip to content

Commit

Permalink
refactor: add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
duongductrong committed Jun 21, 2024
1 parent 10c19a5 commit 00538bd
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 67 deletions.
5 changes: 4 additions & 1 deletion src/form-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { Slot } from "@radix-ui/react-slot"
import React, { forwardRef } from "react"
import { useFormField } from "./hooks/use-form-field"

export interface FormControlProps
extends React.ComponentPropsWithoutRef<typeof Slot> {}

export const FormControl = forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
FormControlProps
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()

Expand Down
5 changes: 4 additions & 1 deletion src/form-description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { forwardRef, HTMLAttributes } from "react"
import { useFormField } from "./hooks/use-form-field"
import { cn } from "./utils"

export interface FormDescriptionProps
extends HTMLAttributes<HTMLParagraphElement> {}

export const FormDescription = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLParagraphElement>
FormDescriptionProps
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()

Expand Down
23 changes: 12 additions & 11 deletions src/form-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import { forwardRef, HTMLAttributes, useId, useMemo } from "react"
import FormItemContext from "./context/form-item-context"
import { cn } from "./utils"

export const FormItem = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = useId()
export interface FormItemProps extends HTMLAttributes<HTMLDivElement> {}

return (
<FormItemContext.Provider value={useMemo(() => ({ id }), [id])}>
<div ref={ref} className={cn("field-item", className)} {...props} />
</FormItemContext.Provider>
)
})
export const FormItem = forwardRef<HTMLDivElement, FormItemProps>(
({ className, ...props }, ref) => {
const id = useId()

return (
<FormItemContext.Provider value={useMemo(() => ({ id }), [id])}>
<div ref={ref} className={cn("field-item", className)} {...props} />
</FormItemContext.Provider>
)
}
)

FormItem.displayName = "FormItem"
34 changes: 18 additions & 16 deletions src/form-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ import React, { forwardRef } from "react"
import { useFormField } from "./hooks/use-form-field"
import { cn } from "./utils"

export const FormLabel = forwardRef<
React.ElementRef<"label">,
React.ComponentPropsWithoutRef<"label">
>(({ className, ...props }, ref) => {
const { error, formItemId, name } = useFormField()
export interface FormLabelProps
extends React.ComponentPropsWithoutRef<"label"> {}

return (
<label
ref={ref}
className={cn("field-label", className)}
data-name={name}
data-state={error ? "error" : "idle"}
htmlFor={formItemId}
{...props}
/>
)
})
export const FormLabel = forwardRef<React.ElementRef<"label">, FormLabelProps>(
({ className, ...props }, ref) => {
const { error, formItemId, name } = useFormField()

return (
<label
ref={ref}
className={cn("field-label", className)}
data-name={name}
data-state={error ? "error" : "idle"}
htmlFor={formItemId}
{...props}
/>
)
}
)

FormLabel.displayName = "FormLabel"
5 changes: 4 additions & 1 deletion src/form-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import React from "react"
import { cn } from "./utils"
import { useFormField } from "./hooks/use-form-field"

export interface FormMessageProps
extends React.HTMLAttributes<HTMLParagraphElement> {}

export const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
FormMessageProps
>(({ className, children, ...props }, ref) => {
const { name, error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
Expand Down
94 changes: 94 additions & 0 deletions src/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client"

import {
ComponentPropsWithoutRef,
ReactNode,
RefObject,
useImperativeHandle,
useRef,
} from "react"
import {
FieldValues,
FormProvider as FormPrimitiveProvider,
UseFormProps,
UseFormReturn,
useForm,
} from "react-hook-form"

export interface FormProps<
TFieldValues extends FieldValues = FieldValues,
TContext = any,
TTransformedValues extends FieldValues | undefined = undefined,
> extends Omit<ComponentPropsWithoutRef<"form">, "onSubmit">,
UseFormProps<TFieldValues, TContext> {
ref?: RefObject<HTMLFormElement>
formRef?: RefObject<UseFormReturn<TFieldValues, TContext, TTransformedValues>>
children: ReactNode
onSubmit?: (data: TFieldValues) => void
}

export const Form = <
TFieldValues extends FieldValues = FieldValues,
TContext = any,
TTransformedValues extends FieldValues | undefined = undefined,
>({
children,
mode,
disabled,
reValidateMode,
defaultValues,
values,
errors,
resetOptions,
context,
shouldFocusError,
shouldUnregister,
shouldUseNativeValidation,
progressive,
criteriaMode,
delayError,
formRef,
ref,
resolver,
onSubmit,
...props
}: FormProps<TFieldValues, TContext, TTransformedValues>) => {
const methods = useForm<TFieldValues, TContext, TTransformedValues>({
resolver,
mode,
disabled,
reValidateMode,
defaultValues,
values,
errors,
resetOptions,
context,
shouldFocusError,
shouldUnregister,
shouldUseNativeValidation,
progressive,
criteriaMode,
delayError,
})

const innerFormElementRef = useRef<HTMLFormElement>(null)

useImperativeHandle(formRef, () => methods as any, [methods])
useImperativeHandle(ref, () => innerFormElementRef.current as any, [
innerFormElementRef.current,
])

return (
<FormPrimitiveProvider {...methods}>
<form
{...props}
onSubmit={onSubmit ? methods.handleSubmit(onSubmit as any) : undefined}
ref={innerFormElementRef}
>
{children}
</form>
</FormPrimitiveProvider>
)
}

Form.displayName = "Form"
35 changes: 0 additions & 35 deletions src/hooks/use-scope.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// export * from "./context/form-context"

export * from "./field"
export * from "./form-control"
export * from "./form-description"
Expand All @@ -9,3 +7,4 @@ export * from "./form-label"
export * from "./form-message"
export * from "./hooks/use-form-field"
export * from "./utils"
export * from "./form"
54 changes: 54 additions & 0 deletions tests/__snapshots__/create-field.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`renders correctly 1`] = `
<form>
<div
className="field-item"
>
<label
className="field-label"
data-name="a"
data-state="idle"
htmlFor=":r0:-form-item"
>
Input
</label>
<input
type="text"
/>
</div>
<div
className="field-item"
>
<input
type="number"
/>
</div>
<div
className="field-item"
>
<label
className="field-label"
data-name="c"
data-state="idle"
htmlFor=":r2:-form-item"
>
File
</label>
<input
type="file"
/>
</div>
<div
className="field-item"
>
<select>
<option
value="test"
>
Test
</option>
</select>
</div>
</form>
`;
62 changes: 62 additions & 0 deletions tests/create-field.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react"
import renderer from "react-test-renderer"
import { expect, it } from "vitest"
import { FormLabel } from "../src/form-label"
import { Form, FormField, createField } from "../src"

interface InputProps {}

interface NumberProps {}

interface FileProps {}

interface SelectProps {
options: any[]
}

const Field = createField({
text: (props: InputProps) => <input type="text" />,
number: (props: NumberProps) => <input type="number" />,
file: (props: FileProps) => <input type="file" />,
select: (props: SelectProps) => (
<select>
<option value="test">Test</option>
</select>
),
})

const MyForm = () => {
return (
<Form>
<Field label="Input" component="text" name="a" />
<Field component="number" name="b" />
<Field label="File" component="file" name="c" />
<Field component="select" name="d" options={[]} />
</Form>
)
}

it("renders correctly", () => {
const form = renderer.create(<MyForm />)
const formInstance = form.root

expect(formInstance).toBeDefined()

expect(
formInstance.findByProps({ component: "text", name: "a" })
).toBeDefined()

expect(
formInstance.findByProps({ component: "number", name: "b" })
).toBeDefined()

expect(
formInstance.findByProps({ component: "file", name: "c" })
).toBeDefined()

expect(
formInstance.findByProps({ component: "select", name: "d" })
).toBeDefined()

expect(form.toJSON()).toMatchSnapshot()
})

0 comments on commit 00538bd

Please sign in to comment.