diff --git a/README.md b/README.md index d6bfd5b..f2bbf67 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,36 @@ const AddressSubform: FC<{field: FormBuilder
}> = ({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,