Draft-js: How to pass custom metadata to decorator strategy?

Created on 17 Jul 2017  Â·  7Comments  Â·  Source: facebook/draft-js

Let's say my wrapper component that renders Draft Editor has property wordsToHighlight. Basing on that I want Draft strategy to use custom element to highlight selected words. List may change over time.

How could I access this property inside Decorator strategy function?

Is it possible to pass some metadata to contentState? (contentState is one of arguments passed to startegy function)

question

Most helpful comment

@roundrobin not yet, but here's the code

You can use it exactly like the CompositeDecorator, just with he added third argument in the callback: callback(start, end, props).

Will put this into a repo when I have time, and post it here, this is in ES6-Syntax, you might have to adapt it.

import Immutable from 'immutable';

const { List } = Immutable;

const DELIMITER = '.';


function canOccupySlice(
    decorations,
    start,
    end,
) {
    for (let ii = start; ii < end; ii++) {
        if (decorations[ii] !== null) {
            return false;
        }
    }
    return true;
}

/**
 * Splice the specified component into our decoration array at the desired
 * range.
 */
function occupySlice(
    targetArr,
    start,
    end,
    componentKey,
) {
    for (let ii = start; ii < end; ii++) {
        targetArr[ii] = componentKey;
    }
}

/**
 * A CompositeDraftDecorator traverses through a list of DraftDecorator
 * instances to identify sections of a ContentBlock that should be rendered
 * in a "decorated" manner. For example, hashtags, mentions, and links may
 * be intended to stand out visually, be rendered as anchors, etc.
 *
 * The list of decorators supplied to the constructor will be used in the
 * order they are provided. This allows the caller to specify a priority for
 * string matching, in case of match collisions among decorators.
 *
 * For instance, I may have a link with a `#` in its text. Though this section
 * of text may match our hashtag decorator, it should not be treated as a
 * hashtag. I should therefore list my link DraftDecorator
 * before my hashtag DraftDecorator when constructing this composite
 * decorator instance.
 *
 * Thus, when a collision like this is encountered, the earlier match is
 * preserved and the new match is discarded.
 */
class ProppableCompositeDraftDecorator {
    constructor(decorators) {
        this._decorators = decorators.slice();
        this._props = [];
    }

    getDecorations(
        contentBlock,
        contentState,
    ) {
        const decorations = Array(contentBlock.getText().length).fill(null);

        this._decorators.forEach((/* object*/ decorator, /* number*/ ii) => {
            let counter = 0;
            const strategy = decorator.strategy;
            const callback = (/* number*/ start, /* number*/ end, props) => {
                // Find out if any of our matching range is already occupied
                // by another decorator. If so, try to wrap around it. Otherwise, store
                // the component key for rendering.
                if (canOccupySlice(decorations, start, end)) {
                    const key = ii + DELIMITER + counter + DELIMITER + contentBlock.getKey();
                    occupySlice(decorations, start, end, key);
                    this._props[key] = props;
                    counter++;
                } 
            };
            strategy(contentBlock, callback, contentState);
        });
        return List(decorations);
    }

    getComponentForKey(key) {
        const componentKey = parseInt(key.split(DELIMITER)[0], 10);
        return this._decorators[componentKey].component;
    }

    getPropsForKey(key) {
        const propsForKey = this._props[key];
        const componentKey = parseInt(key.split(DELIMITER)[0], 10);
        return { ...this._decorators[componentKey].props, ...propsForKey };
    }
}


export default ProppableCompositeDraftDecorator;

PS: I removed the "wrapping" logic I had additionally since I haven't tested it on other usecases yet and it might conflict with whatever you're trying to do…

All 7 comments

I would divide solution into two parts:

  • make from words entities
  • implement strategy that will highlight entities of given type

Second part is quite easy:

function findEntityRangesByType(entityType) {
  return (contentBlock, callback, contentState) => {
    contentBlock.findEntityRanges(
      (character) => {
        const entityKey = character.getEntity();

        if (entityKey === null) {
          return false;
        }

        return contentState.getEntity(entityKey).getType() === entityType;
      },
      callback
    );
  };
}

Strategy generator usage:

const decorator = new CompositeDecorator([{
  strategy: findEntityRangesByType('HIGHLIGHTED_WORD'),
  component: HighlightedWord,
}])

this.state = {
  editorState: EditorState.createEmpty(decorator)
}

