Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
Merge pull request #30 from diego3g/feature/reset-form
Browse files Browse the repository at this point in the history
New registerField strategy and reset form
  • Loading branch information
pellizzetti committed May 16, 2019
2 parents d74feb5 + 1cdf8bb commit 283f00f
Show file tree
Hide file tree
Showing 20 changed files with 2,239 additions and 1,276 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = {
{
extensions: ['.jsx', '.tsx'],
},
],
],
'import-helpers/order-imports': [
'warn',
{
Expand All @@ -49,6 +49,7 @@ module.exports = {
alphabetize: { order: 'asc', ignoreCase: true },
},
],
'no-param-reassign': 'off',
'react/prop-types': 'off',
'jsx-a11y/label-has-for': 'off',
'import/prefer-default-export': 'off',
Expand Down
180 changes: 97 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ yarn add @rocketseat/unform

- [Guides](#guides)
- [Basics](#basics)
- [Reset Form](#reset-form)
- [Nested fields](#nested-fields)
- [Initial data](#initial-data)
- [Validation](#validation)
Expand All @@ -81,6 +82,8 @@ yarn add @rocketseat/unform

### Basics

Unform exposes three default form elements: `<Input />`, `<Select />` and `<Textarea />`. Currently, `<Select />` element does not support multiple values, you can use [React Select](#react-select) example to achieve that.

```js
import React from "react";
import { Form, Input } from "@rocketseat/unform";
Expand Down Expand Up @@ -108,6 +111,28 @@ function App() {
}
```

### Reset form

```js
import React from "react";
import { Form, Input } from "@rocketseat/unform";

function App() {
function handleSubmit(data, { resetForm }) {
resetForm();
};

return (
<Form onSubmit={handleSubmit}>
<Input name="email" />
<Input name="password" type="password" />

<button type="submit">Sign in</button>
</Form>
);
}
```

### Nested fields

```js
Expand Down Expand Up @@ -261,123 +286,112 @@ Below are some examples with [react-select](https://github.com/JedWatson/react-s
### React select

```js
import React, { useState } from "react";
import { Form, useField } from "@rocketseat/unform";
import Select from 'react-select';

/* You can't use your component directly, you have to wrap it
around another component, or you won't be able to use useField properly */
function ReactSelect({ name, options, multiple }) {
import React, { useRef, useEffect } from "react";
import Select from "react-select";

import { useField } from "../../../lib";

export default function ReactSelect({
name,
label,
options,
multiple,
...rest
}) {
const ref = useRef(null);
const { fieldName, registerField, defaultValue, error } = useField(name);
const [value, setValue] = useState(defaultValue);

function getValue() {
function parseSelectValue(selectValue) {
if (!multiple) {
return value;
return selectValue ? selectValue.id : "";
}

return value.reduce((res, item) => {
res.push(item.value);
return res;
}, []);
return selectValue ? selectValue.map(option => option.id) : [];
}

useEffect(() => {
registerField({
name: fieldName,
ref: ref.current,
path: "state.value",
parseValue: parseSelectValue,
clearValue: selectRef => {
selectRef.select.clearValue();
}
});
}, [ref.current, fieldName]);

function getDefaultValue() {
if (!defaultValue) return null;

if (!multiple) {
return options.find(option => option.id === defaultValue);
}

return options.filter(option => defaultValue.includes(option.id));
}

return (
<>
{label && <label htmlFor={fieldName}>{label}</label>}

<Select
name="techs"
name={fieldName}
aria-label={fieldName}
options={options}
isMulti={multiple}
value={value}
onChange={data => setValue(data)}
ref={() => registerField({ name: fieldName, ref: getValue })}
defaultValue={getDefaultValue()}
ref={ref}
getOptionValue={option => option.id}
getOptionLabel={option => option.title}
{...rest}
/>

{error && <span>{error}</span>}
</>
)
}

function App() {
const techs = [
{ value: "react", label: "ReactJS" },
{ value: "node", label: "NodeJS" },
{ value: "rn", label: "React Native" }
];

const colors = [
{ value: "red", label: "Red" },
{ value: "green", label: "Green" },
{ value: "blue", label: "Blue" }
]

const initialData = {
color: { value: 'green', label: 'Green' },
techs: [
{
value: 'react', label: 'ReactJS'
},
],
}

function handleSubmit(data) {};

return (
<Form initialData={initialData} onSubmit={handleSubmit}>
<ReactSelect options={colors} name="color" />

<br />

<ReactSelect options={techs} name="techs" multiple />

<button type="submit">Save</button>
</Form>
);
}
```

### React datepicker

```js
import React, { useState } from "react";
import { Form, useField } from "@rocketseat/unform";
import DatePicker from 'react-datepicker';
import React, { useRef, useEffect, useState } from "react";
import ReactDatePicker from "react-datepicker";

import 'react-datepicker/dist/react-datepicker.css';
import { useField } from "../../../lib";

/* You can't use your component directly, you have to wrap it
around another component, or you won't be able to use useField properly */
function ReactDate({ name }) {
const { fieldName, registerField, defaultValue, error } = useField(name);
const [value, setValue] = useState(defaultValue);
import "react-datepicker/dist/react-datepicker.css";

function getValue() {
return value;
}
export default function DatePicker({ name }) {
const ref = useRef(null);
const { fieldName, registerField, defaultValue, error } = useField(name);
const [selected, setSelected] = useState(defaultValue);

useEffect(() => {
registerField({
name: fieldName,
ref: ref.current,
path: "props.selected",
clearValue: pickerRef => {
pickerRef.clear();
}
});
}, [ref.current, fieldName]);

return (
<>
<DatePicker
selected={value}
onChange={data => setValue(data)}
ref={() => registerField({ name: fieldName, ref: getValue })}
<ReactDatePicker
name={fieldName}
selected={selected}
onChange={date => setSelected(date)}
ref={ref}
/>
{error && <span>{error}</span>}
</>
)
}

function App() {
function handleSubmit(data) {};

return (
<Form onSubmit={handleSubmit}>
<ReactDate name="birthday" />

<button type="submit">Save</button>
</Form>
);
}

```

## Contributing
Expand Down
98 changes: 87 additions & 11 deletions __tests__/Form.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { act, render, fireEvent, wait } from "react-testing-library";
import * as Yup from "yup";

import { Form, Input, Textarea, Select, Scope } from "../lib";
import CustomInputClear from "./utils/CustomInputClear";
import CustomInputParse from "./utils/CustomInputParse";

describe("Form", () => {
it("should render form elements", () => {
Expand Down Expand Up @@ -55,12 +57,17 @@ describe("Form", () => {

fireEvent.submit(getByTestId("form"));

expect(submitMock).toHaveBeenCalledWith({
name: "Diego",
address: {
street: "John Doe Avenue"
expect(submitMock).toHaveBeenCalledWith(
{
name: "Diego",
address: {
street: "John Doe Avenue"
}
},
{
resetForm: expect.any(Function)
}
});
);
});

it("should remove unmounted elements from refs", () => {
Expand All @@ -80,22 +87,32 @@ describe("Form", () => {

fireEvent.submit(getByTestId("form"));

expect(submitMock).toHaveBeenCalledWith({
another: "Diego"
});
expect(submitMock).toHaveBeenCalledWith(
{
another: "Diego"
},
{
resetForm: expect.any(Function)
}
);
});

it("should update data to match schema", async () => {
const submitMock = jest.fn();
const schema = Yup.object().shape({
bio: Yup.string()
name: Yup.string(),
bio: Yup.string().when("$stripBio", {
is: true,
then: Yup.string().strip(true)
})
});

const { getByTestId } = render(
<Form
schema={schema}
context={{ stripBio: true }}
onSubmit={submitMock}
initialData={{ bio: "Testing" }}
initialData={{ name: "Diego", bio: "Testing" }}
>
<Input name="name" />
<Input name="bio" />
Expand All @@ -107,7 +124,66 @@ describe("Form", () => {
});

await wait(() => {
expect(submitMock).toHaveBeenCalledWith({ bio: "Testing" });
expect(submitMock).toHaveBeenCalledWith(
{ name: "Diego" },
{
resetForm: expect.any(Function)
}
);
});
});

it("should reset form data when resetForm helper is dispatched", () => {
const { getByTestId, getByLabelText } = render(
<Form onSubmit={(_, { resetForm }) => resetForm()}>
<Input name="name" />
<Select name="tech" options={[{ id: "node", title: "NodeJS" }]} />
</Form>
);

getByLabelText("name").setAttribute("value", "Diego");

fireEvent.change(getByLabelText("tech"), { target: { value: "node" } });

fireEvent.submit(getByTestId("form"));

expect((getByLabelText("name") as HTMLInputElement).value).toBe("");
expect((getByLabelText("tech") as HTMLSelectElement).value).toBe("");
});

it("should be able to have custom value parser", () => {
const submitMock = jest.fn();

const { getByTestId } = render(
<Form onSubmit={submitMock} initialData={{ name: "Diego" }}>
<CustomInputParse name="name" />
</Form>
);

fireEvent.submit(getByTestId("form"));

expect(submitMock).toHaveBeenCalledWith(
{
name: "Diego-test"
},
{
resetForm: expect.any(Function)
}
);
});

it("should be able to have custom value clearer", () => {
const { getByTestId, getByLabelText } = render(
<Form
onSubmit={(_, { resetForm }) => resetForm()}
initialData={{ name: "Diego" }}
>
<CustomInputClear name="name" />
</Form>
);

fireEvent.submit(getByTestId("form"));

expect((getByLabelText("name") as HTMLInputElement).value).toBe("test");
});
});

0 comments on commit 283f00f

Please sign in to comment.