Skip to content

Commit

Permalink
fix(form): fix issue where FormInput was not rendering field when pas…
Browse files Browse the repository at this point in the history
…sed 'includeField' (#7350)

* chore(test-studio): add example schema + custom FormInput example/playground

* fix(form): fix issue where FormInput was not rendering field when passed 'includeField'
  • Loading branch information
bjoerge authored Aug 15, 2024
1 parent 7531cda commit e6185ef
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 28 deletions.
82 changes: 82 additions & 0 deletions dev/test-studio/components/formBuilder/FormInputExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {CloseIcon, EyeOpenIcon} from '@sanity/icons'
import {Box, Button, Card, Checkbox, Flex, Inline, Stack, Text} from '@sanity/ui'
import {useState} from 'react'
import {
FormInput,
type ObjectInputProps,
type Path,
pathToString,
type RenderInputCallback,
} from 'sanity'

export function FormInputExample(props: ObjectInputProps) {
const [path, setPath] = useState<Path>([])

const [includeField, setIncludeField] = useState(false)
const [includeItem, setIncludeItem] = useState(false)

const renderDefaultForm = path.length === 0

const renderInput: RenderInputCallback = (inputProps) => {
// wraps each input with a button that allows rendering only the selected input
const selected = inputProps.path === path
return (
<Flex>
<Box flex={1}>{props.renderInput(inputProps)}</Box>
<Flex marginLeft={2}>
<Button
mode="ghost"
tone="primary"
fontSize={1}
onClick={() => setPath(selected ? [] : inputProps.path)}
icon={selected ? CloseIcon : EyeOpenIcon}
/>
</Flex>
</Flex>
)
}

if (renderDefaultForm) {
return (
<Stack space={2}>
<Card shadow={2} margin={3} padding={4} radius={2}>
{props.renderDefault({...props, renderInput})}
</Card>
</Stack>
)
}
return (
<Stack space={2}>
<Card padding={3} radius={2}>
<Stack space={4}>
<Stack space={4}>
<Flex gap={2}>
<Text weight="semibold">
Input at <code>{pathToString(path)}</code>
</Text>
</Flex>
<Flex gap={4}>
<Inline space={2}>
<Checkbox checked={includeField} onChange={() => setIncludeField((v) => !v)} />{' '}
<Text>Include field</Text>
</Inline>
<Inline space={2}>
<Checkbox checked={includeItem} onChange={() => setIncludeItem((v) => !v)} />{' '}
<Text>Include item</Text>
</Inline>
</Flex>
</Stack>
<Card shadow={2} padding={3} radius={2}>
<FormInput
{...props}
renderInput={renderInput}
absolutePath={path}
includeField={includeField}
includeItem={includeItem}
/>
</Card>
</Stack>
</Card>
</Stack>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {defineType} from '@sanity/types'

import {FormInputExample} from '../../../../components/formBuilder/FormInputExample'
import {structureGroupOptions} from '../../../../structure/groupByOption'

export const formInputTest = defineType({
name: 'formInputTest',
title: 'FormInput Test',
type: 'document',
options: structureGroupOptions({
structureGroup: 'v3',
}),
components: {
input: FormInputExample,
},
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'array',
type: 'array',
of: [
{
type: 'object',
fields: [
{name: 'title', type: 'string'},
{name: 'text', type: 'string'},
],
},
],
},
{
name: 'description',
title: 'Description',
type: 'text',
},
{
name: 'nested',
type: 'object',
fields: [{name: 'nested', type: 'string'}],
},
],
})
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './formInputTest'
export * from './schema'
4 changes: 2 additions & 2 deletions dev/test-studio/schema/docs/v3/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {validationTest} from './async-functions/schemaType'
import {example1SchemaType} from './example1'
import {formComponentsSchema} from './form-components-api'
import {formComponentsSchema, formInputTest} from './form-components-api'

export const v3docs = {
types: [example1SchemaType, validationTest, formComponentsSchema],
types: [example1SchemaType, validationTest, formComponentsSchema, formInputTest],
}
59 changes: 33 additions & 26 deletions packages/sanity/src/core/form/components/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const FormInput = memo(function FormInput(
* Whether to include the field around the input. Defaults to false
*/
includeField?: boolean
includeItem?: boolean
},
) {
const absolutePath = useMemo(() => {
Expand Down Expand Up @@ -101,6 +102,34 @@ const FormInputInner = memo(function FormInputInner(

const {t} = useTranslation()

const renderField: RenderFieldCallback = useCallback(
(fieldProps) => {
// we want to render the field around the input if either of these are true:
// 1. we have reached the destination path and the `includeField`-prop is passed as true
// 2. we are currently at a node somewhere below/inside the destination path
const atDestination = isEqual(absolutePath, fieldProps.path)
const shouldRenderField = atDestination
? props.includeField
: startsWith(absolutePath, fieldProps.path)
return shouldRenderField ? destinationRenderField(fieldProps) : pass(fieldProps)
},
[absolutePath, destinationRenderField, props.includeField],
)

const renderItem: RenderArrayOfObjectsItemCallback = useCallback(
(itemProps) => {
// we want to render the item around the input if either of these are true:
// 1. we have reached the destination path and the `includeItem`-prop is passed as true
// 2. we are currently at a node somewhere below/inside the destination path
const atDestination = isEqual(absolutePath, itemProps.path)
const shouldRenderItem = atDestination
? props.includeItem
: startsWith(absolutePath, itemProps.path)
return shouldRenderItem ? destinationRenderItem(itemProps) : pass(itemProps)
},
[absolutePath, destinationRenderItem, props.includeItem],
)

const renderInput: RenderInputCallback = useCallback(
(inputProps) => {
const isDestinationReached =
Expand All @@ -120,6 +149,8 @@ const FormInputInner = memo(function FormInputInner(
return (
<FormInputInner
{...inputProps}
includeField={props.includeField}
includeItem={props.includeItem}
absolutePath={absolutePath}
destinationRenderAnnotation={destinationRenderAnnotation}
destinationRenderBlock={destinationRenderBlock}
Expand All @@ -140,35 +171,11 @@ const FormInputInner = memo(function FormInputInner(
destinationRenderInput,
destinationRenderItem,
destinationRenderPreview,
props.includeField,
props.includeItem,
],
)

const renderField: RenderFieldCallback = useCallback(
(fieldProps) => {
// we want to render the field around the input if either of these are true:
// 1. we have reached the destination path and the `includeField`-prop is passed as true
// 2. we are currently at a node somewhere below/inside the destination path
const shouldRenderField =
startsWith(absolutePath, fieldProps.path) &&
(props.includeField || !isEqual(absolutePath, fieldProps.path))
return shouldRenderField ? destinationRenderField(fieldProps) : pass(fieldProps)
},
[absolutePath, destinationRenderField, props.includeField],
)

const renderItem: RenderArrayOfObjectsItemCallback = useCallback(
(itemProps) => {
// we want to render the item around the input if either of these are true:
// 1. we have reached the destination path and the `includeItem`-prop is passed as true
// 2. we are currently at a node somewhere below/inside the destination path
const shouldRenderField =
startsWith(absolutePath, itemProps.path) &&
(props.includeItem || !isEqual(absolutePath, itemProps.path))
return shouldRenderField ? destinationRenderItem(itemProps) : pass(itemProps)
},
[absolutePath, destinationRenderItem, props.includeItem],
)

const renderBlock: RenderBlockCallback = useCallback(
(blockProps) => {
const shouldRenderBlock =
Expand Down

0 comments on commit e6185ef

Please sign in to comment.