Skip to content

Commit daed8a8

Browse files
committed
WIP Enhancing Observable pipe to handle multiple change events
1 parent b7c9977 commit daed8a8

File tree

5 files changed

+270
-36
lines changed

5 files changed

+270
-36
lines changed

examples/basics/ControlledUpdates.jsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import * as R from 'ramda'
2+
import React from 'react'
3+
import { Form } from '@lib'
4+
import { Input, Radio, Checkbox, Select, Textarea } from '@fields'
5+
import Button from '@shared/Button'
6+
7+
const nextState = {
8+
inputOne: 'foo',
9+
inputTwo: 'bar',
10+
radio: 'cheese',
11+
// checkbox1: true,
12+
// checkbox2: false,
13+
// select: 'three',
14+
// textareaOne: 'Text',
15+
// textareaTwo: 'Everywhere',
16+
}
17+
18+
export default class ControlledFields extends React.Component {
19+
state = {
20+
inputOne: '',
21+
inputTwo: 'foo',
22+
radio: 'potato',
23+
checkbox1: false,
24+
checkbox2: true,
25+
select: 'two',
26+
textareaOne: '',
27+
textareaTwo: 'something',
28+
}
29+
30+
handleFieldChange = ({ nextValue, fieldProps }) => {
31+
this.setState({
32+
[fieldProps.name]: nextValue,
33+
})
34+
}
35+
36+
handlePrefillClick = () => {
37+
this.setState(nextState)
38+
}
39+
40+
handleSubmit = ({ serialized }) => {
41+
Object.keys(nextState).forEach((fieldName) => {
42+
const serializedValue = serialized[fieldName]
43+
const expectedValue = nextState[fieldName]
44+
console.assert(
45+
R.equals(serializedValue, expectedValue),
46+
`Invalid state for "${fieldName}". Expected: "${expectedValue}", got: "${serializedValue}".`,
47+
)
48+
})
49+
50+
return new Promise((resolve) => resolve())
51+
}
52+
53+
render() {
54+
const {
55+
inputOne,
56+
inputTwo,
57+
radio,
58+
checkbox1,
59+
checkbox2,
60+
select,
61+
textareaOne,
62+
textareaTwo,
63+
} = this.state
64+
65+
return (
66+
<React.Fragment>
67+
<h1>Controlled updates</h1>
68+
69+
<Form
70+
id="form"
71+
ref={this.props.getRef}
72+
action={this.handleSubmit}
73+
onSubmitStart={this.props.onSubmitStart}
74+
>
75+
{/* Inputs */}
76+
<Input
77+
id="inputOne"
78+
name="inputOne"
79+
label="Field one"
80+
value={inputOne}
81+
onChange={this.handleFieldChange}
82+
/>
83+
<Input
84+
id="inputTwo"
85+
label="Field two"
86+
name="inputTwo"
87+
value={inputTwo}
88+
onChange={this.handleFieldChange}
89+
/>
90+
91+
{/* Radio */}
92+
<Radio
93+
id="radio1"
94+
name="radio"
95+
label="Cheese"
96+
value="cheese"
97+
checked={radio === 'cheese'}
98+
onChange={this.handleFieldChange}
99+
/>
100+
<Radio
101+
id="radio2"
102+
name="radio"
103+
label="Potato"
104+
value="potato"
105+
checked={radio === 'potato'}
106+
onChange={this.handleFieldChange}
107+
/>
108+
<Radio
109+
id="radio3"
110+
name="radio"
111+
label="Cucumber"
112+
value="cucumber"
113+
checked={radio === 'cucumber'}
114+
onChange={this.handleFieldChange}
115+
/>
116+
117+
{/* Checkboxes */}
118+
<Checkbox
119+
id="checkbox1"
120+
name="checkbox1"
121+
label="Checkbox one"
122+
checked={checkbox1}
123+
onChange={this.handleFieldChange}
124+
/>
125+
<Checkbox
126+
id="checkbox2"
127+
name="checkbox2"
128+
label="Checkbox two"
129+
checked={checkbox2}
130+
onChange={this.handleFieldChange}
131+
/>
132+
133+
{/* Select */}
134+
<Select
135+
id="select"
136+
name="select"
137+
label="Select"
138+
value={select}
139+
onChange={this.handleFieldChange}
140+
>
141+
<option value="one">one</option>
142+
<option value="two">two</option>
143+
<option value="three">three</option>
144+
</Select>
145+
146+
{/* Textareas */}
147+
<Textarea
148+
id="textareaOne"
149+
name="textareaOne"
150+
label="Textarea one"
151+
onChange={this.handleFieldChange}
152+
value={textareaOne}
153+
onChange={this.handleFieldChange}
154+
/>
155+
<Textarea
156+
id="textareaTwo"
157+
name="textareaTwo"
158+
label="Textarea two"
159+
onChange={this.handleFieldChange}
160+
value={textareaTwo}
161+
onChange={this.handleFieldChange}
162+
/>
163+
164+
<Button>Submit</Button>
165+
<span onClick={this.handlePrefillClick}>Pre-fill</span>
166+
</Form>
167+
</React.Fragment>
168+
)
169+
}
170+
}

