Skip to content

Commit 99b60c8

Browse files
dummerbderikras
authored andcommitted
Value Lifecycle Props (#74)
* Added type definitions for parse, format, and normalize props * Implemented format, parse, and normalize props * Fixed form.onSubmit call signature assert * Made new props optional * Unbreak build. 😳
1 parent 6114865 commit 99b60c8

File tree

5 files changed

+226
-8
lines changed

5 files changed

+226
-8
lines changed

src/Field.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ export default class Field extends React.PureComponent<Props, State> {
2525
state: State
2626
unsubscribe: () => void
2727

28+
static contextTypes = {
29+
reactFinalForm: PropTypes.object
30+
}
31+
32+
static defaultProps = {
33+
format: (value: ?any, name: string) => (value === undefined ? '' : value),
34+
parse: (value: ?any, name: string) => (value === '' ? undefined : value),
35+
normalize: (value: ?any, previousValue: ?any, allValues: Object) => value
36+
}
37+
2838
constructor(props: Props, context: ReactContext) {
2939
super(props, context)
3040
let initialState
@@ -66,6 +76,17 @@ export default class Field extends React.PureComponent<Props, State> {
6676

6777
notify = (state: FieldState) => this.setState({ state })
6878

79+
parseAndNormalize = (value: ?any) => {
80+
if (this.props.parse !== null) {
81+
value = this.props.parse(value, this.props.name)
82+
}
83+
return this.props.normalize(
84+
value,
85+
this.state.state.value,
86+
this.context.reactFinalForm.getState().values
87+
)
88+
}
89+
6990
componentWillReceiveProps(nextProps: Props) {
7091
const { name, subscription } = nextProps
7192
if (
@@ -95,7 +116,7 @@ export default class Field extends React.PureComponent<Props, State> {
95116
onChange: (event: SyntheticInputEvent<*> | any) => {
96117
const value: any =
97118
event && event.target ? getValue(event, isReactNative) : event
98-
this.state.state.change(value === '' ? undefined : value)
119+
this.state.state.change(this.parseAndNormalize(value))
99120
},
100121
onFocus: (event: ?SyntheticFocusEvent<*>) => {
101122
this.state.state.focus()
@@ -107,6 +128,9 @@ export default class Field extends React.PureComponent<Props, State> {
107128
allowNull,
108129
component,
109130
children,
131+
format,
132+
parse,
133+
normalize,
110134
isEqual,
111135
name,
112136
subscription,
@@ -123,7 +147,10 @@ export default class Field extends React.PureComponent<Props, State> {
123147
name: ignoreName,
124148
...meta
125149
} = this.state.state
126-
if (value === undefined || (value === null && !allowNull)) {
150+
if (format !== null) {
151+
value = format(value, name)
152+
}
153+
if (value === null && !allowNull) {
127154
value = ''
128155
}
129156
const input = { name, value, ...this.handlers }
@@ -154,7 +181,3 @@ export default class Field extends React.PureComponent<Props, State> {
154181
)
155182
}
156183
}
157-
158-
Field.contextTypes = {
159-
reactFinalForm: PropTypes.object
160-
}

src/Field.test.js

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,165 @@ describe('Field', () => {
193193
expect(render.mock.calls[2][0].values.foo).toBeUndefined()
194194
})
195195

196+
it('should accept parse and normalize function props', () => {
197+
const parse = jest.fn((value, name) => `parse.${value}`)
198+
const normalize = jest.fn(
199+
(value, previousValue, allValues) => `normalize.${value}`
200+
)
201+
const renderInput = jest.fn(({ input }) => <input {...input} />)
202+
const render = jest.fn(() => (
203+
<form>
204+
<Field
205+
name="foo"
206+
render={renderInput}
207+
parse={parse}
208+
normalize={normalize}
209+
/>
210+
<Field name="boo" component="select" />
211+
</form>
212+
))
213+
214+
const dom = TestUtils.renderIntoDocument(
215+
<Form
216+
onSubmit={onSubmitMock}
217+
render={render}
218+
initialValues={{ boo: 'abc' }}
219+
/>
220+
)
221+
222+
expect(render).toHaveBeenCalled()
223+
expect(render).toHaveBeenCalledTimes(1)
224+
expect(render.mock.calls[0][0].values).toEqual({
225+
foo: undefined,
226+
boo: 'abc'
227+
})
228+
229+
const input = TestUtils.findRenderedDOMComponentWithTag(dom, 'input')
230+
231+
TestUtils.Simulate.change(input, { target: { value: 'bar' } })
232+
233+
expect(render).toHaveBeenCalledTimes(2)
234+
expect(render.mock.calls[1][0].values.foo).toBe('normalize.parse.bar')
235+
236+
expect(parse).toHaveBeenCalled()
237+
expect(parse).toHaveBeenCalledTimes(1)
238+
expect(parse.mock.calls[0]).toEqual(['bar', 'foo'])
239+
240+
expect(normalize).toHaveBeenCalled()
241+
expect(normalize).toHaveBeenCalledTimes(1)
242+
expect(normalize.mock.calls[0]).toEqual([
243+
'parse.bar',
244+
undefined,
245+
{ foo: undefined, boo: 'abc' }
246+
])
247+
248+
TestUtils.Simulate.change(input, { target: { value: 'x' } })
249+
250+
expect(render).toHaveBeenCalledTimes(3)
251+
expect(render.mock.calls[2][0].values.foo).toBe('normalize.parse.x')
252+
253+
expect(parse).toHaveBeenCalledTimes(2)
254+
expect(parse.mock.calls[1]).toEqual(['x', 'foo'])
255+
256+
expect(normalize).toHaveBeenCalledTimes(2)
257+
expect(normalize.mock.calls[1]).toEqual([
258+
'parse.x',
259+
'normalize.parse.bar',
260+
{ foo: 'normalize.parse.bar', boo: 'abc' }
261+
])
262+
})
263+
264+
it('should accept a null parse prop to preserve empty strings', () => {
265+
const renderInput = jest.fn(({ input }) => <input {...input} />)
266+
const render = jest.fn(() => (
267+
<form>
268+
<Field name="foo" render={renderInput} parse={null} />
269+
</form>
270+
))
271+
272+
const dom = TestUtils.renderIntoDocument(
273+
<Form onSubmit={onSubmitMock} render={render} />
274+
)
275+
276+
expect(render).toHaveBeenCalled()
277+
expect(render).toHaveBeenCalledTimes(1)
278+
expect(render.mock.calls[0][0].values.foo).toBeUndefined()
279+
280+
const input = TestUtils.findRenderedDOMComponentWithTag(dom, 'input')
281+
282+
TestUtils.Simulate.change(input, { target: { value: '' } })
283+
284+
expect(render).toHaveBeenCalledTimes(2)
285+
expect(render.mock.calls[1][0].values.foo).toBe('')
286+
287+
TestUtils.Simulate.change(input, { target: { value: 'abc' } })
288+
289+
expect(render).toHaveBeenCalledTimes(3)
290+
expect(render.mock.calls[2][0].values.foo).toBe('abc')
291+
})
292+
293+
it('should accept a format function prop', () => {
294+
const format = jest.fn((value, name) => `format.${value}`)
295+
const renderInput = jest.fn(({ input }) => <input {...input} />)
296+
const render = jest.fn(() => (
297+
<form>
298+
<Field name="foo" render={renderInput} format={format} />
299+
</form>
300+
))
301+
302+
TestUtils.renderIntoDocument(
303+
<Form onSubmit={onSubmitMock} render={render} />
304+
)
305+
306+
expect(render).toHaveBeenCalled()
307+
expect(render).toHaveBeenCalledTimes(1)
308+
expect(render.mock.calls[0][0].values.foo).toBeUndefined()
309+
310+
expect(format).toHaveBeenCalled()
311+
expect(format).toHaveBeenCalledTimes(1)
312+
expect(format.mock.calls[0]).toEqual([undefined, 'foo'])
313+
314+
expect(renderInput).toHaveBeenCalled()
315+
expect(renderInput).toHaveBeenCalledTimes(1)
316+
expect(renderInput.mock.calls[0][0].input.value).toBe('format.undefined')
317+
318+
renderInput.mock.calls[0][0].input.onChange('bar')
319+
320+
expect(format).toHaveBeenCalledTimes(2)
321+
expect(format.mock.calls[1]).toEqual(['bar', 'foo'])
322+
323+
expect(renderInput).toHaveBeenCalledTimes(2)
324+
expect(renderInput.mock.calls[1][0].input.value).toBe('format.bar')
325+
})
326+
327+
it('should accept a null format prop to preserve undefined values', () => {
328+
const renderInput = jest.fn(({ input }) => (
329+
<input {...input} value={input.value || ''} />
330+
))
331+
const render = jest.fn(() => (
332+
<form>
333+
<Field name="foo" render={renderInput} format={null} />
334+
</form>
335+
))
336+
337+
TestUtils.renderIntoDocument(
338+
<Form onSubmit={onSubmitMock} render={render} />
339+
)
340+
341+
expect(render).toHaveBeenCalled()
342+
expect(render).toHaveBeenCalledTimes(1)
343+
expect(render.mock.calls[0][0].values.foo).toBeUndefined()
344+
345+
expect(renderInput).toHaveBeenCalled()
346+
expect(renderInput).toHaveBeenCalledTimes(1)
347+
expect(renderInput.mock.calls[0][0].input.value).toBeUndefined()
348+
349+
renderInput.mock.calls[0][0].input.onChange('bar')
350+
351+
expect(renderInput).toHaveBeenCalledTimes(2)
352+
expect(renderInput.mock.calls[1][0].input.value).toBe('bar')
353+
})
354+
196355
it('should provide a value of [] when empty on a select multiple', () => {
197356
const dom = TestUtils.renderIntoDocument(
198357
<Form onSubmit={onSubmitMock}>
@@ -210,7 +369,36 @@ describe('Field', () => {
210369
expect(select.value).toBe('')
211370
})
212371

213-
it('should optionally allow null', () => {
372+
it("should convert null values to ''", () => {
373+
const renderInput = jest.fn(({ input }) => (
374+
<input {...input} value={input.value} />
375+
))
376+
const render = jest.fn(() => (
377+
<form>
378+
<Field name="foo" render={renderInput} />
379+
</form>
380+
))
381+
382+
TestUtils.renderIntoDocument(
383+
<Form onSubmit={onSubmitMock} render={render} />
384+
)
385+
386+
expect(renderInput).toHaveBeenCalled()
387+
expect(renderInput).toHaveBeenCalledTimes(1)
388+
expect(renderInput.mock.calls[0][0].input.value).toBe('')
389+
390+
renderInput.mock.calls[0][0].input.onChange('bar')
391+
392+
expect(renderInput).toHaveBeenCalledTimes(2)
393+
expect(renderInput.mock.calls[1][0].input.value).toBe('bar')
394+
395+
renderInput.mock.calls[1][0].input.onChange(null)
396+
397+
expect(renderInput).toHaveBeenCalledTimes(3)
398+
expect(renderInput.mock.calls[2][0].input.value).toBe('')
399+
})
400+
401+
it('should optionally allow null values', () => {
214402
const renderInput = jest.fn(({ input }) => (
215403
<input
216404
{...input}

src/ReactFinalForm.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,11 @@ describe('ReactFinalForm', () => {
175175

176176
const form = TestUtils.findRenderedDOMComponentWithTag(dom, 'form')
177177
TestUtils.Simulate.submit(form)
178+
const formComponent = TestUtils.findRenderedComponentWithType(dom, Form)
178179

179180
expect(onSubmit).toHaveBeenCalled()
180181
expect(onSubmit).toHaveBeenCalledTimes(1)
181-
expect(onSubmit).toHaveBeenCalledWith({ foo: 'bar' })
182+
expect(onSubmit).toHaveBeenCalledWith({ foo: 'bar' }, formComponent.form)
182183
})
183184

184185
it('should reinitialize when initialValues prop changes', () => {

src/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ export type FormProps = {
6262

6363
export type FieldProps = {
6464
allowNull?: boolean
65+
format: (value: any, name: string) => any | null
66+
parse: (value: any, name: string) => any | null
6567
name: string
68+
normalize: (value: any, previousValue: any, allValues: object) => any
6669
subscription?: FieldSubscription
6770
validate?: (value: any, allValues: object) => any
6871
value?: any

src/types.js.flow

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ export type FormProps = {
6363

6464
export type FieldProps = {
6565
allowNull?: boolean,
66+
format: (value: ?any, name: string) => ?any | null,
67+
parse: (value: ?any, name: string) => ?any | null,
6668
isEqual?: (a: any, b: any) => boolean,
6769
name: string,
70+
normalize: (value: ?any, previousValue: ?any, allValues: Object) => ?any,
6871
subscription?: FieldSubscription,
6972
validate?: (value: ?any, allValues: Object) => ?any,
7073
validateFields?: string[],

0 commit comments

Comments
 (0)