Skip to content

Commit f2a7c1f

Browse files
authored
Merge pull request #7 from atmina/fieldarray
feat: Improve `useFieldArray` ergonomics
2 parents 972a23d + 557ab66 commit f2a7c1f

4 files changed

Lines changed: 88 additions & 9 deletions

File tree

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ const AddressSubform: FC<{field: FormBuilder<Address>}> = ({field}) => {
9797
}
9898
```
9999

100+
## Field arrays
101+
102+
Fields which are typed as arrays provide a `$useFieldArray()` hook which can be used to map over the contents, as well
103+
as mutate them using operations such as `append`, `insert`, `move` and `remove`.
104+
105+
The `fields` returned by `$useFieldArray` are themselves `FormBuilder`s that can be registered on inputs or passed to
106+
other Subform components.
107+
108+
```tsx
109+
import { FC } from "react";
110+
111+
const AddressesSubform: FC<{field: FormBuilder<Person[]>}> = ({field}) => {
112+
const {fields, append} = field.$useFieldArray();
113+
const add = () => {
114+
append({state: '', city: '', /* etc. */});
115+
}
116+
return <div>
117+
{fields.map(f => <AddressSubForm key={f.$key} field={f} />)}
118+
<button onClick={add}>Add new address</button>
119+
<div>
120+
}
121+
```
122+
123+
The `$key` contains a unique id for the array item and must be passed as the `key` when [rendering the list](https://react.dev/learn/rendering-lists).
124+
125+
Note: Field arrays are intended for use with arrays of objects. When dealing with arrays of primitives, you can either
126+
wrap the primitive in an object, or use a controller (`$useController`) to implement your own array logic.
127+
128+
For more information, see the React Hook Form docs on [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray).
129+
100130
## Compatibility with `useForm`
101131

102132
Currently, `useFormBuilder` is almost compatible with `useForm`. This means you get the entire bag of tools provided by
@@ -107,4 +137,4 @@ streamline this API and increase its type-safety.
107137

108138
## License
109139

110-
MIT
140+
MIT

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@atmina/formbuilder",
3-
"version": "0.0.7",
3+
"version": "1.0.0",
44
"description": "A strongly-typed alternative API for React Hook Form.",
55
"source": "src/index.ts",
66
"main": "lib/index.js",

src/formbuilder.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ describe("useFormBuilder", () => {
1515
firstName: string;
1616
lastName: string;
1717
};
18+
list: {
19+
id: string;
20+
action: string;
21+
}[];
1822
}
1923

2024
const createHarness = (
@@ -59,6 +63,10 @@ describe("useFormBuilder", () => {
5963
firstName: "John",
6064
lastName: "Smith",
6165
},
66+
list: [
67+
{ id: "0", action: "frobnicate" },
68+
{ id: "1", action: "skedaddle" },
69+
],
6270
};
6371

6472
beforeAll(() => {
@@ -151,7 +159,10 @@ describe("useFormBuilder", () => {
151159

152160
await waitFor(() => {
153161
expect(watchedRoot).toHaveTextContent(
154-
JSON.stringify({ person: { firstName: "Joe", lastName: "Smith" } })
162+
JSON.stringify({
163+
...defaultValues,
164+
person: { ...defaultValues.person, firstName: "Joe" },
165+
})
155166
);
156167
expect(watchedRoot).toHaveTextContent("Smith");
157168
expect(watchedFirstName).toHaveTextContent("Joe");
@@ -218,4 +229,29 @@ describe("useFormBuilder", () => {
218229
expect(errorType).toHaveTextContent("required");
219230
});
220231
});
232+
233+
test("$useFieldArray", async () => {
234+
const harness = createHarness({ defaultValues }, (builder) => {
235+
const { fields } = builder.fields.list.$useFieldArray();
236+
237+
return (
238+
<div>
239+
{fields.map((field, i) => (
240+
<input
241+
key={field.$key}
242+
{...field.action()}
243+
aria-label={`action-${i}`}
244+
/>
245+
))}
246+
</div>
247+
);
248+
});
249+
250+
render(<harness.Form />);
251+
252+
await waitFor(() => {
253+
expect(screen.getByLabelText("action-0")).toHaveValue("frobnicate");
254+
expect(screen.getByLabelText("action-1")).toHaveValue("skedaddle");
255+
});
256+
});
221257
});

src/formbuilder.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ type FormBuilderRegisterFn<T> = {
134134
*/
135135
export function createFormBuilder<TFieldValues extends FieldValues>(
136136
methods: UseFormReturn<TFieldValues>,
137-
path: string[]
137+
path: string[],
138+
// Set if created in $useFieldArray()
139+
key?: string,
138140
): FormBuilder<TFieldValues> {
139141
const currentPath = path.join(".") as FieldPath<TFieldValues>;
140142
// Cache generated functions to stabilize references across re-renders.
@@ -160,14 +162,23 @@ export function createFormBuilder<TFieldValues extends FieldValues>(
160162
// Called when used with `String(...)`.
161163
useCached = () => currentPath;
162164
break;
165+
case "$key":
166+
return key ?? currentPath;
163167
case "$useFieldArray":
164-
useCached = (props?: $UseFieldArrayProps<never>) =>
165-
useFieldArray({
168+
useCached = (props?: $UseFieldArrayProps<never>) => {
169+
const { fields, ...rest } = useFieldArray({
166170
name: currentPath as FieldArrayPath<TFieldValues>,
167-
keyName: "key" as const,
171+
keyName: "$key" as const,
168172
control,
169173
...props,
170174
});
175+
return {
176+
fields: fields.map(
177+
({ $key }, i) => createFormBuilder(methods, [...path, i.toString()], $key)
178+
),
179+
...rest
180+
};
181+
}
171182
break;
172183
case "$useController":
173184
useCached = (
@@ -331,10 +342,12 @@ interface $UseFieldArrayProps<T> {
331342
shouldUnregister?: boolean;
332343
}
333344

334-
type $UseFieldArrayReturn<T> = UseFieldArrayReturn<
345+
type $UseFieldArrayReturn<T> = Omit<UseFieldArrayReturn<
335346
{ __: T[] },
336347
T extends Primitive | BrowserNativeObject ? never : "__"
337-
>;
348+
>, "fields"> & {
349+
fields: (FormBuilder<T> & {$key: string})[];
350+
};
338351

339352
export type UseFormBuilderProps<
340353
TFieldValues extends FieldValues = FieldValues,

0 commit comments

Comments
 (0)