Skip to content

Commit

Permalink
WIP Enhancing Observable pipe to handle multiple change events
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Oct 24, 2018
1 parent 6c7ad24 commit 67e77da
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 38 deletions.
170 changes: 170 additions & 0 deletions examples/basics/ControlledUpdates.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import * as R from 'ramda'
import React from 'react'
import { Form } from '@lib'
import { Input, Radio, Checkbox, Select, Textarea } from '@fields'
import Button from '@shared/Button'

const nextState = {
inputOne: 'foo',
inputTwo: 'bar',
radio: 'cheese',
// checkbox1: true,
// checkbox2: false,
// select: 'three',
// textareaOne: 'Text',
// textareaTwo: 'Everywhere',
}

export default class ControlledFields extends React.Component {
state = {
inputOne: '',
inputTwo: 'foo',
radio: 'potato',
checkbox1: false,
checkbox2: true,
select: 'two',
textareaOne: '',
textareaTwo: 'something',
}

handleFieldChange = ({ nextValue, fieldProps }) => {
this.setState({
[fieldProps.name]: nextValue,
})
}

handlePrefillClick = () => {
this.setState(nextState)
}

handleSubmit = ({ serialized }) => {
Object.keys(nextState).forEach((fieldName) => {
const serializedValue = serialized[fieldName]
const expectedValue = nextState[fieldName]
console.assert(
R.equals(serializedValue, expectedValue),
`Invalid state for "${fieldName}". Expected: "${expectedValue}", got: "${serializedValue}".`,
)
})

return new Promise((resolve) => resolve())
}

render() {
const {
inputOne,
inputTwo,
radio,
checkbox1,
checkbox2,
select,
textareaOne,
textareaTwo,
} = this.state

return (
<React.Fragment>
<h1>Controlled updates</h1>

<Form
id="form"
ref={this.props.getRef}
action={this.handleSubmit}
onSubmitStart={this.props.onSubmitStart}
>
{/* Inputs */}
<Input
id="inputOne"
name="inputOne"
label="Field one"
value={inputOne}
onChange={this.handleFieldChange}
/>
<Input
id="inputTwo"
label="Field two"
name="inputTwo"
value={inputTwo}
onChange={this.handleFieldChange}
/>

{/* Radio */}
<Radio
id="radio1"
name="radio"
label="Cheese"
value="cheese"
checked={radio === 'cheese'}
onChange={this.handleFieldChange}
/>
<Radio
id="radio2"
name="radio"
label="Potato"
value="potato"
checked={radio === 'potato'}
onChange={this.handleFieldChange}
/>
<Radio
id="radio3"
name="radio"
label="Cucumber"
value="cucumber"
checked={radio === 'cucumber'}
onChange={this.handleFieldChange}
/>

{/* Checkboxes */}
<Checkbox
id="checkbox1"
name="checkbox1"
label="Checkbox one"
checked={checkbox1}
onChange={this.handleFieldChange}
/>
<Checkbox
id="checkbox2"
name="checkbox2"
label="Checkbox two"
checked={checkbox2}
onChange={this.handleFieldChange}
/>

{/* Select */}
<Select
id="select"
name="select"
label="Select"
value={select}
onChange={this.handleFieldChange}
>
<option value="one">one</option>
<option value="two">two</option>
<option value="three">three</option>
</Select>

{/* Textareas */}
<Textarea
id="textareaOne"
name="textareaOne"
label="Textarea one"
onChange={this.handleFieldChange}
value={textareaOne}
onChange={this.handleFieldChange}
/>
<Textarea
id="textareaTwo"
name="textareaTwo"
label="Textarea two"
onChange={this.handleFieldChange}
value={textareaTwo}
onChange={this.handleFieldChange}
/>

<Button>Submit</Button>
<span onClick={this.handlePrefillClick}>Pre-fill</span>
</Form>
</React.Fragment>
)
}
}
2 changes: 2 additions & 0 deletions examples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Reset from './basics/Reset'
import Serialize from './basics/Serialize'
import UncontrolledFields from './basics/UncontrolledFields'
import ControlledFields from './basics/ControlledFields'
import ControlledUpdates from './basics/ControlledUpdates'
import SubmitCallbacks from './basics/SubmitCallbacks'
import Submit from './basics/Submit'

Expand Down Expand Up @@ -62,6 +63,7 @@ storiesOf('Basics|Interaction', module)
.add('Serialize', addComponent(<Serialize />))
.add('Uncontrolled fields', addComponent(<UncontrolledFields />))
.add('Controlled fields', addComponent(<ControlledFields />))
.add('Controlled updates', addComponent(<ControlledUpdates />))
.add('Submit callbacks', addComponent(<SubmitCallbacks />))
.add('Form submit', addComponent(<Submit />))

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 58 additions & 13 deletions src/components/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EventEmitter } from 'events'
import { Observable } from 'rxjs/internal/Observable'
import { fromEvent } from 'rxjs/internal/observable/fromEvent'
import { bufferTime } from 'rxjs/internal/operators/bufferTime'
import * as rxJs from 'rxjs/internal/operators'

