Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/en/Form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# `<Form>`

Keeps form values in state and exposes helpers for binding fields, submitting,
and resetting the form.

`Form` is intentionally a lightweight state helper. It does not include schema
validation, async submission state, or a field registry.

## Usage

```jsx
import {Form} from 'libreact/lib/Form';

<Form
init={{email: '', remember: false}}
onSubmit={(values) => console.log(values)}
>
{({field, submit, reset}) =>
<form onSubmit={submit}>
<input type="email" {...field('email')} />
<label>
<input type="checkbox" {...field('remember')} />
Remember me
</label>
<button type="submit">Submit</button>
<button type="button" onClick={reset}>Reset</button>
</form>
}
</Form>
```

## Props

Signature

```ts
interface IFormProps {
init?: {[key: string]: any};
onChange?: (values, name, value) => void;
onSubmit?: (values, event?) => void;
onReset?: (values) => void;
}
```

, where

- `init` - optional map of initial field values.
- `onChange` - optional callback fired after a field changes.
- `onSubmit` - optional callback fired by `submit()`.
- `onReset` - optional callback fired after values are reset.

## Render props

`<Form>` passes the following helpers to `children` or `render`:

- `values` - current form values.
- `get(name?)` - returns a single field value, or all values when called
without a field name.
- `set(name, value)` - sets a field value.
- `field(name)` - returns `{name, value, checked, onChange}` props for an
input-like control.
- `submit(event?)` - prevents the default event and calls `onSubmit`.
- `reset()` - restores values from `init`.


## `withForm()` HOC

HOC that merges `form` prop into enhanced component's props.

```jsx
import {withForm} from 'libreact/lib/Form';

const MyCompWithForm = withForm(MyComp);
```

You can overwrite the injected prop name:

```js
const MyCompWithForm = withForm(MyComp, 'myForm');
```

Set initial values:

```js
const MyCompWithForm = withForm(MyComp, 'form', {email: ''});
```
1 change: 1 addition & 0 deletions docs/en/Inversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ in JSX tree without having to write stateful React components.

- [`<State>`](./State.md) &mdash; inverts `.state` and `.setState()` method.
- [`<Toggle>`](./Toggle.md), [`<Flipflop>`](./Flipflop.md), [`<Value>`](./Value.md), [`<Counter>`](./Counter.md), [`<List>`](./List.md), and [`<Map>`](./Map.md)
- [`<Form>`](./Form.md) &mdash; inverts lightweight form values and field bindings.
- [`<ShouldUpdate>`](./ShouldUpdate.md) &mdash; inverts `.shouldComponentUpdate()` life-cycle method.
- [`invert()`](./invert.md) and [`<Inverted>`](./invert.md#inverted) &mdash; inverts DOM element `ref` reference.

Expand Down
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* [Counter](Counter.md)
* [List](List.md)
* [Map](Map.md)
* [Form](Form.md)
* [ShouldUpdate](ShouldUpdate.md)
* [Lifecycles](Lifecycles.md)
* [Sensors](Sensors.md)
Expand Down
28 changes: 28 additions & 0 deletions src/Form/__story__/story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {createElement as h} from 'react';
import {storiesOf} from '@storybook/react';
import {Form} from '..';
import ShowDocs from '../../ShowDocs';

storiesOf('Inversion/Form', module)
.add('Documentation', () => h(ShowDocs, {md: require('../../../docs/en/Form.md')}))
.add('Example', () =>
<Form
init={{
email: '',
remember: false
}}
onSubmit={(values) => console.log(values)}
>
{({field, submit, reset}) => (
<form onSubmit={submit}>
<input type="email" placeholder="Email" {...field('email')} />
<label>
<input type="checkbox" {...field('remember')} />
Remember me
</label>
<button type="submit">Submit</button>
<button type="button" onClick={reset}>Reset</button>
</form>
)}
</Form>
);
161 changes: 161 additions & 0 deletions src/Form/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {h} from '../../util';
import {mount} from 'enzyme';
import {Form, withForm} from '..';

describe('<Form>', () => {
it('is a component', () => {
expect(Form).toBeInstanceOf(Function);
});

it('exposes initial values', () => {
mount(
<Form init={{email: 'hello@example.com'}}>
{({values, get}) => {
expect(values).toEqual({email: 'hello@example.com'});
expect(get('email')).toBe('hello@example.com');
expect(get()).toEqual({email: 'hello@example.com'});

return null;
}}
</Form>
);
});

it('uses the render prop', () => {
const render = jest.fn(() => null);

mount(h(Form, {
init: {email: 'hello@example.com'},
render
}));

expect(render).toHaveBeenCalledWith(expect.objectContaining({
values: {email: 'hello@example.com'}
}));
});

it('sets field values', () => {
let form;
const onChange = jest.fn();

mount(
<Form init={{email: ''}} onChange={onChange}>
{(props) => {
form = props;

return null;
}}
</Form>
);

form.set('email', 'hello@example.com');

expect(form.get('email')).toBe('hello@example.com');
expect(onChange).toHaveBeenCalledWith(
{email: 'hello@example.com'},
'email',
'hello@example.com'
);
});

it('binds text input fields', () => {
const wrapper = mount(
<Form init={{email: ''}}>
{({field}) => <input type="email" {...field('email')} />}
</Form>
);

const input = wrapper.find('input');

expect(input.prop('name')).toBe('email');
expect(input.prop('value')).toBe('');

input.simulate('change', {
target: {
value: 'hello@example.com'
}
});

expect(wrapper.find('input').prop('value')).toBe('hello@example.com');
});

it('binds checkbox fields', () => {
const wrapper = mount(
<Form init={{remember: false}}>
{({field}) => <input type="checkbox" {...field('remember')} />}
</Form>
);

wrapper.find('input').simulate('change', {
target: {
type: 'checkbox',
checked: true
}
});

expect(wrapper.find('input').prop('checked')).toBe(true);
expect(wrapper.find('input').prop('value')).toBeUndefined();
});

it('exports the form component from the package root', () => {
const libreact = require('../../');

expect(libreact.Form).toBe(Form);
});

it('submits current values', () => {
let form;
const onSubmit = jest.fn();
const event = {
preventDefault: jest.fn()
};

mount(
<Form init={{email: ''}} onSubmit={onSubmit}>
{(props) => {
form = props;

return null;
}}
</Form>
);

form.set('email', 'hello@example.com');
form.submit(event);

expect(event.preventDefault).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith(
{email: 'hello@example.com'},
event
);
});

it('resets to initial values', () => {
let form;
const onReset = jest.fn();

mount(
<Form init={{email: 'initial@example.com'}} onReset={onReset}>
{(props) => {
form = props;

return null;
}}
</Form>
);

form.set('email', 'changed@example.com');
form.reset();

expect(form.get('email')).toBe('initial@example.com');
expect(onReset).toHaveBeenCalledWith({email: 'initial@example.com'});
});

it('injects a form object through withForm()', () => {
const View = ({form}) => <span>{form.get('email')}</span>;
const Enhanced = withForm(View, 'form', {email: 'hello@example.com'});
const wrapper = mount(<Enhanced />);

expect(wrapper.text()).toBe('hello@example.com');
});
});
Loading