Search code examples
reactjsasynchronousdraftjs

Perform Asynchronous Decorations in DraftJS?


I'm trying to perform real-time Named Entity Recognition highlighting in a WYSIWYG editor, which requires me to make a request to my back-end in between each keystroke.

After spending about a week on ProseMirror I gave up on it and decided to try DraftJS. I have searched the repository and docs and haven't found any asynchronous examples using Decorations. (There are some examples with Entities, but they seem like a bad fit for my problem.)

Here is the stripped down Codepen of what I'd like to solve.

It boils down to me wanting to do something like this:

const handleStrategy = (contentBlock, callback, contentState) => {
  const text = contentBlock.getText();
  let matchArr, start;
  while ((matchArr = properNouns.exec(text)) !== null) {
    start = matchArr.index;
    setTimeout(() => {
//    THROWS ERROR: Cannot read property '0' of null
      callback(start, start + matchArr[0].length);
    }, 200) // to simulate API request
  }
};

I expected it to asynchronously call the callback once the timeout resolved but instead matchArr is empty, which just confuses me.

Any help is appreciated!


Solution

  • ok, one possible solution, a example, simple version (may not be 100% solid) :

    1. write a function take editor's string, send it to server, and resolve the data get from server, you need to figure out send the whole editor string or just one word
    getServerResult = data => new Promise((resolve, reject) => {
          ...
    
          fetch(link, {
            method: 'POST',
            headers: {
              ...
            },
            // figure what to send here
            body: this.state.editorState.getCurrentContent().getPlainText(),
          })
            .then(res => resolve(res))
            .catch(reject);
        });
    
    1. determine when to call the getServerResult function(i.e when to send string to server and get entity data), from what I understand from your comment, when user hit spacebar key, send the word before to server, this can done by draftjs Key Bindings or react SyntheticEvent. You will need to handle case what if user hit spacebar many times continuously.
    function myKeyBindingFn(e: SyntheticKeyboardEvent): string {
      if (e.keyCode === 32) {
        return 'send-server';
      }
      return getDefaultKeyBinding(e);
    }
    
    async handleKeyCommand(command: string): DraftHandleValue {
      if (command === 'send-server') {
        // you need to manually add a space char to the editorState
        // and get result from server
    
        ...
    
        // entity data get from server
        const result = await getServerResult()
    
        return 'handled';
      }
      return 'not-handled';
    }
    
    1. add entity data get from server to specific word using ContentState.createEntity()
      async handleKeyCommand(command: string): DraftHandleValue {
        if (command === 'send-server') {
          // you need to manually add a space char to the editorState
          // and get result from server
    
          ...
    
          // entity data get from server
          const result = await getServerResult()
    
          const newContentState = ContentState.createEntity(
            type: 'string',
            mutability: ...
            data: result
          )
    
          const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
    
          // you need to figure out the selectionState, selectionState mean add 
          // the entity data to where
    
          const contentStateWithEntity = Modifier.applyEntity(
             newContentState,
             selectionState,
             entityKey
          );
    
          // create a new EditorState and use this.setState()
          const newEditorState = EditorState.push(
            ...
            contentState: contentStateWithEntity
          )
    
          this.setState({
            editorState: newEditorState
          })
    
          return 'handled';
        }
        return 'not-handled';
      }
    
    1. create different decorators find words with specific entity data, and return different style or whatever you need to return
    ...
    const compositeDecorator = new CompositeDecorator([
      strategy: findSubjStrategy,
      component: HandleSubjSpan,
    ])
    
    function findSubjStrategy(contentBlock, callback, contentState) {
      // search whole editor content find words with subj entity data
      // if the word's entity data === 'Subj'
      // pass the start index & end index of the word to callback
    
      ...
      if(...) {
       ... 
       callback(startIndex, endIndex);
      }
    }
    
    // this function handle what if findSubjStrategy() find any word with subj
    // entity data
    const HandleSubjSpan = (props) => {
    
      // if the word with subj entity data, it font color become red
      return <span {...props} style={{ color: 'red' }}>{props.children}</span>;
    };