Skip to content

Commit b37e5d6

Browse files
fix(NestedAntDFormContext): TextControl state management (#142)
* Update tsconfig build * WiP * Backfill test on numeric arrays * 1 bug down, one introduced * Wip * It works * A bit of cleanup * Back out bundling changes * Document a bit * Add numeric test case for object array * More cleanup * Remove unused type * Add a return type
1 parent 6dce91a commit b37e5d6

9 files changed

Lines changed: 262 additions & 113 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createContext } from "react"
2+
3+
export interface NestedAntDFormData {
4+
path: string
5+
index?: number
6+
}
7+
8+
/**
9+
* Context to store the nested AntD form data.
10+
*
11+
* This is used for for passing down data across nested form items in places
12+
* where we use JsonFormsDispatch to render nested form items.
13+
*/
14+
export const NestedAntDFormContext = createContext<
15+
NestedAntDFormData | undefined
16+
>(undefined)

src/controls/ObjectArrayControl.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
objectArrayWithCombinator_FavoriteThing1UISchemaRegistryEntry as objectArrayWithCombinator_CombinatorSubschemaUISchemaRegistryEntry,
1414
arrayControlSortableUISchema,
1515
arrayControlSortableWithIconsUISchema,
16+
objectArrayWithNumericFieldControlJsonSchema,
17+
peopleArrayControlUISchema,
1618
} from "../testSchemas/arraySchema"
1719
import { UISchema } from "../ui-schema"
1820
import { JSONFormData } from "../common/schema-derived-types"
@@ -122,6 +124,49 @@ describe("ObjectArrayControl", () => {
122124
screen.getByDisplayValue("my other asset")
123125
})
124126

