}> = ({field}) => {
}
```
+## Field arrays
+
+Fields which are typed as arrays provide a `$useFieldArray()` hook which can be used to map over the contents, as well
+as mutate them using operations such as `append`, `insert`, `move` and `remove`.
+
+The `fields` returned by `$useFieldArray` are themselves `FormBuilder`s that can be registered on inputs or passed to
+other Subform components.
+
+```tsx
+import { FC } from "react";
+
+const AddressesSubform: FC<{field: FormBuilder}> = ({field}) => {
+ const {fields, append} = field.$useFieldArray();
+ const add = () => {
+ append({state: '', city: '', /* etc. */});
+ }
+ return
+ {fields.map(f => )}
+
+
+}
+```
+
+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).
+
+Note: Field arrays are intended for use with arrays of objects. When dealing with arrays of primitives, you can either
+wrap the primitive in an object, or use a controller (`$useController`) to implement your own array logic.
+
+For more information, see the React Hook Form docs on [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray).
+
## Compatibility with `useForm`
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.
## License
-MIT
\ No newline at end of file
+MIT
diff --git a/package.json b/package.json
index 683fdb0..4eacf96 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@atmina/formbuilder",
- "version": "0.0.7",
+ "version": "1.0.0",
"description": "A strongly-typed alternative API for React Hook Form.",
"source": "src/index.ts",
"main": "lib/index.js",
diff --git a/src/formbuilder.test.tsx b/src/formbuilder.test.tsx
index 80ed3fe..4708ed7 100644
--- a/src/formbuilder.test.tsx
+++ b/src/formbuilder.test.tsx
@@ -15,6 +15,10 @@ describe("useFormBuilder", () => {
firstName: string;
lastName: string;
};
+ list: {
+ id: string;
+ action: string;
+ }[];
}
const createHarness = (
@@ -59,6 +63,10 @@ describe("useFormBuilder", () => {
firstName: "John",
lastName: "Smith",
},
+ list: [
+ { id: "0", action: "frobnicate" },
+ { id: "1", action: "skedaddle" },
+ ],
};
beforeAll(() => {
@@ -151,7 +159,10 @@ describe("useFormBuilder", () => {
await waitFor(() => {
expect(watchedRoot).toHaveTextContent(
- JSON.stringify({ person: { firstName: "Joe", lastName: "Smith" } })
+ JSON.stringify({
+ ...defaultValues,
+ person: { ...defaultValues.person, firstName: "Joe" },
+ })
);
expect(watchedRoot).toHaveTextContent("Smith");
expect(watchedFirstName).toHaveTextContent("Joe");
@@ -218,4 +229,29 @@ describe("useFormBuilder", () => {
expect(errorType).toHaveTextContent("required");
});
});
+
+ test("$useFieldArray", async () => {
+ const harness = createHarness({ defaultValues }, (builder) => {
+ const { fields } = builder.fields.list.$useFieldArray();
+
+ return (
+
+ {fields.map((field, i) => (
+
+ ))}
+
+ );
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByLabelText("action-0")).toHaveValue("frobnicate");
+ expect(screen.getByLabelText("action-1")).toHaveValue("skedaddle");
+ });
+ });
});
diff --git a/src/formbuilder.tsx b/src/formbuilder.tsx
index fbdd31b..5008768 100644
--- a/src/formbuilder.tsx
+++ b/src/formbuilder.tsx
@@ -134,7 +134,9 @@ type FormBuilderRegisterFn = {
*/
export function createFormBuilder(
methods: UseFormReturn,
- path: string[]
+ path: string[],
+ // Set if created in $useFieldArray()
+ key?: string,
): FormBuilder {
const currentPath = path.join(".") as FieldPath;
// Cache generated functions to stabilize references across re-renders.
@@ -160,14 +162,23 @@ export function createFormBuilder(
// Called when used with `String(...)`.
useCached = () => currentPath;
break;
+ case "$key":
+ return key ?? currentPath;
case "$useFieldArray":
- useCached = (props?: $UseFieldArrayProps) =>
- useFieldArray({
+ useCached = (props?: $UseFieldArrayProps) => {
+ const { fields, ...rest } = useFieldArray({
name: currentPath as FieldArrayPath,
- keyName: "key" as const,
+ keyName: "$key" as const,
control,
...props,
});
+ return {
+ fields: fields.map(
+ ({ $key }, i) => createFormBuilder(methods, [...path, i.toString()], $key)
+ ),
+ ...rest
+ };
+ }
break;
case "$useController":
useCached = (
@@ -331,10 +342,12 @@ interface $UseFieldArrayProps {
shouldUnregister?: boolean;
}
-type $UseFieldArrayReturn = UseFieldArrayReturn<
+type $UseFieldArrayReturn = Omit;
+>, "fields"> & {
+ fields: (FormBuilder & {$key: string})[];
+};
export type UseFormBuilderProps<
TFieldValues extends FieldValues = FieldValues,