Slate: Characters are typed twice with both onBeforeChange and onChange

Created on 24 Apr 2017  Â·  9Comments  Â·  Source: ianstormtaylor/slate

Here's a fiddle illustrating the behaviour: https://jsfiddle.net/4890mgej/4/

Code:

class MyEditor extends React.Component {

  constructor(props) {
    super(props)
    this.state = {
      state: Plain.deserialize('')
    }
  }

  onChange(state) {
    this.setState({ state })
  }

  onBeforeChange(state) {
    return state.transform().setBlock('paragraph').apply()
  }

  render() {
    return (
      <Editor
        placeholder="Enter some text..."
        onChange={state => this.onChange(state)}
        onBeforeChange={state => this.onBeforeChange(state)}
        state={this.state.state}
      />
    )
  }

}
âš‘ ux

Most helpful comment

I have a similar use case, where I need to setNodeByKey on a bunch of custom nodes to update their data (either onBeforeChange or onChange) and there are various reasons why a schema rule isn't well suited. Unfortunately I'm also getting the duplicate text issue when using either callback.

As with @jatins' example, being able to insert some transformations between when the next document state is computed from inputs and the render happens is occasionally very useful.

If onBeforeChange's days are numbered, what about an alternative with a more explicit purpose of finalising state—a blunter instrument than a schema rule—which can be used with the understanding that it might cause suboptimal re-renders?

All 9 comments

Hey @jatins, this is actually a UX issue, I'm curious to get your thoughts on. (And anyone else!)

Basically what's going on here is that for certain operations, (like inserting single text characters), Slate uses the isNative flag to avoid having to re-render, and instead just accept what is in the DOM, since these are cases where we know contenteditable will get the insertion right. This greatly improves performance since it avoids more re-renders.

But in this case, when using onBeforeChange and performing another transform, that flag is removed. This results in the original DOM event not having been preventDefault()'d, and also that the rendering logic adds the character too.

What are you looking to use onBeforeChange for here?

I'm considering removing it entirely since it leads to weird issues.

My use case is essentially a series of these steps:

  1. Get _text_ content from editor
  2. Based on text content set block type, or do some formatting. However, at this point I want the text _after_ edit. (So, I can't use something like onKeyDown which essentially gives me the state _before_ edit is applied)
  3. Render the new state.

But also, I would want the typed character to be displayed as soon as user types (which I think won't happen if I preventDefault).

Also, that double character typed behaviour doesn't have much to do with onBeforeChange. If I do this inside my onChange I get the same behaviour.

 onChange(state) {
    this.setState({ state: state.transform().setBlock('paragraph').apply() })
  }

https://jsfiddle.net/4890mgej/5/

I have a similar use case, where I need to setNodeByKey on a bunch of custom nodes to update their data (either onBeforeChange or onChange) and there are various reasons why a schema rule isn't well suited. Unfortunately I'm also getting the duplicate text issue when using either callback.

As with @jatins' example, being able to insert some transformations between when the next document state is computed from inputs and the render happens is occasionally very useful.

If onBeforeChange's days are numbered, what about an alternative with a more explicit purpose of finalising state—a blunter instrument than a schema rule—which can be used with the understanding that it might cause suboptimal re-renders?

If you're not bothered about summoning the dark lord of hacks, something like this could do the job...

const Plugin = (options) => {
  let checksum
  return {
    schema: {
      rules: [
        {
          match: node => node.kind === 'document',
          validate: node => node.data.get('checksum') !== checksum ? node : null,
          normalize: (transform, node) => transform
            .setNodeByKey(node.key, { data: { checksum } })
            .call(transformAllTheThings)
        }
      ]
    },
    onBeforeChange() {
      checksum = Math.floor(Math.random() * 1e16).toString(32)
    }
  }
}

💻 :trollface:

It doesn't play well with undo/redo though

I agree with @soulwire and onBeforeChange can be very useful when we want to transform the state before applying the state change. E.g. I have markdown editor, when I type something, I want to parse current block and see if it's an H1 block, then decide to update the current block type.

It looks like the root cause is that the onInput handle in content.js/div is triggered which inserts the character again.

It seems like if I just return the state in onBeforeInput handler, the double character issue is solved. See https://jsfiddle.net/f5gaL2c0/1/

The doc said if a state is returned from one of the plugin handlers, the other plugin will not be triggered. Does that mean core plugin's onBeforeInput is skipped in this case? Seems like core plugin is doing a lot of work in onBeforeInput https://github.com/ianstormtaylor/slate/blob/3eb72a86422acb543f518292a19965bb039b1604/src/plugins/core.js#L76. What's the implication on skipping it?

Should be fixed with isNative removed now!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AlexeiAndreev picture AlexeiAndreev  Â·  3Comments

ianstormtaylor picture ianstormtaylor  Â·  3Comments

adrianclay picture adrianclay  Â·  3Comments

vdms picture vdms  Â·  3Comments

ianstormtaylor picture ianstormtaylor  Â·  3Comments