Skip to content

Commit

Permalink
feat: new useForm hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Pagebakers committed Jan 29, 2025
1 parent 5115e8f commit 78e70fb
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-lions-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@saas-ui/forms': major
---

New useForm hook that returns typed form components
4 changes: 2 additions & 2 deletions packages/saas-ui-forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@
"url": "https://storybook.saas-ui.dev"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@hookform/resolvers": "^3.10.0",
"@saas-ui/core": "workspace:*",
"@saas-ui/react": "workspace:*",
"react-hook-form": "^7.53.2"
"react-hook-form": "^7.54.2"
},
"peerDependencies": {
"@chakra-ui/react": "^3.2.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/saas-ui-forms/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Import and export Form and StepForm
import { createForm } from './create-form'

export { useForm, useZodForm } from './use-form'
export type { UseZodFormProps, UseFormReturn, UseFormProps } from './use-form'

// import { createStepForm } from './create-step-form'

// Exporting from './display-field'
Expand Down Expand Up @@ -271,12 +274,10 @@ export type {
UseFormClearErrors,
UseFormGetValues,
UseFormHandleSubmit,
UseFormProps,
UseFormRegister,
UseFormRegisterReturn,
UseFormReset,
UseFormResetField,
UseFormReturn,
UseFormSetError,
UseFormSetFocus,
UseFormSetValue,
Expand Down Expand Up @@ -311,7 +312,6 @@ export {
appendErrors,
useController,
useFieldArray,
useForm,
useFormState,
useWatch,
Controller,
Expand Down
106 changes: 105 additions & 1 deletion packages/saas-ui-forms/src/use-form.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,128 @@
import { forwardRef } from 'react'

import { type HTMLChakraProps, chakra } from '@chakra-ui/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { cx } from '@saas-ui/core/utils'
import {
type DefaultValues,
type FieldValues,
UseFormProps as UseHookFormProps,
type UseFormReturn as UseHookFormReturn,
useForm as useHookForm,
} from 'react-hook-form'
import type { z } from 'zod'

import { ArrayField, type ArrayFieldProps } from './array-field'
import { DisplayIf, type DisplayIfProps } from './display-if'
import { Field } from './field.tsx'
import { FormProvider } from './form-context'
import { ObjectField, type ObjectFieldProps } from './object-field'
import type { FieldProps } from './types'
import type { UseArrayFieldReturn } from './use-array-field'

export interface UseFormProps<
TFieldValues extends FieldValues,
TContext extends object,
> extends UseHookFormProps<TFieldValues, TContext> {}

export interface UseFormReturn<
TFieldValues extends FieldValues,
TContext extends object,
> extends UseHookFormReturn<TFieldValues, TContext> {
Form: React.FC<Omit<FormProps<TFieldValues, TContext>, 'form'>>
Field: React.FC<FieldProps<TFieldValues>>
DisplayIf: React.FC<DisplayIfProps<TFieldValues>>
ArrayField: React.FC<
ArrayFieldProps<TFieldValues> & React.RefAttributes<UseArrayFieldReturn>
>
ObjectField: React.FC<ObjectFieldProps<TFieldValues>>
}

export function useForm<
TFieldValues extends FieldValues,
TContext extends object,
>(props: UseFormProps<TFieldValues, TContext> = {}) {
const form = useHookForm<TFieldValues, TContext>(props)

const FormComponent = forwardRef<HTMLFormElement, Omit<FormProps, 'form'>>(
function FormComponent(props, ref) {
return <Form {...props} form={form} ref={ref} />
},
)

return {
...form,
Form: FormComponent,
Field,
}
DisplayIf,
ArrayField,
ObjectField,
} as UseFormReturn<TFieldValues, TContext>
}

export interface FormProps<
TFieldValues extends FieldValues = FieldValues,
TContext extends object = object,
> extends HTMLChakraProps<'form'> {
children: React.ReactNode
form: ReturnType<typeof useHookForm<TFieldValues, TContext>>
onSubmit: (data: any) => void
onError?: (errors: any) => void
}

export const Form = forwardRef<HTMLFormElement, FormProps>(
function Form(props, ref) {
const { children, form, onSubmit, onError, ...rest } = props
return (
<FormProvider {...form}>
<chakra.form
ref={ref}
onSubmit={form.handleSubmit(onSubmit, onError)}
{...rest}
className={cx('sui-form', props.className)}
>
{props.children}
</chakra.form>
</FormProvider>
)
},
) as <TFieldValues extends FieldValues, TContext extends object>(
props: FormProps<TFieldValues, TContext> & {
ref?: React.Ref<HTMLFormElement>
},
) => React.ReactElement

export interface UseZodFormProps<
TSchema extends
| z.AnyZodObject
| z.ZodEffects<z.AnyZodObject> = z.AnyZodObject,
TFieldValues extends InferObjectSchema<TSchema> = InferObjectSchema<TSchema>,
TContext extends object = object,
> extends Omit<UseHookFormProps<TFieldValues, TContext>, 'defaultValues'> {
schema: TSchema
defaultValues?:
| DefaultValues<InferObjectSchema<TSchema>>
| AsyncDefaultValues<InferObjectSchema<TSchema>>
}

export function useZodForm<
TSchema extends
| z.AnyZodObject
| z.ZodEffects<z.AnyZodObject> = z.AnyZodObject,
TFieldValues extends InferObjectSchema<TSchema> = InferObjectSchema<TSchema>,
TContext extends object = object,
>(props: UseZodFormProps<TSchema, TFieldValues, TContext>) {
const { schema, ...rest } = props

return useForm({
resolver: zodResolver(schema) as any,
...rest,
})
}

type InferObjectSchema<T extends z.ZodTypeAny | z.ZodEffects<z.ZodTypeAny>> =
T extends z.ZodEffects<infer TSchema> ? z.infer<TSchema> : z.infer<T>

type AsyncDefaultValues<TFieldValues> = (
payload?: unknown,
) => Promise<TFieldValues>
81 changes: 81 additions & 0 deletions packages/saas-ui-forms/stories/use-form.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react'

import { z } from 'zod'

import { FormLayout } from '../src/form-layout'
import { SubmitButton } from '../src/submit-button'
import { useForm, useZodForm } from '../src/use-form'

export default {
title: 'Forms/useForm',
}

const onSubmit = (data: any) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data)
}, 500)
})
}