/* Internal modules */
import {
Expand Down Expand Up @@ -125,12 +126,52 @@ export default class Form extends React.Component {
/* Field events observerables */
fromEvent(eventEmitter, 'fieldRegister')
.pipe(bufferTime(50))
/**
* @todo @performance
* Iterative state updates have no reason. Once the register events are buffered,
* perform a single state update with all the pending fields.
*/
.subscribe((pendingFields) => pendingFields.forEach(this.registerField))
fromEvent(eventEmitter, 'fieldFocus').subscribe(this.handleFieldFocus)
fromEvent(eventEmitter, 'fieldChange').subscribe(this.handleFieldChange)

fromEvent(eventEmitter, 'fieldChange')
.pipe(
rxJs.bufferTime(50),
rxJs.filter(R.complement(R.isEmpty)),
rxJs.tap((e) => console.log('buffered:', e)),
rxJs.map(R.map(this.handleFieldChange)),
rxJs.tap((e) => console.log('mapped:', e)),
)
.subscribe(async (pendingUpdates) => {
console.log({ pendingUpdates })

const fieldsList = await Promise.all(pendingUpdates)
console.log({ fieldsList })

const updatedFields = fieldsList.filter(Boolean)
console.log({ updatedFields })

if (updatedFields.length === 0) {
return
}

const fieldsDelta = fieldUtils.stitchFields(updatedFields)
console.log({ fieldsDelta })

const nextFields = R.mergeDeepRight(this.state.fields, fieldsDelta)
console.log('next fields:', nextFields)

return this.setState({ fields: nextFields })
})

fromEvent(eventEmitter, 'fieldBlur').subscribe(this.handleFieldBlur)
fromEvent(eventEmitter, 'fieldUnregister').subscribe(this.unregisterField)
fromEvent(eventEmitter, 'validateField').subscribe(this.validateField)

/**
* @todo @performance
* Buffer incoming unregister events and dispatch a single state update.
*/
fromEvent(eventEmitter, 'fieldUnregister').subscribe(this.unregisterField)
}

/**
Expand Down Expand Up @@ -336,24 +377,28 @@ export default class Form extends React.Component {
* @param {mixed} nextValue
*/
handleFieldChange = this.withRegisteredField(async (args) => {
console.log('handleFieldChange called with', args)

const { fields, dirty } = this.state

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

/**
* Change handler for controlled fields does not return the next field props
* record, therefore, need to explicitly ensure the payload was returned.
*/
if (changePayload) {
await this.updateFieldsWith(changePayload.nextFieldProps)
}
return changePayload

/* Mark form as dirty if it's not already */
if (!dirty) {
this.handleFirstChange(args)
}
// /**
// * Change handler for controlled fields does not return the next field props
// * record, therefore, need to explicitly ensure the payload was returned.
// */
// if (changePayload) {
// await this.updateFieldsWith(changePayload.nextFieldProps)
// }

// /* Mark form as dirty if it's not already */
// if (!dirty) {
// this.handleFirstChange(args)
// }
})

/**
Expand Down
23 changes: 13 additions & 10 deletions src/components/createField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,11 @@ export default function connectField(options) {
})

if (controlled && shouldUpdateRecord) {
console.warn('(2) cWRP: emitting (controlled) "fieldChange" event...')
this.context.form.eventEmitter.emit('fieldChange', {
event: {
nativeEvent: {
isForcedUpdate: true,
},
},
nextValue,
isForcedUpdate: true,
prevValue,
nextValue,
fieldProps: contextProps,
})
}
Expand All @@ -222,14 +219,17 @@ export default function connectField(options) {
* Ensure "this.contextProps" reference is updated according to the context updates.
*/
componentWillUpdate(nextProps, nextState, nextContext) {
/* Bypass scenarios when field is being updated, but not yet registred within the Form */
const nextContextProps = R.path(this.__fieldPath, nextContext.fields)

/**
* Bypass the scenarios when field is being updated, but not yet registred
* within the Form.
*/
if (!nextContextProps) {
return
}

/* Update the internal reference to contextProps */
/* Update the internal field's reference to contextProps */
const { props: prevProps, contextProps: prevContextProps } = this
this.contextProps = nextContextProps

Expand Down Expand Up @@ -312,6 +312,7 @@ export default function connectField(options) {
nextValue: customNextValue,
prevValue: customPrevValue,
} = args

const {
contextProps,
context: { form },
Expand All @@ -325,10 +326,12 @@ export default function connectField(options) {
? customPrevValue
: contextProps[valuePropName]

console.warn(
'(1) handleChange: emitting regular "fieldChange" event...',
)
form.eventEmitter.emit('fieldChange', {
event,
nextValue,
prevValue,
nextValue,
fieldProps: contextProps,
})
}
Expand Down
Loading

0 comments on commit 67e77da

Please sign in to comment.