Skip to content

Commit 67e77da

Browse files
committed
WIP Enhancing Observable pipe to handle multiple change events
1 parent 6c7ad24 commit 67e77da

File tree

6 files changed

+272
-38
lines changed

6 files changed

+272
-38
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

@@ -62,6 +63,7 @@ storiesOf('Basics|Interaction', module)
6263
.add('Serialize', addComponent(<Serialize />))
6364
.add('Uncontrolled fields', addComponent(<UncontrolledFields />))
6465
.add('Controlled fields', addComponent(<ControlledFields />))
66+
.add('Controlled updates', addComponent(<ControlledUpdates />))
6567
.add('Submit callbacks', addComponent(<SubmitCallbacks />))
6668
.add('Form submit', addComponent(<Submit />))
6769

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Form.jsx

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

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

136177
/**
@@ -336,24 +377,28 @@ export default class Form extends React.Component {
336377
* @param {mixed} nextValue
337378
*/
338379
handleFieldChange = this.withRegisteredField(async (args) => {
380+
console.log('handleFieldChange called with', args)
381+
339382
const { fields, dirty } = this.state
340383

341384
const changePayload = await handlers.handleFieldChange(args, fields, this, {
342385
onUpdateValue: this.updateFieldsWith,
343386
})
344387

345-
/**
346-
* Change handler for controlled fields does not return the next field props
347-
* record, therefore, need to explicitly ensure the payload was returned.
348-
*/
349-
if (changePayload) {
350-
await this.updateFieldsWith(changePayload.nextFieldProps)
351-
}
388+
return changePayload
352389

353-
/* Mark form as dirty if it's not already */
354-
if (!dirty) {
355-
this.handleFirstChange(args)
356-
}
390+
// /**
391+
// * Change handler for controlled fields does not return the next field props
392+
// * record, therefore, need to explicitly ensure the payload was returned.
393+
// */
394+
// if (changePayload) {
395+
// await this.updateFieldsWith(changePayload.nextFieldProps)
396+
// }
397+
398+
// /* Mark form as dirty if it's not already */
399+
// if (!dirty) {
400+
// this.handleFirstChange(args)
401+
// }
357402
})
358403

359404
/**

src/components/createField.jsx

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

207207
if (controlled && shouldUpdateRecord) {
208+
console.warn('(2) cWRP: emitting (controlled) "fieldChange" event...')
208209
this.context.form.eventEmitter.emit('fieldChange', {
209-
event: {
210-
nativeEvent: {
211-
isForcedUpdate: true,
212-
},
213-
},
214-
nextValue,
210+
isForcedUpdate: true,
215211
prevValue,
212+
nextValue,
216213
fieldProps: contextProps,
217214
})
218215
}
@@ -222,14 +219,17 @@ export default function connectField(options) {
222219
* Ensure "this.contextProps" reference is updated according to the context updates.
223220
*/
224221
componentWillUpdate(nextProps, nextState, nextContext) {
225-
/* Bypass scenarios when field is being updated, but not yet registred within the Form */
226222
const nextContextProps = R.path(this.__fieldPath, nextContext.fields)
227223

224+
/**
225+
* Bypass the scenarios when field is being updated, but not yet registred
226+
* within the Form.
227+
*/
228228
if (!nextContextProps) {
229229
return
230230
}
231231

232-
/* Update the internal reference to contextProps */
232+
/* Update the internal field's reference to contextProps */
233233
const { props: prevProps, contextProps: prevContextProps } = this
234234
this.contextProps = nextContextProps
235235

@@ -312,6 +312,7 @@ export default function connectField(options) {
312312
nextValue: customNextValue,
313313
prevValue: customPrevValue,
314314
} = args
315+
315316
const {
316317
contextProps,
317318
context: { form },
@@ -325,10 +326,12 @@ export default function connectField(options) {
325326
? customPrevValue
326327
: contextProps[valuePropName]
327328

329+
console.warn(
330+
'(1) handleChange: emitting regular "fieldChange" event...',
331+
)
328332
form.eventEmitter.emit('fieldChange', {
329-
event,
330-
nextValue,
331333
prevValue,
334+
nextValue,
332335
fieldProps: contextProps,
333336
})
334337
}

0 commit comments

Comments
 (0)