examples/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Reset from './basics/Reset'
1919
import Serialize from './basics/Serialize'
2020
import UncontrolledFields from './basics/UncontrolledFields'
2121
import ControlledFields from './basics/ControlledFields'
22+
import ControlledUpdates from './basics/ControlledUpdates'
2223
import SubmitCallbacks from './basics/SubmitCallbacks'
2324
import Submit from './basics/Submit'
2425

@@ -78,6 +79,7 @@ storiesOf('Basics|Interaction', module)
7879
.add('Serialize', addComponent(<Serialize />))
7980
.add('Uncontrolled fields', addComponent(<UncontrolledFields />))
8081
.add('Controlled fields', addComponent(<ControlledFields />))
82+
.add('Controlled updates', addComponent(<ControlledUpdates />))
8183
.add('Form submit', addComponent(<Submit />))
8284
.add('Submit callbacks', addComponent(<SubmitCallbacks />))
8385

src/components/Form.jsx

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EventEmitter } from 'events'
66
import { Observable } from 'rxjs/internal/Observable'
77
import { fromEvent } from 'rxjs/internal/observable/fromEvent'
88
import { bufferTime } from 'rxjs/internal/operators/bufferTime'
9+
import * as rxJs from 'rxjs/internal/operators'
910

1011
/* Internal modules */
1112
import {
@@ -124,12 +125,52 @@ export default class Form extends React.Component {
124125
/* Field events observerables */
125126
fromEvent(eventEmitter, 'fieldRegister')
126127
.pipe(bufferTime(50))
128+
/**
129+
* @todo @performance
130+
* Iterative state updates have no reason. Once the register events are buffered,
131+
* perform a single state update with all the pending fields.
132+
*/
127133
.subscribe((pendingFields) => pendingFields.forEach(this.registerField))
128134
fromEvent(eventEmitter, 'fieldFocus').subscribe(this.handleFieldFocus)
129-
fromEvent(eventEmitter, 'fieldChange').subscribe(this.handleFieldChange)
135+
136+
fromEvent(eventEmitter, 'fieldChange')
137+
.pipe(
138+
rxJs.bufferTime(50),
139+
rxJs.filter(R.complement(R.isEmpty)),
140+
rxJs.tap((e) => console.log('buffered:', e)),
141+
rxJs.map(R.map(this.handleFieldChange)),
142+
rxJs.tap((e) => console.log('mapped:', e)),
143+
)
144+
.subscribe(async (pendingUpdates) => {
145+
console.log({ pendingUpdates })
146+
147+
const fieldsList = await Promise.all(pendingUpdates)
148+
console.log({ fieldsList })
149+
150+
const updatedFields = fieldsList.filter(Boolean)
151+
console.log({ updatedFields })
152+
153+
if (updatedFields.length === 0) {
154+
return
155+
}
156+
157+
const fieldsDelta = fieldUtils.stitchFields(updatedFields)
158+
console.log({ fieldsDelta })
159+
160+
const nextFields = R.mergeDeepRight(this.state.fields, fieldsDelta)
161+
console.log('next fields:', nextFields)
162+
163+
return this.setState({ fields: nextFields })
164+
})
165+
130166
fromEvent(eventEmitter, 'fieldBlur').subscribe(this.handleFieldBlur)
131-
fromEvent(eventEmitter, 'fieldUnregister').subscribe(this.unregisterField)
132167
fromEvent(eventEmitter, 'validateField').subscribe(this.validateField)
168+
169+
/**
170+
* @todo @performance
171+
* Buffer incoming unregister events and dispatch a single state update.
172+
*/
173+
fromEvent(eventEmitter, 'fieldUnregister').subscribe(this.unregisterField)
133174
}
134175

135176
/**
@@ -362,24 +403,28 @@ export default class Form extends React.Component {
362403
* @param {mixed} nextValue
363404
*/
364405
handleFieldChange = this.withRegisteredField(async (args) => {
406+
console.log('handleFieldChange called with', args)
407+
365408
const { fields, dirty } = this.state
366409

367410
const changePayload = await handlers.handleFieldChange(args, fields, this, {
368411
onUpdateValue: this.updateFieldsWith,
369412
})
370413

371-
/**
372-
* Change handler for controlled fields does not return the next field props
373-
* record, therefore, need to explicitly ensure the payload was returned.
374-
*/
375-
if (changePayload) {
376-
await this.updateFieldsWith(changePayload.nextFieldProps)
377-
}
414+
return changePayload
378415

379-
/* Mark form as dirty if it's not already */
380-
if (!dirty) {
381-
this.handleFirstChange(args)
382-
}
416+
// /**
417+
// * Change handler for controlled fields does not return the next field props
418+
// * record, therefore, need to explicitly ensure the payload was returned.
419+
// */
420+
// if (changePayload) {
421+
// await this.updateFieldsWith(changePayload.nextFieldProps)
422+
// }
423+
424+
// /* Mark form as dirty if it's not already */
425+
// if (!dirty) {
426+
// this.handleFirstChange(args)
427+
// }
383428
})
384429

385430
/**

src/components/createField.jsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,11 @@ export default function connectField(options) {
227227
})
228228

229229
if (controlled && shouldUpdateRecord) {
230+
console.warn('(2) cWRP: emitting (controlled) "fieldChange" event...')
230231
this.context.form.eventEmitter.emit('fieldChange', {
231-
event: {
232-
nativeEvent: {
233-
isForcedUpdate: true,
234-
},
235-
},
236-
nextValue,
232+
isForcedUpdate: true,
237233
prevValue,
234+
nextValue,
238235
fieldProps: contextProps,
239236
})
240237
}
@@ -244,14 +241,17 @@ export default function connectField(options) {
244241
* Ensure "this.contextProps" reference is updated according to the context updates.
245242
*/
246243
componentWillUpdate(nextProps, nextState, nextContext) {
247-
/* Bypass scenarios when field is being updated, but not yet registred within the Form */
248244
const nextContextProps = R.path(this.__fieldPath, nextContext.fields)
249245

246+
/**
247+
* Bypass the scenarios when field is being updated, but not yet registred
248+
* within the Form.
249+
*/
250250
if (!nextContextProps) {
251251
return
252252
}
253253

254-
/* Update the internal reference to contextProps */
254+
/* Update the internal field's reference to contextProps */
255255
const { props: prevProps, contextProps: prevContextProps } = this
256256
this.contextProps = nextContextProps
257257

@@ -334,6 +334,7 @@ export default function connectField(options) {
334334
nextValue: customNextValue,
335335
prevValue: customPrevValue,
336336
} = args
337+
337338
const {
338339
contextProps,
339340
context: { form },
@@ -347,10 +348,12 @@ export default function connectField(options) {
347348
? customPrevValue
348349
: contextProps[valuePropName]
349350

351+
console.warn(
352+
'(1) handleChange: emitting regular "fieldChange" event...',
353+
)
350354
form.eventEmitter.emit('fieldChange', {
351-
event,
352-
nextValue,
353355
prevValue,
356+
nextValue,
354357
fieldProps: contextProps,
355358
})
356359
}

0 commit comments

Comments
 (0)