Skip to content

Commit

Permalink
feat: add list group inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
AdeAttwood committed Oct 8, 2022
1 parent 39c38e5 commit 1f09904
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/list-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { FC } from "react";
import { useFormContext } from "./form-context";
import { AttributeContext, useAttributeContext } from "./attribute-context";

export interface ListGroupChildProps {
/**
* Callback function that will add a new item in to the list. This will use
* the `newItem` function to generate a new item.
*/
add: () => void;
}

export interface ListGroupProps {
/**
* The attribute this input is for
*/
attribute: string;
/**
* Function that will return a new empty item. This will be called whenever
* added a new item to the list. The default will return a new empty object.
*/
newItem?: () => any;
/**
* Render a list of inputs
*/
children: (props: ListGroupChildProps) => JSX.Element;
}

export const ListGroup: FC<ListGroupProps> = ({ children, attribute, newItem }) => {
const { getAttribute, setAttribute } = useFormContext();
const options = getAttribute(attribute, []);

if (typeof newItem === "undefined") {
throw new Error("newItem must not be undefined.");
}

const add = () => {
setAttribute(attribute, [...getAttribute(attribute, []), newItem()]);
};

return <AttributeContext.Provider value={{ attribute, options }}>{children({ add })}</AttributeContext.Provider>;
};

ListGroup.defaultProps = {
newItem: () => ({}),
};

export interface ListOptionChildProps {
/**
* The index this option is at
*/
index: string;
}

export interface ListOptionProps {
/**
* Render all the form elements for this list option
*/
children: (props: ListOptionChildProps) => JSX.Element;
}

export const ListOption: FC<ListOptionProps> = ({ children }) => {
const { options } = useAttributeContext();

return (
<>
{Object.keys(options).map((index) =>
children({
index,
})
)}
</>
);
};

export default ListGroup;
55 changes: 55 additions & 0 deletions tests/list-group.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Form from "../src/form";
import { ListGroup, ListOption } from "../src/list-group";
import { InputGroup } from "../src/input-group";

it("will render and submit with a radio list", async () => {
const onSubmit = jest.fn();
const { getByLabelText, getByText } = render(
<Form initialValues={{}} onSubmit={onSubmit}>
<ListGroup attribute="tags" newItem={() => ""}>
{({ add }) => (
<div>
<ListOption>
{({ index }) => (
<div key={index}>
<InputGroup attribute={`tags.${index}`}>
{({ props }) => (
<div>
<label htmlFor={props.id}>Tag {parseInt(index) + 1}</label>
<input {...props} />
</div>
)}
</InputGroup>
</div>
)}
</ListOption>
<button type="button" onClick={add}>Add Tag</button>
</div>
)}
</ListGroup>
<button>Submit</button>
</Form>
);

await userEvent.click(getByText("Add Tag"));
await userEvent.click(getByText("Submit"));

expect(onSubmit).toBeCalledTimes(1);
expect(onSubmit).toBeCalledWith({ formState: { tags: [""] } });

await userEvent.type(getByLabelText("Tag 1"), "Tag One");
await userEvent.click(getByText("Submit"));

expect(onSubmit).toBeCalledTimes(2);
expect(onSubmit).toBeCalledWith({ formState: { tags: ["Tag One"] } });

await userEvent.click(getByText("Add Tag"));
await userEvent.type(getByLabelText("Tag 2"), "Tag Two");
await userEvent.click(getByText("Submit"));

expect(onSubmit).toBeCalledTimes(3);
expect(onSubmit).toBeCalledWith({ formState: { tags: ["Tag One", "Tag Two"] } });
});

0 comments on commit 1f09904

Please sign in to comment.