Skip to content

Commit

Permalink
[form-builder] Fix race condition when patching block node values (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjoerge committed Nov 7, 2017
1 parent 57300b2 commit 9f189e1
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export default class BlockEditor extends React.Component {
type: PropTypes.any,
level: PropTypes.number,
value: PropTypes.instanceOf(State),
onChange: PropTypes.func
onChange: PropTypes.func,
onNodePatch: PropTypes.func
}

static defaultProps = {
Expand Down Expand Up @@ -63,6 +64,8 @@ export default class BlockEditor extends React.Component {
// this._inputContainer.removeEventListener('mousewheel', this.handleInputScroll)
}

handleNodePatch = event => this.props.onNodePatch(event)

handleInsertBlock = item => {
if (item.options && item.options.inline) {
this.operations.insertInline(item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export default class FormBuilderBlock extends React.Component {
node: PropTypes.object,
editor: PropTypes.object,
state: PropTypes.object,
attributes: PropTypes.object
attributes: PropTypes.object,
onPatch: PropTypes.func
}

state = {
Expand All @@ -44,30 +45,13 @@ export default class FormBuilderBlock extends React.Component {
}

handleChange = event => {
const {node, editor} = this.props
const change = editor.getState()
.change()
.setNodeByKey(node.key, {
data: {value: applyAll(node.data.get('value'), event.patches)}
})
editor.onChange(change)
const {onPatch, node} = this.props
onPatch(event.prefixAll(node.key))
}

handleInvalidValueChange = event => {
// the setimeout is a workaround because there seems to be a race condition with clicks and state updates
setTimeout(() => {
const {node, editor} = this.props

const nextValue = applyAll(node.data.get('value'), event.patches)
const change = editor.getState().change()
const nextChange = (nextValue === undefined)
? change.removeNodeByKey(node.key)
: change.setNodeByKey(node.key, {
data: {value: nextValue}
})

editor.onChange(nextChange)
}, 0)
const {onPatch, node} = this.props
onPatch(event.prefixAll(node.key))
}

handleDragStart = event => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export default class FormBuilderInline extends React.Component {
node: PropTypes.object,
editor: PropTypes.object,
state: PropTypes.object,
attributes: PropTypes.object
attributes: PropTypes.object,
onPatch: PropTypes.func
}

state = {
Expand All @@ -44,30 +45,13 @@ export default class FormBuilderInline extends React.Component {
}

handleChange = event => {
const {node, editor} = this.props
const change = editor.getState()
.change()
.setNodeByKey(node.key, {
data: {value: applyAll(node.data.get('value'), event.patches)}
})

editor.onChange(change)
const {onPatch, node} = this.props
onPatch(event.prefixAll(node.key))
}

handleInvalidValueChange = event => {
// the setimeout is a workaround because there seems to be a race condition with clicks and state updates
setTimeout(() => {
const {node, editor} = this.props

const nextValue = applyAll(node.data.get('value'), event.patches)
const change = editor.getState().change()
const nextChange = (nextValue === undefined)
? change.removeNodeByKey(node.key)
: change.setNodeByKey(node.key, {
data: {value: nextValue}
})
editor.onChange(nextChange)
}, 0)
const {onPatch, node} = this.props
onPatch(event.prefixAll(node.key))
}

handleDragStart = event => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import PatchEvent, {set, unset} from '../../PatchEvent'
import withPatchSubscriber from '../../utils/withPatchSubscriber'
import Button from 'part:@sanity/components/buttons/default'
import styles from './styles/Syncer.css'
import apply from '../../simplePatch'

function deserialize(value, type) {
return State.fromJSON(blockTools.blocksToSlateState(value, type))
}

function serialize(state, type) {
return blockTools.slateStateToBlocks(state.toJSON({preserveKeys: true}), type)
}
Expand Down Expand Up @@ -64,12 +66,28 @@ export default withPatchSubscriber(class Syncer extends React.PureComponent {
this.unsubscribe = props.subscribe(this.receivePatches)
}

handleNodePatch = patchEvent => {
this.setState(prevState => {
if (prevState.isOutOfSync) {
return prevState
}
const nextValue = patchEvent.patches.reduce((state, patch) => {
const [key, ...path] = patch.path
const nodeValue = state.document.getDescendant(key).data.get('value')
const change = state.change()
.setNodeByKey(key, {
data: {value: apply(nodeValue, {...patch, path})}
})
return change.state
}, prevState.value)

return {value: nextValue}
})
}
handleChange = slateChange => {
this.setState(prevState => (prevState.isOutOfSync ? {} : {value: slateChange.state}))
}

receivePatches = ({snapshot, shouldReset, patches}) => {

if (patches.some(patch => patch.origin === 'remote')) {
this.setState({isOutOfSync: true})
}
Expand Down Expand Up @@ -125,11 +143,12 @@ export default withPatchSubscriber(class Syncer extends React.PureComponent {
const {type} = this.props
return (
<div className={styles.root}>
{ !isDeprecated && (
{!isDeprecated && (
<BlockEditor
{...this.props}
disabled={isOutOfSync}
onChange={this.handleChange}
onNodePatch={this.handleNodePatch}
value={value}
/>)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import FormBuilderBlock from './FormBuilderBlock'

export default function createBlockNode(type) {
export default function createBlockNode(type, onPatch) {
return function BlockNode(props) {
return <FormBuilderBlock type={type} {...props} />
return <FormBuilderBlock type={type} {...props} onPatch={onPatch} />
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import FormBuilderInline from './FormBuilderInline'

export default function createInlineNode(type) {
export default function createInlineNode(type, onPatch) {
return function InlineNode(props) {
return <FormBuilderInline type={type} {...props} />
return <FormBuilderInline type={type} {...props} onPatch={onPatch} />
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react'
import styles from '../styles/contentStyles/Normal.css'

function Normal(props) {
return <p {...props.attributes} className={styles.root}>{props.children}</p>
return <div {...props.attributes} className={styles.root}>{props.children}</div>
}

Normal.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ export default function prepareSlateForBlockEditor(blockEditor) {
const spanType = getSpanType(type)
const allowedDecorators = spanType.decorators.map(decorator => decorator.value)

const FormBuilderBlock = createBlockNode(type)
const FormBuilderInline = createInlineNode(type)
const FormBuilderBlock = createBlockNode(type, blockEditor.handleNodePatch)
const FormBuilderInline = createInlineNode(type, blockEditor.handleNodePatch)

const slateSchema = {
nodes: {
Expand Down
32 changes: 31 additions & 1 deletion packages/test-studio/schemas/uploads.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export default {
name: 'uploadsTest',
type: 'document',
Expand All @@ -16,6 +15,37 @@ export default {
type: 'array',
of: [{type: 'image', title: 'Image'}]
},
{
name: 'blocks',
title: 'Blocks',
description: 'Upload to array of images in block text',
type: 'array',
of: [
{type: 'block'},
{
type: 'object',
title: 'Gallery',
fields: [
{
name: 'title',
title: 'Title',
type: 'string'
},
{
name: 'images',
type: 'array',
of: [{type: 'image', title: 'Image'}]
}
],
preview: {
select: {
title: 'title',
imageUrl: 'images.0.asset.url'
}
}
}
]
},
{
name: 'imagesAndFiles',
title: 'Images and files',
Expand Down

0 comments on commit 9f189e1

Please sign in to comment.