Skip to content

Commit

Permalink
Replace <Form /> input with generic field factory (#150)
Browse files Browse the repository at this point in the history
In this diff I added `<Form />` generic `field` factory as
a replacement for `input` factory. The main difference is each
value can have now any type, not only strings like before.

The second change is more generic `onChange` method which accepts
either value, or event like object which has this structure

```js
{
  target: {
    value: T
  }
}
```

This allows to work with components which emulates their event and
produces not string type.
  • Loading branch information
TrySound authored Aug 11, 2018
1 parent 132b5ea commit 4cfb56f
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 51 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"eslint": "^4.19.1",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-react": "^7.9.1",
"flow-bin": "^0.66.0",
"flow-bin": "^0.78.0",
"husky": "^0.14.3",
"jest": "^23.0.0",
"jest-environment-node": "^23.0.0",
Expand Down
14 changes: 11 additions & 3 deletions src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import * as React from 'react'
import Value from './Value'
import renderProps from '../utils/renderProps'

const isObject = value => typeof value === 'object' && value

const Form = ({ initial = {}, onChange, ...props }) => (
<Value initial={{ ...initial }} onChange={onChange}>
{({ value: values, set }) =>
renderProps(props, {
values,
input: id => {
const value = values[id] || ''
field: id => {
const value = values[id]
const setValue = updater =>
typeof updater === 'function'
? set(prev => ({ ...prev, [id]: updater(prev[id]) }))
Expand All @@ -19,7 +21,13 @@ const Form = ({ initial = {}, onChange, ...props }) => (
set: setValue,
bind: {
value,
onChange: event => setValue(event.target.value),
onChange: event => {
if (isObject(event) && isObject(event.target)) {
setValue(event.target.value)
} else {
setValue(event)
}
},
},
}
},
Expand Down
17 changes: 9 additions & 8 deletions src/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,16 @@ type FormChange<T> = T => void

type FormRender<T> = ({|
values: T,
input: <K: $Keys<T>>(
field: <K: $Keys<T>>(
key: K
) => {|
value: string,
set: Updater<string>,
value: $ElementType<T, K>,
set: Updater<$ElementType<T, K>>,
bind: {|
value: string,
onChange: (SyntheticInputEvent<*>) => void,
value: $ElementType<T, K>,
onChange: (
event: { target: { value: $ElementType<T, K> } } | $ElementType<T, K>
) => void,
|},
|},
|}) => React.Node
Expand All @@ -98,9 +100,8 @@ type FormProps<T> =
| {| initial: T, onChange?: FormChange<T>, render: FormRender<T> |}
| {| initial: T, onChange?: FormChange<T>, children: FormRender<T> |}

declare export class Form<T: { [string]: string }> extends React.Component<
FormProps<T>,
*
declare export class Form<T: { +[string]: mixed }> extends React.Component<
FormProps<T>
> {}

/* Hover */
Expand Down
50 changes: 35 additions & 15 deletions tests/components/Form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,64 @@ import { lastCallArg } from './utils'
test('<Form />', () => {
const renderFn = jest.fn().mockReturnValue(null)
TestRenderer.create(
<Form initial={{ prop1: '1', prop2: '2' }} render={renderFn} />
<Form initial={{ prop1: '1', prop2: 2 }} render={renderFn} />
)

expect(renderFn).toBeCalledTimes(1)
expect(renderFn).lastCalledWith(
expect.objectContaining({ values: { prop1: '1', prop2: '2' } })
expect.objectContaining({ values: { prop1: '1', prop2: 2 } })
)

expect(lastCallArg(renderFn).input('prop1')).toEqual(
expect(lastCallArg(renderFn).field('prop1')).toEqual(
expect.objectContaining({
value: '1',
bind: expect.objectContaining({ value: '1' }),
})
)
expect(lastCallArg(renderFn).input('prop2')).toEqual(
expect(lastCallArg(renderFn).field('prop2')).toEqual(
expect.objectContaining({
value: '2',
bind: expect.objectContaining({ value: '2' }),
value: 2,
bind: expect.objectContaining({ value: 2 }),
})
)

lastCallArg(renderFn)
.input('prop1')
.field('prop1')
.set('10')
lastCallArg(renderFn)
.input('prop2')
.bind.onChange({ target: { value: '20' } })
.field('prop2')
.bind.onChange({ target: { value: 20 } })

expect(lastCallArg(renderFn).input('prop1')).toEqual(
expect(lastCallArg(renderFn).field('prop1')).toEqual(
expect.objectContaining({
value: '10',
bind: expect.objectContaining({ value: '10' }),
})
)
expect(lastCallArg(renderFn).input('prop2')).toEqual(
expect(lastCallArg(renderFn).field('prop2')).toEqual(
expect.objectContaining({
value: '20',
bind: expect.objectContaining({ value: '20' }),
value: 20,
bind: expect.objectContaining({ value: 20 }),
})
)

lastCallArg(renderFn)
.field('prop1')
.bind.onChange('100')
lastCallArg(renderFn)
.field('prop2')
.bind.onChange({ target: 200 })

expect(lastCallArg(renderFn).field('prop1')).toEqual(
expect.objectContaining({
value: '100',
bind: expect.objectContaining({ value: '100' }),
})
)
expect(lastCallArg(renderFn).field('prop2')).toEqual(
expect.objectContaining({
value: { target: 200 },
bind: expect.objectContaining({ value: { target: 200 } }),
})
)
})
Expand All @@ -58,13 +78,13 @@ test('<Form onChange />', () => {
expect(onChangeFn).toBeCalledTimes(0)

lastCallArg(renderFn)
.input('prop')
.field('prop')
.set('10')
expect(onChangeFn).toBeCalledTimes(1)
expect(onChangeFn).lastCalledWith({ prop: '10' })

lastCallArg(renderFn)
.input('prop')
.field('prop')
.bind.onChange({ target: { value: '100' } })
expect(onChangeFn).toBeCalledTimes(2)
expect(onChangeFn).lastCalledWith({ prop: '100' })
Expand Down
69 changes: 48 additions & 21 deletions tests/test_flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,33 +218,64 @@ const noop = () => null

/* Form */
{
const render = ({ input }) => {
const name = input('a')
;(name.value: string)
name.set('')
;(name.bind.value: string)
;(name.bind.onChange: Function)
const isNumber = (value: number): number => value

const render = ({ field }) => {
const a = field('a')
;(a.value: string)
;(a.bind.value: string)
a.set('')
a.bind.onChange('')
a.bind.onChange({ target: { value: '' } })
// $FlowFixMe
;(a.value: boolean)
// $FlowFixMe
input('b')
;(a.bind.value: boolean)
a.set((value: string) => value)
// $FlowFixMe
;(name.value: number)
a.set((value: boolean) => value)
// $FlowFixMe
name.setValue(0)
a.set(true)
// TODO should fail
a.bind.onChange(true)
// TODO should fail
a.bind.onChange({ target: { value: true } })

const b = field('b')
;(b.value: number)
// $FlowFixMe
;(b.value: boolean)

const c = field('c')
;(c.value: { value: string })
// $FlowFixMe
;(name.bind.value: number)
;(c.value: { value: boolean })

// $FlowFixMe
;(name.bind.onChange: number)
const d = field('d')
}
const onChange = data => {
;(data.a: string)
// $FlowFixMe
;(data.a: number)
;(data.b: number)
;(data.c: { value: string })
// $FlowFixMe value is string
;(data.a: boolean)
// $FlowFixMe value is number
;(data.b: boolean)
// $FlowFixMe value is object
;(data.c: boolean)
// $FlowFixMe field does not exist
;(data.d: boolean)
}
;[
<Form initial={{ a: '' }} render={render} />,
<Form initial={{ a: '' }}>{render}</Form>,
<Form initial={{ a: '' }} onChange={onChange} render={noop} />,
<Form initial={{ a: '' }} onChange={onChange}>
<Form initial={{ a: '', b: 0, c: { value: '' } }} render={render} />,
<Form initial={{ a: '', b: 0, c: { value: '' } }}>{render}</Form>,
<Form
initial={{ a: '', b: 0, c: { value: '' } }}
onChange={onChange}
render={noop}
/>,
<Form initial={{ a: '', b: 0, c: { value: '' } }} onChange={onChange}>
{noop}
</Form>,
// $FlowFixMe
Expand All @@ -253,10 +284,6 @@ const noop = () => null
<Form render={noop} />,
// $FlowFixMe
<Form>{noop}</Form>,
// $FlowFixMe
<Form initial={{ a: 0 }} render={noop} />,
// $FlowFixMe
<Form initial={{ a: 0 }}>{noop}</Form>,
]
}

Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2732,9 +2732,9 @@ flat-cache@^1.2.1:
graceful-fs "^4.1.2"
write "^0.2.1"

flow-bin@^0.66.0:
version "0.66.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.66.0.tgz#a96dde7015dc3343fd552a7b4963c02be705ca26"
flow-bin@^0.78.0:
version "0.78.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.78.0.tgz#df9fe7f9c9a2dfaff39083949fe2d831b41627b7"

flush-write-stream@^1.0.0:
version "1.0.3"
Expand Down

0 comments on commit 4cfb56f

Please sign in to comment.