First step isn't complicated too: you should create entity with specified type, find word's position in text and make from found position selections, and apply entity to a content state.

I hope this will help you.

Closing, since this question seems to be answered.

@xomyaq Would you be able to provide a little demo on JSFiddle? I'm struggling come up with a good solution. Would you hook into the onChange method and implement the idea you described?

I'm referring to this
First step isn't complicated too: you should create entity with specified type, find word's position in text and make from found position selections, and apply entity to a content state.

@pakoito I don't think this answers the question… entities are meaningful data for the text, that would (if you were to save the editor state) be saved too… decorators are on top and parsed in the editor without being saved to the backend)… If you were to for example implement a custom spellchecking that highlights errors with different colors based on the error, you wouldn't want to have that saved to the state but applied in real time… and for that, your strategy needs to pass data to the component…

@pie6k probably 2 years too late, but I have a solution that allows to pass props from the strategy to the component…

I wrote a custom decorator (a copy of the CompositeDecorator) and passed props from the callback to the component

@AlexandreKilian would love to take a look at your custom decorator. Do you have a repo for it?

@roundrobin not yet, but here's the code

You can use it exactly like the CompositeDecorator, just with he added third argument in the callback: callback(start, end, props).

Will put this into a repo when I have time, and post it here, this is in ES6-Syntax, you might have to adapt it.

import Immutable from 'immutable';

const { List } = Immutable;

const DELIMITER = '.';


function canOccupySlice(
    decorations,
    start,
    end,
) {
    for (let ii = start; ii < end; ii++) {
        if (decorations[ii] !== null) {
            return false;
        }
    }
    return true;
}

/**
 * Splice the specified component into our decoration array at the desired
 * range.
 */
function occupySlice(
    targetArr,
    start,
    end,
    componentKey,
) {
    for (let ii = start; ii < end; ii++) {
        targetArr[ii] = componentKey;
    }
}

/**
 * A CompositeDraftDecorator traverses through a list of DraftDecorator
 * instances to identify sections of a ContentBlock that should be rendered
 * in a "decorated" manner. For example, hashtags, mentions, and links may
 * be intended to stand out visually, be rendered as anchors, etc.
 *
 * The list of decorators supplied to the constructor will be used in the
 * order they are provided. This allows the caller to specify a priority for
 * string matching, in case of match collisions among decorators.
 *
 * For instance, I may have a link with a `#` in its text. Though this section
 * of text may match our hashtag decorator, it should not be treated as a
 * hashtag. I should therefore list my link DraftDecorator
 * before my hashtag DraftDecorator when constructing this composite
 * decorator instance.
 *
 * Thus, when a collision like this is encountered, the earlier match is
 * preserved and the new match is discarded.
 */
class ProppableCompositeDraftDecorator {
    constructor(decorators) {
        this._decorators = decorators.slice();
        this._props = [];
    }

    getDecorations(
        contentBlock,
        contentState,
    ) {
        const decorations = Array(contentBlock.getText().length).fill(null);

        this._decorators.forEach((/* object*/ decorator, /* number*/ ii) => {
            let counter = 0;
            const strategy = decorator.strategy;
            const callback = (/* number*/ start, /* number*/ end, props) => {
                // Find out if any of our matching range is already occupied
                // by another decorator. If so, try to wrap around it. Otherwise, store
                // the component key for rendering.
                if (canOccupySlice(decorations, start, end)) {
                    const key = ii + DELIMITER + counter + DELIMITER + contentBlock.getKey();
                    occupySlice(decorations, start, end, key);
                    this._props[key] = props;
                    counter++;
                } 
            };
            strategy(contentBlock, callback, contentState);
        });
        return List(decorations);
    }

    getComponentForKey(key) {
        const componentKey = parseInt(key.split(DELIMITER)[0], 10);
        return this._decorators[componentKey].component;
    }

    getPropsForKey(key) {
        const propsForKey = this._props[key];
        const componentKey = parseInt(key.split(DELIMITER)[0], 10);
        return { ...this._decorators[componentKey].props, ...propsForKey };
    }
}


export default ProppableCompositeDraftDecorator;

PS: I removed the "wrapping" logic I had additionally since I haven't tested it on other usecases yet and it might conflict with whatever you're trying to do…

Was this page helpful?
0 / 5 - 0 ratings