Skip to content

Commit

Permalink
feat(zui): Allow validation with a dataTransform, improve path handli…
Browse files Browse the repository at this point in the history
…ng (#339)
  • Loading branch information
charlescatta authored Jun 26, 2024
1 parent 7308095 commit cf1ff88
Show file tree
Hide file tree
Showing 7 changed files with 36 additions and 19 deletions.
2 changes: 1 addition & 1 deletion zui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bpinternal/zui",
"version": "0.8.11",
"version": "0.8.12",
"description": "An extension of Zod for working nicely with UIs and JSON Schemas",
"type": "module",
"source": "./src/index.ts",
Expand Down
1 change: 1 addition & 0 deletions zui/src/ui/ElementRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const FormElementRenderer: FC<FormRendererProps> = ({
formErrors,
formValid,
updateForm: handlePropertyChange,
updateFormData: (data) => handlePropertyChange([], data),
},
onChange: (data: any) => handlePropertyChange(path, data),
disabled,
Expand Down
4 changes: 2 additions & 2 deletions zui/src/ui/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { Component, ErrorInfo, FC, ReactNode } from 'react'
import { JSONSchema } from './types'
import { JSONSchema, Path } from './types'

export type BoundaryFallbackComponent = FC<{ error: Error; schema: JSONSchema }>

export type ErrorBoundaryProps = {
children?: ReactNode
fallback?: BoundaryFallbackComponent
fieldSchema: JSONSchema
path: string[]
path: Path
}
type State =
| {
Expand Down
4 changes: 2 additions & 2 deletions zui/src/ui/hooks/useDiscriminator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useMemo, useEffect } from 'react'
import { JSONSchema, ObjectSchema } from '../types'
import { JSONSchema, ObjectSchema, Path } from '../types'
import { useFormData } from './useFormData'
import { zuiKey } from '../constants'

export const useDiscriminator = (fieldSchema: JSONSchema, path: string[]) => {
export const useDiscriminator = (fieldSchema: JSONSchema, path: Path) => {
const { handlePropertyChange, data } = useFormData(fieldSchema, path)

const { discriminator, value, discriminatedSchema } = useMemo(() => {
Expand Down
26 changes: 17 additions & 9 deletions zui/src/ui/hooks/useFormData.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { ArraySchema, JSONSchema } from '../types'
import { ArraySchema, FormError, JSONSchema, Path } from '../types'
import { jsonSchemaToZui } from '../../transforms/json-schema-to-zui'
import { zuiKey } from '../constants'
import { Maskable, ZodIssue } from '../../z'
import { pathMatches } from '../utils'

export type FormDataContextProps = {
/**
Expand Down Expand Up @@ -100,7 +101,7 @@ const parseMaskableField = (key: 'hidden' | 'disabled', fieldSchema: JSONSchema,
return false
}

export const useFormData = (fieldSchema: JSONSchema, path: string[]) => {
export const useFormData = (fieldSchema: JSONSchema, path: Path) => {
const formContext = useContext(FormDataContext)
if (formContext === undefined) {
throw new Error('useFormData must be used within a FormDataProvider')
Expand All @@ -115,7 +116,14 @@ export const useFormData = (fieldSchema: JSONSchema, path: string[]) => {
if (formContext.validation.formValid === false) {
return {
formValid: false,
formErrors: formContext.validation.formErrors?.filter((issue) => issue.path === path) || null,
formErrors:
formContext.validation.formErrors
?.filter((issue) => pathMatches(issue.path, path))
.map<FormError>((issue) => ({
message: issue.message,
code: issue.code,
path: path,
})) || null,
}
}
return { formValid: true, formErrors: [] }
Expand Down Expand Up @@ -144,13 +152,13 @@ export const useFormData = (fieldSchema: JSONSchema, path: string[]) => {
}, [formContext.hiddenState, formContext.disabledState, hiddenMask, disabledMask, path])

const handlePropertyChange = useCallback(
(path: string[], data: any) => {
(path: Path, data: any) => {
formContext.setFormData((formData) => setObjectPath(formData, path, data))
},
[formContext.setFormData],
)
const addArrayItem = useCallback(
(path: string[], data: any = undefined) => {
(path: Path, data: any = undefined) => {
const defaultData = getDefaultValues((fieldSchema as ArraySchema).items)

formContext.setFormData((formData) => {
Expand All @@ -165,7 +173,7 @@ export const useFormData = (fieldSchema: JSONSchema, path: string[]) => {
)

const removeArrayItem = useCallback(
(path: string[], index: number) => {
(path: Path, index: number) => {
formContext.setFormData((formData) => {
const currentData = getPathData(formData, path) || []

Expand All @@ -183,14 +191,14 @@ export const useFormData = (fieldSchema: JSONSchema, path: string[]) => {
return { ...formContext, data, disabled, hidden, handlePropertyChange, addArrayItem, removeArrayItem, ...validation }
}

export function setObjectPath(obj: any, path: string[], data: any): any {
export function setObjectPath(obj: any, path: Path, data: any): any {
if (path.length === 0) {
return data
}

const pathLength = path.length

path.reduce((current: any, key: string, index: number) => {
path.reduce((current: any, key: string | number, index: number) => {
if (index === pathLength - 1) {
current[key] = data
} else {
Expand Down Expand Up @@ -321,7 +329,7 @@ export const FormDataProvider: React.FC<PropsWithChildren<FormDataProviderProps>
)
}

export function getPathData(object: any, path: string[]): any {
export function getPathData(object: any, path: Path): any {
return path.reduce((prev, curr) => {
return prev ? prev[curr] : null
}, object)
Expand Down
11 changes: 7 additions & 4 deletions zui/src/ui/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ZodEnumDef, z } from '../z/index'
import { ZodEnumDef, ZodIssueCode, z } from '../z/index'
import type { FC } from 'react'
import { zuiKey } from './constants'

Expand Down Expand Up @@ -208,7 +208,8 @@ export type SchemaContext<

export type FormError = {
message: string
path: (string | number)[]
path: Path
code: ZodIssueCode
}

export type ZuiReactComponentBaseProps<
Expand All @@ -227,12 +228,13 @@ export type ZuiReactComponentBaseProps<
label: string
errors: FormError[]
context: {
path: string[]
path: Path
formValid: boolean | null
formErrors: FormError[] | null
formData?: any
readonly: boolean
updateForm: (path: string[], data: any) => void
updateForm: (path: Path, data: any) => void
updateFormData: (data: any) => void
}
zuiProps: BaseSchema[typeof zuiKey]
} & ZuiReactArrayChildProps
Expand Down Expand Up @@ -330,3 +332,4 @@ export type MergeUIComponentDefinitions<T extends UIComponentDefinitions, U exte
}

export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
export type Path = (string | number)[]
7 changes: 6 additions & 1 deletion zui/src/ui/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { zuiKey } from './constants'
import { resolveDiscriminator } from './hooks/useDiscriminator'
import { BaseType, JSONSchema, ZuiComponentMap, ZuiReactComponent } from './types'
import { BaseType, JSONSchema, Path, ZuiComponentMap, ZuiReactComponent } from './types'

type ComponentMeta<Type extends BaseType = BaseType> = {
type: Type
Expand Down Expand Up @@ -62,6 +62,11 @@ export const resolveComponent = <Type extends BaseType>(
}
}

export function pathMatches(path1: Path, path2: Path): boolean {
if (path1.length !== path2.length) return false
return path1.every((part, index) => part === path2[index])
}

export function formatTitle(title: string, separator?: RegExp): string {
if (!separator) separator = new RegExp('/s|-|_| ', 'g')
return decamelize(title).split(separator).map(capitalize).map(handleSpecialWords).reduce(combine)
Expand Down

0 comments on commit cf1ff88

Please sign in to comment.