export const Basic = () => {
const form = useForm({
defaultValues: {
name: '',
},
})

return (
<form.Form onSubmit={onSubmit}>
<FormLayout>
<form.Field type="text" name="name" label="Name" />

<SubmitButton />
</FormLayout>
</form.Form>
)
}

export const ConditionalFields = () => {
const form = useForm({
defaultValues: {
name: '',
description: '',
},
})

return (
<form.Form onSubmit={onSubmit}>
<FormLayout>
<form.Field type="text" name="name" label="Name" />

<form.DisplayIf name="name" condition={(value) => !!value}>
<form.Field type="text" name="description" label="Description" />
</form.DisplayIf>

<SubmitButton />
</FormLayout>
</form.Form>
)
}

export const ZodForm = () => {
const form = useZodForm({
defaultValues: {
name: '',
},
schema: z.object({
name: z.string().min(1, { message: 'Name is required' }),
}),
})

return (
<form.Form onSubmit={onSubmit}>
<FormLayout>
<form.Field type="text" name="name" label="Name" />

<SubmitButton />
</FormLayout>
</form.Form>
)
}
3 changes: 2 additions & 1 deletion packages/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.6.3",
"vite": "^5.4.11"
"vite": "^5.4.11",
"vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
"storybook": "8.4.5"
Expand Down
5 changes: 4 additions & 1 deletion packages/storybook/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "."
"baseUrl": ".",
"paths": {
"compositions/*": ["../../apps/compositions/src/*"]
}
},
"include": [".storybook/*.ts"]
}
3 changes: 2 additions & 1 deletion packages/storybook/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
plugins: [react()],
plugins: [react(), tsconfigPaths()],
// optimizeDeps: {
// include: ['@saas-ui/storybook-addon'],
// },
Expand Down
Loading

0 comments on commit 78e70fb

Please sign in to comment.