Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Stale) WIP Enhance Observable pipe to handle concurrent events #311

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -78,6 +79,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('Form submit', addComponent(<Submit />))
.add('Submit callbacks', addComponent(<SubmitCallbacks />))

Expand Down
71 changes: 58 additions & 13 deletions src/components/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 @@ -124,12 +125,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 @@ -362,24 +403,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 @@ -227,14 +227,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 @@ -244,14 +241,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 @@ -334,6 +334,7 @@ export default function connectField(options) {
nextValue: customNextValue,
prevValue: customPrevValue,
} = args

const {
contextProps,
context: { form },
Expand All @@ -347,10 +348,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