127+
test("correctly removes from the object list with remove button", async () => {
128+
let data: JSONFormData<
129+
typeof objectArrayWithNumericFieldControlJsonSchema
130+
> = {
131+
people: [],
132+
}
133+
const user = userEvent.setup()
134+
render({
135+
schema: objectArrayWithNumericFieldControlJsonSchema,
136+
uischema: peopleArrayControlUISchema,
137+
data: data,
138+
onChange: (result) => {
139+
data = result.data as JSONFormData<
140+
typeof objectArrayWithNumericFieldControlJsonSchema
141+
>
142+
},
143+
})
144+
const addButton = await screen.findByRole("button", { name: "Add People" })
145+
await user.click(addButton)
146+
await user.click(addButton)
147+
const inputFields = await screen.findAllByLabelText("Age")
148+
await user.type(inputFields[0], "25")
149+
await user.type(inputFields[1], "30")
150+
await user.type(inputFields[2], "35")
151+
152+
const removeButtons = await screen.findAllByRole("button", {
153+
name: "Delete",
154+
})
155+
expect(removeButtons).toHaveLength(3)
156+
await user.click(removeButtons[1])
157+
await waitFor(() => {
158+
expect(data).toEqual({
159+
people: [{ age: 25 }, { age: 35 }],
160+
})
161+
})
162+
const updatedRemoveButtons = screen.getAllByRole("button", {
163+
name: "Delete",
164+
})
165+
expect(updatedRemoveButtons).toHaveLength(2)
166+
screen.getByDisplayValue("25")
167+
screen.getByDisplayValue("35")
168+
})
169+
125170
test("renders with overwritten icons and does not allow overwriting onClick", async () => {
126171
const user = userEvent.setup()
127172
let data: JSONFormData<typeof objectArrayControlJsonSchema> = {

src/controls/ObjectArrayControl.tsx

Lines changed: 69 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useEffect, useMemo } from "react"
1515
import { ArrayControlOptions, ControlUISchema } from "../ui-schema"
1616
import { usePreviousValue } from "../common/usePreviousValue"
1717
import React from "react"
18+
import { NestedAntDFormContext } from "../contexts/NestedAntDFormContext"
1819

1920
type ArrayLayoutProps = Omit<JSFArrayLayoutProps, "uischema"> & {
2021
uischema: ControlUISchema<unknown> | JSFArrayLayoutProps["uischema"]
@@ -81,13 +82,14 @@ export function ObjectArrayControl({
8182
return moveDown?.(path, index)()
8283
}
8384

84-
const addButton = (
85+
const addButton = (antdAdd: () => void) => (
8586
<Flex justify="center">
8687
<Button
8788
{...options.addButtonProps}
8889
onClick={(e) => {
8990
e.stopPropagation()
9091
addItemToList()
92+
antdAdd()
9193
}}
9294
>
9395
{options.addButtonProps?.children ?? `Add ${label}`}
@@ -98,6 +100,7 @@ export function ObjectArrayControl({
98100
if (!visible) {
99101
return null
100102
}
103+
101104
return (
102105
<Form.Item
103106
required={required}
@@ -106,67 +109,74 @@ export function ObjectArrayControl({
106109
{...formItemProps}
107110
>
108111
<>{label}</>
109-
<List<unknown>
110-
dataSource={dataSource}
111-
renderItem={(_item: unknown, index: number) => {
112-
return (
113-
<List.Item
114-
key={index}
115-
actions={[
116-
dataSource.length > 1 && options.showSortButtons ? (
117-
<Space key="sort">
118-
<Button
119-
aria-label={`Move up`}
120-
disabled={index === 0}
121-
{...options.moveUpButtonProps}
122-
onClick={handleUpClick(path, index)}
123-
>
124-
{!options.moveUpButtonProps?.icon && "Up"}
125-
</Button>
112+
<Form.List name={path}>
113+
{(_, { add, remove }) => (
114+
<List<unknown>
115+
dataSource={dataSource}
116+
renderItem={(_item: unknown, index: number) => {
117+
return (
118+
<List.Item
119+
key={index}
120+
actions={[
121+
dataSource.length > 1 && options.showSortButtons ? (
122+
<Space key="sort">
123+
<Button
124+
aria-label={`Move up`}
125+
disabled={index === 0}
126+
{...options.moveUpButtonProps}
127+
onClick={handleUpClick(path, index)}
128+
>
129+
{!options.moveUpButtonProps?.icon && "Up"}
130+
</Button>
131+
<Button
132+
aria-label={`Move down`}
133+
disabled={index === dataSource.length - 1}
134+
{...options.moveDownButtonProps}
135+
onClick={handleDownClick(path, index)}
136+
>
137+
{!options.moveDownButtonProps?.icon && "Down"}
138+
</Button>
139+
</Space>
140+
) : undefined,
126141
<Button
127-
aria-label={`Move down`}
128-
disabled={index === dataSource.length - 1}
129-
{...options.moveDownButtonProps}
130-
onClick={handleDownClick(path, index)}
142+
key="remove"
143+
{...options.removeButtonProps}
144+
disabled={
145+
!removeItems ||
146+
(required && dataSource.length == 1 && index === 0)
147+
}
148+
onClick={(e) => {
149+
e.stopPropagation()
150+
removeItems?.(path, [index])()
151+
remove(index)
152+
}}
131153
>
132-
{!options.moveDownButtonProps?.icon && "Down"}
133-
</Button>
134-
</Space>
135-
) : undefined,
136-
<Button
137-
key="remove"
138-
{...options.removeButtonProps}
139-
disabled={
140-
!removeItems ||
141-
(required && dataSource.length == 1 && index === 0)
142-
}
143-
onClick={(e) => {
144-
e.stopPropagation()
145-
removeItems?.(path, [index])()
146-
}}
154+
{options.removeButtonProps?.children ?? "Delete"}
155+
</Button>,
156+
].filter(Boolean)}
147157
>
148-
{options.removeButtonProps?.children ?? "Delete"}
149-
</Button>,
150-
].filter(Boolean)}
151-
>
152-
<div style={{ width: "100%" }}>
153-
<JsonFormsDispatch
154-
enabled={enabled}
155-
schema={schema}
156-
path={composePaths(path, `${index}`)}
157-
uischema={foundUISchema}
158-
renderers={renderers}
159-
cells={cells}
160-
uischemas={uischemas}
161-
/>
162-
</div>
163-
</List.Item>
164-
)
165-
}}
166-
{...(options.addButtonLocation === "top"
167-
? { header: addButton }
168-
: { footer: addButton })}
169-
/>
158+
<div style={{ width: "100%" }}>
159+
<NestedAntDFormContext.Provider value={{ path, index }}>
160+
<JsonFormsDispatch
161+
enabled={enabled}
162+
schema={schema}
163+
path={composePaths(path, `${index}`)}
164+
uischema={foundUISchema}
165+
renderers={renderers}
166+
cells={cells}
167+
uischemas={uischemas}
168+
/>
169+
</NestedAntDFormContext.Provider>
170+
</div>
171+
</List.Item>
172+
)
173+
}}
174+
{...(options.addButtonLocation === "top"
175+
? { header: addButton(add) }
176+
: { footer: addButton(add) })}
177+
/>
178+
)}
179+
</Form.List>
170180
</Form.Item>
171181
)
172182
}

src/controls/PrimitiveArrayControl.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,33 @@ describe("PrimitiveArrayControl", () => {
118118
screen.getByDisplayValue("my other asset")
119119
})
120120

121+
test("correctly removes from the numeric list with remove button", async () => {
122+
const user = userEvent.setup()
123+
render({
124+
schema: numberArrayControlJsonSchema,
125+
uischema: arrayControlUISchema,
126+
})
127+
const addButton = await screen.findByRole("button", { name: "Add Assets" })
128+
await user.click(addButton)
129+
await user.click(addButton)
130+
const inputFields = await screen.findAllByLabelText(/Assets \d+/)
131+
await user.type(inputFields[0], "1")
132+
await user.type(inputFields[1], "2")
133+
await user.type(inputFields[2], "3")
134+
135+
const removeButtons = await screen.findAllByRole("button", {
136+
name: "Delete",
137+
})
138+
expect(removeButtons).toHaveLength(3)
139+
await user.click(removeButtons[1])
140+
const updatedRemoveButtons = await screen.findAllByRole("button", {
141+
name: "Delete",
142+
})
143+
expect(updatedRemoveButtons).toHaveLength(2)
144+
screen.getByDisplayValue("01")
145+
screen.getByDisplayValue("3")
146+
})
147+
121148
test("renders with title", async () => {
122149
const data = { assets: ["apple"] }
123150
render({

src/controls/PrimitiveArrayControl.tsx

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Form, Button, Col, Row, Space } from "antd"
1313
import React, { useEffect, useMemo } from "react"
1414
import { ArrayControlOptions, ControlUISchema } from "../ui-schema"
1515
import { usePreviousValue } from "../common/usePreviousValue"
16+
import { NestedAntDFormContext } from "../contexts/NestedAntDFormContext"
1617

1718
type ArrayControlProps = Omit<JSFArrayControlProps, "data" | "uischema"> & {
1819
data?: unknown[]
@@ -84,13 +85,7 @@ export function PrimitiveArrayControl({
8485
}
8586

8687
return (
87-
<Form.Item
88-
id={id}
89-
name={path}
90-
label={label}
91-
required={required}
92-
{...formItemProps}
93-
>
88+
<Form.Item id={id} label={label} required={required} {...formItemProps}>
9489
<Form.List name={path} initialValue={data ?? [undefined]}>
9590
{(fields, { add, remove }, { errors }) => (
9691
<Row justify={"start"}>
@@ -120,18 +115,22 @@ export function PrimitiveArrayControl({
120115
</Col>
121116
) : null}
122117
<Col>
123-
<JsonFormsDispatch
124-
enabled={enabled} // not crazy about this pattern of overriding the description, but it solves the problem of disappearing aria labels
125-
schema={{
126-
...schema,
127-
description: `${label} ${index + 1}`,
128-
}}
129-
path={composePaths(path, `${index}`)}
130-
uischema={foundUISchema}
131-
renderers={renderers}
132-
cells={cells}
133-
uischemas={uischemas}
134-
/>
118+
<NestedAntDFormContext.Provider
119+
value={{ path, index: field.name }}
120+
>
121+
<JsonFormsDispatch
122+
enabled={enabled} // not crazy about this pattern of overriding the description, but it solves the problem of disappearing aria labels
123+
schema={{
124+
...schema,
125+
description: `${label} ${index + 1}`,
126+
}}
127+
path={composePaths(path, `${index}`)}
128+
uischema={foundUISchema}
129+
renderers={renderers}
130+
cells={cells}
131+
uischemas={uischemas}
132+
/>
133+
</NestedAntDFormContext.Provider>
135134
</Col>
136135
{fields.length > 1 ? (
137136
<Col>

0 commit comments

Comments
 (0)