diff --git a/README.md b/README.md index 32198d10..e0ce6f6b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ✅ Opt-in subscriptions - only update on the state you need! -✅ 💥 **2.2k gzipped** 💥 +✅ 💥 **2.5k gzipped** 💥 --- @@ -177,8 +177,9 @@ const MyForm = () => ( * [`FormSpyProps`](#formspyprops) * [`children?: ((props: FormSpyRenderProps) => React.Node) | React.Node`](#children-props-formspyrenderprops--reactnode--reactnode) * [`component?: React.ComponentType`](#component-reactcomponenttypeformspyrenderprops) + * [`onChange?: (formState: FormState) => void`](#onchange-formstate-formstate--void) * [`render?: (props: FormSpyRenderProps) => React.Node`](#render-props-formspyrenderprops--reactnode) - * [`formSubscription?: FormSubscription`](#formsubscription-formsubscription) + * [`subscription?: FormSubscription`](#subscription-formsubscription-1) * [`FormSpyRenderProps`](#formspyrenderprops) @@ -538,19 +539,28 @@ of the ways to render: `component`, `render`, or `children`. #### `children?: ((props: FormSpyRenderProps) => React.Node) | React.Node` A render function that is given [`FormSpyRenderProps`](#formspyrenderprops), as -well as any non-API props passed into the `` component. +well as any non-API props passed into the `` component. Will not be +called if an `onChange` prop is provided. #### `component?: React.ComponentType` A component that is given [`FormSpyRenderProps`](#formspyrenderprops) as props, -as well as any non-API props passed into the `` component. +as well as any non-API props passed into the `` component. Will not be +called if an `onChange` prop is provided. + +#### `onChange?: (formState: FormState) => void` + +A change listener that will be called with form state whenever the form state, +as subscribed to by the `subscription` prop, has changed. When an `onChange` +prop is provided, the `` will not render anything. #### `render?: (props: FormSpyRenderProps) => React.Node` A render function that is given [`FormSpyRenderProps`](#formspyrenderprops), as -well as any non-API props passed into the `` component. +well as any non-API props passed into the `` component. Will not be +called if an `onChange` prop is provided. -#### `formSubscription?: FormSubscription` +#### `subscription?: FormSubscription` A [`FormSubscription`](https://github.com/final-form/final-form#formsubscription--string-boolean-) diff --git a/docs/comparison.md b/docs/comparison.md index 9b7cf156..9c531aac 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -6,7 +6,7 @@ and you'd like it to be), please correct the mistake. | Feature | [🏁 React-Final-Form](https://github.com/final-form/react-final-form#-react-final-form) | [Formik](https://github.com/jaredpalmer/formik) | [Redux-Form](https://github.com/erikras/redux-form) | | ------------------------------------- | :---------------------------------------------------------------------------------------------------------------: | :----------------------------------------------: | :---------------------------------------------------: | -| Bundle Size | [3.5k](https://bundlephobia.com/result?p=final-form) + [2.2k](https://bundlephobia.com/result?p=react-final-form) | [7.5k](https://bundlephobia.com/result?p=formik) | [26.8k](https://bundlephobia.com/result?p=redux-form) | +| Bundle Size | [3.5k](https://bundlephobia.com/result?p=final-form) + [2.5k](https://bundlephobia.com/result?p=react-final-form) | [7.5k](https://bundlephobia.com/result?p=formik) | [26.8k](https://bundlephobia.com/result?p=redux-form) | | Works without Redux | ✅ | ✅ | ❌ | | Record-Level Sync Validation | ✅ | ✅ | ✅ | | Record-Level Async Validation | ✅ | ✅ | ✅ | diff --git a/src/FormSpy.js b/src/FormSpy.js index 93d65831..d8703eba 100644 --- a/src/FormSpy.js +++ b/src/FormSpy.js @@ -31,6 +31,9 @@ export default class FormSpy extends React.PureComponent { this.notify(state) } else { initialState = state + if (props.onChange) { + props.onChange(state) + } } }) } @@ -47,7 +50,12 @@ export default class FormSpy extends React.PureComponent { ) } - notify = (state: FormState) => this.setState({ state }) + notify = (state: FormState) => { + this.setState({ state }) + if (this.props.onChange) { + this.props.onChange(state) + } + } componentWillReceiveProps(nextProps: Props) { const { subscription } = nextProps @@ -71,8 +79,10 @@ export default class FormSpy extends React.PureComponent { } render() { - const { subscription, ...rest } = this.props - return renderComponent({ ...rest, ...this.state.state }, 'FormSpy') + const { onChange, subscription, ...rest } = this.props + return onChange + ? null + : renderComponent({ ...rest, ...this.state.state }, 'FormSpy') } } diff --git a/src/FormSpy.test.js b/src/FormSpy.test.js index 31f2c941..93fc5774 100644 --- a/src/FormSpy.test.js +++ b/src/FormSpy.test.js @@ -202,4 +202,91 @@ describe('FormSpy', () => { const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') TestUtils.Simulate.click(button) }) + + it('should call onChange', () => { + const input = jest.fn(({ input }) => ) + const onChange = jest.fn() + TestUtils.renderIntoDocument( +
+ {() => ( + + + + + )} + + ) + expect(input).toHaveBeenCalled() + expect(input).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalled() + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith({ dirty: false }) + + input.mock.calls[0][0].input.onChange('bar') + + expect(input).toHaveBeenCalledTimes(2) + expect(onChange).toHaveBeenCalledTimes(2) + expect(onChange).toHaveBeenCalledWith({ dirty: true }) + }) + + it('should not render with render prop when given onChange', () => { + const onChange = jest.fn() + const render = jest.fn() + TestUtils.renderIntoDocument( +
+ {() => ( + + + + )} + + ) + expect(onChange).toHaveBeenCalled() + expect(onChange).toHaveBeenCalledTimes(1) + expect(render).not.toHaveBeenCalled() + }) + + it('should not render with child render prop when given onChange', () => { + const onChange = jest.fn() + const render = jest.fn() + TestUtils.renderIntoDocument( +
+ {() => ( + + + {render} + +
+ )} + + ) + expect(onChange).toHaveBeenCalled() + expect(onChange).toHaveBeenCalledTimes(1) + expect(render).not.toHaveBeenCalled() + }) + + it('should not render with component prop when given onChange', () => { + const onChange = jest.fn() + const render = jest.fn() + TestUtils.renderIntoDocument( +
+ {() => ( + + + + )} + + ) + expect(onChange).toHaveBeenCalled() + expect(onChange).toHaveBeenCalledTimes(1) + expect(render).not.toHaveBeenCalled() + }) }) diff --git a/src/types.js.flow b/src/types.js.flow index d8febc0c..49b3ad96 100644 --- a/src/types.js.flow +++ b/src/types.js.flow @@ -46,7 +46,7 @@ export type FormRenderProps = { reset: () => void } & FormState -export type FormSpyRenderProps = {} & FormState +export type FormSpyRenderProps = FormState export type RenderableProps = $Shape<{ children: ((props: T) => React.Node) | React.Node, @@ -68,5 +68,6 @@ export type FieldProps = { } & RenderableProps export type FormSpyProps = { + onChange?: (formState: FormState) => void, subscription?: FormSubscription } & RenderableProps