Skip to content

Commit

Permalink
Merge pull request #40 from stevent-team/fix/nested-zod-object-types
Browse files Browse the repository at this point in the history
Fix nested zod types in the field chain
  • Loading branch information
GRA0007 committed Aug 12, 2023
2 parents b6aa5c9 + f403cf8 commit 1b953a2
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-clouds-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stevent-team/react-zoom-form": patch
---

Remove lib export of internal register function
5 changes: 5 additions & 0 deletions .changeset/popular-pianos-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stevent-team/react-zoom-form": patch
---

Properly pass through the correct Zod schema types to the field chain using a second InnerSchema type param
9 changes: 2 additions & 7 deletions examples/kitchen-sink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,8 @@ const initialValues = {
defaultString: 'Default value',
}

const KitchenSink = () => {
const { fields, handleSubmit, isDirty, reset } = useForm({ schema, initialValues })

const onSubmit: SubmitHandler<typeof schema> = values => {
console.log(values)
reset(values)
}
const KitchenSink = ({ onSubmit }: { onSubmit: SubmitHandler<typeof schema> }) => {
const { fields, handleSubmit, isDirty } = useForm({ schema, initialValues })

return <>
<form onSubmit={handleSubmit(onSubmit)}>
Expand Down
67 changes: 4 additions & 63 deletions lib/field.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { z } from 'zod'
import { NonUndefined, PartialObject, PathSegment, RecursivePartial, arrayStartsWith, getDeepProp, isRadio, setDeepProp, unwrapZodType } from './utils'
import { FieldRefs } from './useForm'
import { NonUndefined, PartialObject, PathSegment, RecursivePartial, arrayStartsWith, getDeepProp, setDeepProp } from './utils'

/** The controls that each path along the field chain can access under `_field`. */
export type Field<Schema extends z.ZodType = z.ZodType> = {
Expand Down Expand Up @@ -35,7 +34,7 @@ export type ControlledField<T> = {
*/
name: string
/** The zod schema for this field. */
schema: z.ZodType<T>
schema: z.ZodType<T | undefined>
/** Reactive value of this field. */
value: PartialObject<T> | undefined
/** Takes a new value to set `value` of this field. */
Expand All @@ -44,64 +43,6 @@ export type ControlledField<T> = {
errors: z.ZodIssue[]
}

/** Options that can be passed to the register fn. */
export type RegisterOptions = {
ref?: React.ForwardedRef<any>
}

/** Type of the `.register()` function for native elements. */
export type RegisterFn = (
path: PathSegment[],
schema: z.ZodType,
setFormValue: React.Dispatch<React.SetStateAction<RecursivePartial<z.ZodType>>>,
fieldRefs: React.MutableRefObject<FieldRefs>,
options: RegisterOptions,
) => {
onChange: React.ChangeEventHandler<any>
ref: React.Ref<any>
name: string
}

// Register for native elements (input, textarea, select)
export const register: RegisterFn = (path, fieldSchema, setFormValue, fieldRefs, options) => {
const name = path.map(p => p.key).join('.')
const unwrapped = unwrapZodType(fieldSchema)

return {
onChange: e => {
let newValue: string | boolean | undefined = e.currentTarget.value
if (!(unwrapped instanceof z.ZodString) && newValue === '') {
newValue = undefined
}
// If this field uses a checkbox, read it's `checked` state
if (e.currentTarget.type?.toLowerCase() === 'checkbox') {
newValue = e.currentTarget.checked
}
setFormValue(v => setDeepProp(v, path, newValue) as typeof v)
},
name,
ref: ref => {
// Store field ref in an object to dedupe them per field
if (ref) {
// If the user has provided their own ref to use as well
if (options.ref) {
if (typeof options.ref === 'function') {
options.ref(ref)
} else {
options.ref.current = ref
}
}

// Note, radio fields use the same name per group, so they have to be referenced by value
const refIndex = isRadio(ref) ? `${name}.${ref.value}` : name
fieldRefs.current[refIndex] = { path, ref }
} else {
delete fieldRefs.current[name]
}
},
} satisfies React.ComponentProps<'input'>
}

/**
* Control a custom field. Takes the field you want to control from
* `fields` given by the `useForm` hook, and returns an object with
Expand All @@ -112,11 +53,11 @@ export const register: RegisterFn = (path, fieldSchema, setFormValue, fieldRefs,
* <CustomField {...controlled(fields.myCustomField)} />
* ```
*/
export const controlled = <T>({ _field }: Field<z.ZodType<NonUndefined<T>>>): ControlledField<NonUndefined<T>> => {
export const controlled = <T>({ _field }: Field<z.ZodType<T>>): ControlledField<NonUndefined<T>> => {
const { schema, path, formValue, setFormValue, formErrors } = _field

return {
schema,
schema: schema as z.ZodType<NonUndefined<T>, z.ZodTypeDef, NonUndefined<T>>,
name: path.map(p => p.key).join('.'),
value: getDeepProp(formValue.current, path) as PartialObject<NonUndefined<T>> | undefined,
onChange: value => setFormValue(v => setDeepProp(v, path, value) as typeof v),
Expand Down
63 changes: 60 additions & 3 deletions lib/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { z } from 'zod'

import { PathSegment, RecursivePartial, fieldChain, getDeepProp, deepEqual, FieldChain, isCheckbox, isRadio } from './utils'
import { register } from './field'
import { PathSegment, RecursivePartial, fieldChain, getDeepProp, deepEqual, FieldChain, isCheckbox, isRadio, unwrapZodType, setDeepProp } from './utils'

export interface UseFormOptions<Schema extends z.ZodTypeAny> {
/**
Expand Down Expand Up @@ -118,7 +117,7 @@ export const useForm = <Schema extends z.ZodTypeAny>({

const fields = useMemo(() => new Proxy({}, {
get: (_target, key) => fieldChain(schema, [], register, fieldRefs, { formValue, setFormValue, formErrors })[key]
}) as FieldChain<Schema>, [schema, setFormValue, formErrors])
}) as unknown as FieldChain<Schema>, [schema, setFormValue, formErrors])

return {
fields,
Expand All @@ -127,3 +126,61 @@ export const useForm = <Schema extends z.ZodTypeAny>({
reset,
}
}

/** Options that can be passed to the register fn. */
export type RegisterOptions = {
ref?: React.ForwardedRef<any>
}

/** Type of the `.register()` function for native elements. */
export type RegisterFn = (
path: PathSegment[],
schema: z.ZodType,
setFormValue: React.Dispatch<React.SetStateAction<RecursivePartial<z.ZodType>>>,
fieldRefs: React.MutableRefObject<FieldRefs>,
options: RegisterOptions,
) => {
onChange: React.ChangeEventHandler<any>
ref: React.Ref<any>
name: string
}

// Register for native elements (input, textarea, select)
const register: RegisterFn = (path, fieldSchema, setFormValue, fieldRefs, options) => {
const name = path.map(p => p.key).join('.')
const unwrapped = unwrapZodType(fieldSchema)

return {
onChange: e => {
let newValue: string | boolean | undefined = e.currentTarget.value
if (!(unwrapped instanceof z.ZodString) && newValue === '') {
newValue = undefined
}
// If this field uses a checkbox, read it's `checked` state
if (e.currentTarget.type?.toLowerCase() === 'checkbox') {
newValue = e.currentTarget.checked
}
setFormValue(v => setDeepProp(v, path, newValue) as typeof v)
},
name,
ref: ref => {
// Store field ref in an object to dedupe them per field
if (ref) {
// If the user has provided their own ref to use as well
if (options.ref) {
if (typeof options.ref === 'function') {
options.ref(ref)
} else {
options.ref.current = ref
}
}

// Note, radio fields use the same name per group, so they have to be referenced by value
const refIndex = isRadio(ref) ? `${name}.${ref.value}` : name
fieldRefs.current[refIndex] = { path, ref }
} else {
delete fieldRefs.current[name]
}
},
} satisfies React.ComponentProps<'input'>
}
13 changes: 8 additions & 5 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Field, FieldRefs, RegisterFn, RegisterOptions } from '.'
import { z } from 'zod'

type AnyZodContainer = z.AnyZodObject | z.AnyZodTuple | z.ZodArray<any>
// A Zod object that can hold nested data
type AnyZodContainer = z.AnyZodObject | z.AnyZodTuple | z.ZodArray<any> | z.ZodRecord | z.ZodMap | z.ZodSet

// Creates the type for the field chain by recusively travelling through the Zod schema
type recursiveFieldChain<Schema extends z.ZodType, LeafValue> =
type RecursiveFieldChain<Schema extends z.ZodType, LeafValue> =
z.infer<Schema> extends LeafValue ? z.infer<Schema>
: Schema extends z.AnyZodTuple ? { [K in keyof z.infer<Schema>]: FieldChain<Schema['_type'][K]> }
: Schema extends z.ZodArray<any> ? { [k: number]: FieldChain<Schema['_def']['type']> }
: Schema extends z.AnyZodObject ? { [K in keyof z.infer<Schema>]: FieldChain<Schema['shape'][K]> }
: Schema extends (z.ZodDefault<AnyZodContainer> | z.ZodOptional<AnyZodContainer> | z.ZodNullable<AnyZodContainer>) ? FieldChain<Schema['_def']['innerType']>
: Schema extends (z.ZodEffects<AnyZodContainer>) ? FieldChain<Schema['_def']['schema']>
: Schema extends (z.ZodDefault<AnyZodContainer> | z.ZodOptional<AnyZodContainer> | z.ZodNullable<AnyZodContainer>) ? FieldChain<Schema, Schema['_def']['innerType']>
: Schema extends z.ZodEffects<AnyZodContainer> ? FieldChain<Schema, Schema['_def']['schema']>
: Schema extends z.ZodLazy<AnyZodContainer> ? FieldChain<Schema, ReturnType<Schema['_def']['getter']>>
: Schema extends z.ZodPipeline<AnyZodContainer, AnyZodContainer> ? FieldChain<Schema, Schema['_def']['out']>
: LeafValue

export type FieldChain<Schema extends z.ZodType> = Field<Schema> & Required<recursiveFieldChain<Schema, {
export type FieldChain<Schema extends z.ZodType, InnerSchema extends z.ZodType = Schema> = Field<Schema> & Required<RecursiveFieldChain<InnerSchema, {
/**
* Provides props to pass to native elements (input, textarea, select)
*
Expand Down

0 comments on commit 1b953a2

Please sign in to comment.