From f0750b98c1e202b98686d94c34e76ee1ab06cbf5 Mon Sep 17 00:00:00 2001 From: Pagebakers Date: Wed, 20 Mar 2024 13:46:55 -0300 Subject: [PATCH] feat: add getBaseField support to zod and yup forms --- .vscode/settings.json | 4 +- .../advanced-data/data-grid/usage.mdx | 57 ++++++++++++++++++- .../docs/components/forms/form/usage.mdx | 47 +++++++++++++++ .../docs/components/layout/sidebar/usage.mdx | 4 +- .../src/sidebar/sidebar.stories.tsx | 16 +++--- packages/saas-ui-forms/src/create-form.tsx | 16 ++++-- packages/saas-ui-forms/src/form.tsx | 5 +- packages/saas-ui-forms/src/index.ts | 1 + packages/saas-ui-forms/src/types.ts | 8 +-- .../saas-ui-forms/src/use-array-field.tsx | 5 +- .../saas-ui-forms/stories/form.stories.tsx | 37 +++++++++--- packages/saas-ui-forms/tests/form.test-d.ts | 22 ++++++- .../saas-ui-forms/yup/src/create-yup-form.ts | 28 ++++++--- .../saas-ui-forms/zod/src/create-zod-form.ts | 28 ++++++--- 14 files changed, 227 insertions(+), 51 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef424..25fa6215f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,3 @@ -{} +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/apps/website/src/pages/docs/components/advanced-data/data-grid/usage.mdx b/apps/website/src/pages/docs/components/advanced-data/data-grid/usage.mdx index a1459145b..a01a32e6e 100644 --- a/apps/website/src/pages/docs/components/advanced-data/data-grid/usage.mdx +++ b/apps/website/src/pages/docs/components/advanced-data/data-grid/usage.mdx @@ -742,7 +742,7 @@ import { DataGrid, } from '@saas-ui-pro/react' -export default function InternalState() { +export default function SlotProps() { return ( @@ -774,6 +774,61 @@ export default function InternalState() { } ``` +### Context Menu + +Using `slotsProps` you can use the `as` prop to customize the `tr` element and wrap it with a `ContextMenu`. + +```jsx +import { + Page, + PageHeader, + PageBody, + Toolbar, + ToolbarButton, + DataGrid, +} from '@saas-ui-pro/react' +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuList, + ContextMenuItem +} from '@saas-ui/react' + +export default function DataGridWithContextMenu() { + return ( + + + + ({ + as: RowWithContext, + }), + }} + /> + + + ) +} + +const RowWithContext = (props: TableRowProps) => { + return ( + + + + + + Edit + Copy + Delete + + + ) +} +``` + ## Typescript diff --git a/apps/website/src/pages/docs/components/forms/form/usage.mdx b/apps/website/src/pages/docs/components/forms/form/usage.mdx index 5514e952a..f84b6e047 100644 --- a/apps/website/src/pages/docs/components/forms/form/usage.mdx +++ b/apps/website/src/pages/docs/components/forms/form/usage.mdx @@ -624,6 +624,53 @@ export default function MyForm = () => { } ``` +### Custom base field + +The base field is responsible for rendering the label, help text, error message and the input itself. +You can configure a custom base field using the `getBaseField` prop of `createForm`. + +You can configure `extraProps` that can be passed to the `Field` component and will be available in your custom `BaseField` component. + +```tsx +import { FormControl, FormLabel, HStack, Tooltip } from '@chakra-ui/react' +import { createForm, useBaseField, splitProps } from '@saas-ui/react' +import { LuInfo } from 'react-icons/lu' + +const getBaseField: GetBaseField<{ infoLabel?: string }> = () => { + return { + extraProps: ['infoLabel'], + BaseField: (props) => { + const [{ children, infoLabel }, fieldProps] = splitProps(props, [ + 'children', + 'infoLabel', + ]) + + const { controlProps, labelProps, error } = useBaseField(fieldProps) + + return ( + + + {labelProps.label} + {infoLabel ? ( + + + + + + ) : null} + + {children} + + ) + }, + } +} + +export const Form = createForm({ + getBaseField, +}) +``` + ## Accessibility The `Form` component wraps the children in a HTML `
` element. diff --git a/apps/website/src/pages/docs/components/layout/sidebar/usage.mdx b/apps/website/src/pages/docs/components/layout/sidebar/usage.mdx index b685b3156..505d3c901 100644 --- a/apps/website/src/pages/docs/components/layout/sidebar/usage.mdx +++ b/apps/website/src/pages/docs/components/layout/sidebar/usage.mdx @@ -276,9 +276,9 @@ export default function Page() { } ``` -### Condensed variant +### Compact variant -The compact variant can be used as a collapsed state on smaller screens, or as the primary navigation in a double sidebar layout, see below. +The `compact` variant can be used as a collapsed state on smaller screens, or as the primary navigation in a double sidebar layout, see below. NavItem labels will be rendered as tooltips. Use the `tooltipProps` prop to customize the tooltip. diff --git a/packages/saas-ui-core/src/sidebar/sidebar.stories.tsx b/packages/saas-ui-core/src/sidebar/sidebar.stories.tsx index 1705a0f55..eefb96ead 100644 --- a/packages/saas-ui-core/src/sidebar/sidebar.stories.tsx +++ b/packages/saas-ui-core/src/sidebar/sidebar.stories.tsx @@ -320,8 +320,8 @@ WithSolidLinks.args = { ), } -export const VariantCondensed = Template.bind({}) -VariantCondensed.args = { +export const VariantCompact = Template.bind({}) +VariantCompact.args = { variant: 'compact', children: ( <> @@ -341,8 +341,8 @@ VariantCondensed.args = { ), } -export const VariantCondensedColor = Template.bind({}) -VariantCondensedColor.args = { +export const VariantCompactColor = Template.bind({}) +VariantCompactColor.args = { variant: 'compact', colorScheme: 'purple', children: ( @@ -365,8 +365,8 @@ VariantCondensedColor.args = { ), } -export const VariantCondensedResponsive = Template.bind({}) -VariantCondensedResponsive.args = { +export const VariantCompactResponsive = Template.bind({}) +VariantCompactResponsive.args = { variant: { base: 'compact' }, toggleBreakpoint: false, colorScheme: 'purple', @@ -405,8 +405,8 @@ VariantCondensedResponsive.args = { ), } -export const VariantCondensedNavGroup = Template.bind({}) -VariantCondensedNavGroup.args = { +export const VariantCompactNavGroup = Template.bind({}) +VariantCompactNavGroup.args = { variant: 'compact', colorScheme: 'purple', children: ( diff --git a/packages/saas-ui-forms/src/create-form.tsx b/packages/saas-ui-forms/src/create-form.tsx index fd1af5af8..378893789 100644 --- a/packages/saas-ui-forms/src/create-form.tsx +++ b/packages/saas-ui-forms/src/create-form.tsx @@ -9,12 +9,12 @@ import { GetFieldResolver } from './field-resolver' export interface CreateFormProps< FieldDefs, - ExtraFieldProps extends object = object, + TGetBaseField extends GetBaseField = GetBaseField, > { resolver?: GetResolver fieldResolver?: GetFieldResolver fields?: FieldDefs extends Record> ? FieldDefs : never - getBaseField?: GetBaseField + getBaseField?: TGetBaseField } export type FormType< @@ -39,12 +39,20 @@ export type FormType< id?: string } -export function createForm({ +export function createForm< + FieldDefs, + TGetBaseField extends GetBaseField = GetBaseField, +>({ resolver, fieldResolver = objectFieldResolver, fields, getBaseField, -}: CreateFormProps = {}) { +}: CreateFormProps = {}) { + type ExtraFieldProps = + TGetBaseField extends GetBaseField + ? ExtraFieldProps + : object + const DefaultForm = forwardRef( < TSchema = any, diff --git a/packages/saas-ui-forms/src/form.tsx b/packages/saas-ui-forms/src/form.tsx index a55bb05ec..ccacf9907 100644 --- a/packages/saas-ui-forms/src/form.tsx +++ b/packages/saas-ui-forms/src/form.tsx @@ -34,8 +34,7 @@ import { UseArrayFieldReturn } from './use-array-field' export interface FormRenderContext< TFieldValues extends FieldValues = FieldValues, TContext extends object = object, - TExtraFieldProps extends object = object, - TFieldTypes = FieldProps, + TFieldTypes = FieldProps, > extends UseFormReturn { Field: React.FC> DisplayIf: React.FC> @@ -76,7 +75,7 @@ interface FormOptions< * The form children, can be a render prop or a ReactNode. */ children?: MaybeRenderProp< - FormRenderContext + FormRenderContext > /** * The field resolver, used to resolve the fields from schemas. diff --git a/packages/saas-ui-forms/src/index.ts b/packages/saas-ui-forms/src/index.ts index ca86c9da5..a37550f0a 100644 --- a/packages/saas-ui-forms/src/index.ts +++ b/packages/saas-ui-forms/src/index.ts @@ -178,6 +178,7 @@ export type { FieldOptions, DefaultFieldOverrides, WithStepFields, + GetBaseField, } from './types' // Exporting from './create-form' diff --git a/packages/saas-ui-forms/src/types.ts b/packages/saas-ui-forms/src/types.ts index b6de9e6cf..6cece503d 100644 --- a/packages/saas-ui-forms/src/types.ts +++ b/packages/saas-ui-forms/src/types.ts @@ -97,12 +97,12 @@ type FieldPathWithArray< export type MergeFieldProps< FieldDefs, TFieldValues extends FieldValues = FieldValues, - TCustomProps extends object = object, + TExtraFieldProps extends object = object, TName extends FieldPath = FieldPath, > = ValueOf<{ [K in keyof FieldDefs]: FieldDefs[K] extends React.FC ? { type?: K } & ShallowMerge> & - TCustomProps + TExtraFieldProps : never }> @@ -115,7 +115,7 @@ export type FormChildren< FieldDefs, TFieldValues extends FieldValues = FieldValues, TContext extends object = object, - TCustomProps extends object = object, + TExtraFieldProps extends object = object, > = MaybeRenderProp< FormRenderContext< TFieldValues, @@ -125,7 +125,7 @@ export type FormChildren< ? DefaultFields : ShallowMerge, TFieldValues, - TCustomProps + TExtraFieldProps > > > diff --git a/packages/saas-ui-forms/src/use-array-field.tsx b/packages/saas-ui-forms/src/use-array-field.tsx index ee87b018d..659f937c8 100644 --- a/packages/saas-ui-forms/src/use-array-field.tsx +++ b/packages/saas-ui-forms/src/use-array-field.tsx @@ -64,7 +64,7 @@ export const [ArrayFieldRowProvider, useArrayFieldRowContext] = export interface ArrayFieldOptions< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > { /** * The field name @@ -91,12 +91,13 @@ export const useArrayField = ({ max, }: ArrayFieldOptions) => { const { control } = useFormContext() + const context = useFieldArray({ control, name, keyName, }) - + console.log(context) return { ...context, name, diff --git a/packages/saas-ui-forms/stories/form.stories.tsx b/packages/saas-ui-forms/stories/form.stories.tsx index 325ae2a7d..a2be2f95d 100644 --- a/packages/saas-ui-forms/stories/form.stories.tsx +++ b/packages/saas-ui-forms/stories/form.stories.tsx @@ -148,7 +148,7 @@ const getBaseField: GetBaseField<{ infoLabel?: string }> = () => { const TypedForm = createForm({ fields: { custom: CustomField }, - //getBaseField, + getBaseField, }) export const BasicTyped = () => ( @@ -187,8 +187,8 @@ export const CustomBaseField = () => ( name="custom" type="custom" label="Custom" + customFieldProp="custom" infoLabel="Hello there" - customFieldProps="custom" /> @@ -198,11 +198,13 @@ export const CustomBaseField = () => ( const ZodForm = createZodForm({ fields: { custom: CustomField }, + getBaseField, }) const zodSchema = z.object({ firstName: z.string(), age: z.number(), + custom: z.string().optional(), }) export const WithZodSchema = { @@ -220,6 +222,13 @@ export const WithZodSchema = { + )} @@ -228,7 +237,10 @@ export const WithZodSchema = { }, } -const YupForm = createYupForm() +const YupForm = createYupForm({ + fields: { custom: CustomField }, + getBaseField, +}) const yupSchema = yup.object({ name: yup @@ -243,6 +255,7 @@ const yupSchema = yup.object({ .max(25, 'Too long') .required() .label('Description'), + custom: yup.string(), }) export const WithYupSchema = { @@ -250,17 +263,25 @@ export const WithYupSchema = { return ( {({ Field }) => ( - + )} diff --git a/packages/saas-ui-forms/tests/form.test-d.ts b/packages/saas-ui-forms/tests/form.test-d.ts index 8ed587594..8f6b5cc57 100644 --- a/packages/saas-ui-forms/tests/form.test-d.ts +++ b/packages/saas-ui-forms/tests/form.test-d.ts @@ -1,8 +1,14 @@ import * as zod from 'zod' -import { FieldPath } from 'react-hook-form' +import { FieldPath, FieldValues } from 'react-hook-form' import { expectType } from 'tsd' -import { ArrayFieldPath, FieldOverrides } from '../src/types' +import { + ArrayFieldPath, + BaseFieldProps, + FieldOverrides, + GetBaseField, +} from '../src/types' +import { FormType } from '../src' const schema = zod.object({ name: zod.string(), @@ -71,3 +77,15 @@ type Overrides = { } expectType(test()) + +type TypedForm = FormType< + { + custom: React.FC< + Omit, 'customFieldProp'> & { + customFieldProp?: string | undefined + } + > + }, + { extraProp?: string }, + GetBaseField<{ infoLabel?: string }> +> diff --git a/packages/saas-ui-forms/yup/src/create-yup-form.ts b/packages/saas-ui-forms/yup/src/create-yup-form.ts index 9fa7f7238..75fd26aa6 100644 --- a/packages/saas-ui-forms/yup/src/create-yup-form.ts +++ b/packages/saas-ui-forms/yup/src/create-yup-form.ts @@ -4,6 +4,7 @@ import { FormProps, WithFields, FieldValues, + GetBaseField, } from '@saas-ui/forms' import { yupFieldResolver, yupResolver } from './yup-resolver' import { InferType } from 'yup' @@ -12,8 +13,10 @@ import { AnyObjectSchema } from './types' type ResolverArgs = Parameters -export interface CreateYupFormProps - extends CreateFormProps { +export interface CreateYupFormProps< + FieldDefs, + TGetBaseField extends GetBaseField = GetBaseField, +> extends CreateFormProps { schemaOptions?: ResolverArgs[1] resolverOptions?: ResolverArgs[2] } @@ -21,15 +24,16 @@ export interface CreateYupFormProps export type YupFormType< FieldDefs, ExtraProps = object, + ExtraFieldProps extends object = object, ExtraOverrides = object, - Type extends 'yup' = 'yup' + Type extends 'yup' = 'yup', > = (< TSchema extends AnyObjectSchema = AnyObjectSchema, TFieldValues extends InferType = InferType, // placeholder - TContext extends object = object + TContext extends object = object, >( props: WithFields< - FormProps, + FormProps, FieldDefs, ExtraOverrides > & { @@ -40,9 +44,17 @@ export type YupFormType< id?: 'YupForm' } -export const createYupForm = ( - options?: CreateYupFormProps +export const createYupForm = < + FieldDefs, + TGetBaseField extends GetBaseField = GetBaseField, +>( + options?: CreateYupFormProps ) => { + type ExtraFieldProps = + TGetBaseField extends GetBaseField + ? ExtraFieldProps + : object + const YupForm = createForm({ resolver: (schema: any) => yupResolver( @@ -57,5 +69,5 @@ export const createYupForm = ( YupForm.displayName = 'YupForm' YupForm.id = 'YupForm' - return YupForm as YupFormType + return YupForm as YupFormType } diff --git a/packages/saas-ui-forms/zod/src/create-zod-form.ts b/packages/saas-ui-forms/zod/src/create-zod-form.ts index 457da7f86..628269b45 100644 --- a/packages/saas-ui-forms/zod/src/create-zod-form.ts +++ b/packages/saas-ui-forms/zod/src/create-zod-form.ts @@ -3,14 +3,17 @@ import { CreateFormProps, WithFields, FormProps, + GetBaseField, } from '@saas-ui/forms' import { zodFieldResolver, zodResolver } from './zod-resolver' import { z } from 'zod' type ResolverArgs = Parameters -export interface CreateZodFormProps - extends CreateFormProps { +export interface CreateZodFormProps< + FieldDefs, + TGetBaseField extends GetBaseField = GetBaseField, +> extends CreateFormProps { schemaOptions?: ResolverArgs[1] resolverOptions?: ResolverArgs[2] } @@ -18,15 +21,16 @@ export interface CreateZodFormProps export type ZodFormType< FieldDefs, ExtraProps = object, + ExtraFieldProps extends object = object, ExtraOverrides = object, - Type extends 'zod' = 'zod' + Type extends 'zod' = 'zod', > = (< TSchema extends z.AnyZodObject = z.AnyZodObject, TFieldValues extends z.infer = z.infer, - TContext extends object = object + TContext extends object = object, >( props: WithFields< - FormProps, + FormProps, FieldDefs, ExtraOverrides > & { @@ -37,9 +41,17 @@ export type ZodFormType< id?: string } -export const createZodForm = ( - options?: CreateZodFormProps +export const createZodForm = < + FieldDefs, + TGetBaseField extends GetBaseField = GetBaseField, +>( + options?: CreateZodFormProps ) => { + type ExtraFieldProps = + TGetBaseField extends GetBaseField + ? ExtraFieldProps + : object + const ZodForm = createForm({ resolver: (schema: any) => zodResolver(schema, options?.schemaOptions, options?.resolverOptions), @@ -50,5 +62,5 @@ export const createZodForm = ( ZodForm.displayName = 'ZodForm' ZodForm.id = 'ZodForm' - return ZodForm as ZodFormType + return ZodForm as ZodFormType }