Skip to content

Instantly share code, notes, and snippets.

@erikras
Created October 28, 2016 12:23
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erikras/5ca8dc618bae5dbc8faf7d2324585d01 to your computer and use it in GitHub Desktop.
Save erikras/5ca8dc618bae5dbc8faf7d2324585d01 to your computer and use it in GitHub Desktop.
Using react-rte with redux-form
import React, { Component, PropTypes } from 'react'
class RichTextMarkdown extends Component {
static propTypes = {
input: PropTypes.shape({
onChange: PropTypes.func.isRequired,
value: PropTypes.string
}).isRequired
}
constructor(props) {
super(props)
this.state = { value: undefined }
}
componentDidMount() {
this.RichTextEditor = window.RichTextEditor
this.setState({
value: this.props.input.value ?
this.RichTextEditor.createValueFromString(this.props.input.value, 'markdown') :
this.RichTextEditor.createEmptyValue()
})
}
handleChange = value => {
this.setState({ value })
let markdown = value.toString('markdown')
if(markdown.length === 2 && markdown.charCodeAt(0) === 8203 && markdown.charCodeAt(1) === 10) {
markdown = ''
}
this.props.input.onChange(markdown)
}
render() {
const { RichTextEditor, state: { value }, handleChange } = this
return RichTextEditor ? <RichTextEditor value={value} onChange={handleChange}/> : <div/>
}
}
export default RichTextMarkdown
@markau
Copy link

markau commented Dec 7, 2016

This example really shows the power of redux and redux-forms. Just plug in a WYSIWYG editor no sweat.

I wanted to add that for Server Side Rendering (using ReactJS.NET) the window object is undefined. Therefore, I am ensuring only the client attempts to import the RichTextEditor module:

var RichTextEditor;
if (typeof(window) !== 'undefined') { RichTextEditor = require('react-rte').default; }

In this case, line 17 changes to:

this.RichTextEditor = RichTextEditor

@markau
Copy link

markau commented Dec 7, 2016

The only problem with this example is that it doesn't respond to a form reset action; the content doesn't change back to the original state.

However, if pressing undo until the content is back to the original state, then press the reset button, the form returns to a pristine state (i.e. the reset button becomes disabled.)

@erikras
Copy link
Author

erikras commented Dec 7, 2016

The only problem with this example is that it doesn't respond to a form reset action; the content doesn't change back to the original state.

Interesting observation. I am never using reset on the form where I am using this component, so it hasn't come up.

@gsdean
Copy link

gsdean commented Dec 14, 2016

Seems like the initialize action will not work either, since the editor value is only set when mounting.

In my case, I'm loading the form data async so I need to reinitialize the form with the data when its available. Any attempt I'v made to utilize componentWillReceiveProps causes the cursor to bounce around...

@markau
Copy link

markau commented Dec 18, 2016

I think you have nailed it @gsdean, the editor value being set in componentDidMount is the limitation, as this event only seems to occur once (on page load) therefore the editor instance ignores any further events. That's probably fine for a use case of loading a single editor window, and saving some data. I guess we have edge cases where the editor needs to be more aware of the redux-form state.

@markau
Copy link

markau commented Dec 19, 2016

OK! I believe the answer is this.

In the doco for Field, using a stateless component, it says:

You must define the stateless function outside of your render() method, or else it will be recreated on every render and will force the Field to rerender because its component prop will be different.

Well, we need the Field to rerender upon any changes, so this is actually desirable. I therefore defined the stateless function inside of the render() method, and now the react-rte field responds to events like form reset.

Understandably, performance isn't great with this workaround 😭

@markau
Copy link

markau commented Feb 15, 2017

Here's an example of instantiating the RTE component outside the render function.

class RichTextFieldArrayComponent extends React.Component {

  constructor(props) {
      super(props);
  }

  // The renderField const should go here

  render() {

    // The renderField const should  be instantiated OUTSIDE the render function for performance reasons.
    // However, it needs to be reloaded upon any change to the DOM, so here it is.
    //
    const renderField = ({ input, meta: { touched, error } }) => (
      <div>
        <RichTextMarkdown {...input} />
        {touched && (error && <div className="formValidationErrorText">{error}</div>)}
      </div>
    );

    return (
                {fields.map((component, index) =>
                  <div key={index}>

                              <Field
                                name={`${component}.text`}
                                component={renderField}
                              />
                  </div>
                )}
    );
  }
}

@smeijer
Copy link

smeijer commented Feb 15, 2017

I had some trouble with reset logic myself; and decided to use the componentWillReceiveProps hook as workaround.

  componentWillReceiveProps(nextProps) {
    const wasSubmitting = this.props.meta.submitting;
    const becomePristine = nextProps.meta.pristine;

    if (wasSubmitting && becomePristine) {
      const editorState = createEditorStateWithText(nextProps.input.value || '');

      this.setState({
        editorState,
      });
    }
  }

@davidsonsns
Copy link

davidsonsns commented Feb 22, 2017

Thanks @smeijer I also used the componentWillReceiveProps but changed for my situation

  componentWillReceiveProps(nextProps) {
    if (nextProps.submitSucceeded) {
      this.setState({ textEditor: RichTextEditor.createEmptyValue() });
    }
  }

@mastertux
Copy link

Very good @davidsonsns

@markau
Copy link

markau commented Feb 23, 2017

Thanks @smeijer, @davidsonsns.

Confirming componentWillReceiveProps is the answer!

@strongui
Copy link

strongui commented Mar 23, 2017

I took a bit of a different approach that should be much better performance wise, as well as integrate completely with Redux Form.


import React, { Component, PropTypes } from 'react';
import RichTextEditor from 'react-rte';
class Wysiwyg extends Component {

  static propTypes = {
    i18n: PropTypes.object.isRequired,
    onBlur: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    value: PropTypes.string
  }

  constructor (props){
    super(props);
    this.state = {
      value: this.props.value === '' ? RichTextEditor.createEmptyValue() : RichTextEditor.createValueFromString(this.props.value, 'html'),
      typing: false,
      typingTimeOut: 0
    };
    this.onChange = this.onChange.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    if(nextProps.value === '') {
      // eslint-disable-next-line react/no-set-state
      this.setState({
        ...this.state,
        value: RichTextEditor.createEmptyValue()
      });
    }
  }

  onChange(value) {
    if (this.state.typingTimeout) {
       clearTimeout(this.state.typingTimeout);
    }
    // 0.25sec timeout for sending text value to redux state for performance
    // eslint-disable-next-line react/no-set-state
    this.setState({
       value: value,
       typing: false,
       typingTimeout: setTimeout(() => {
          let isEmpty = !value.getEditorState().getCurrentContent().hasText();
          let val = isEmpty ? '' : value.toString('html');
           this.props.onChange(val);
         }, 250)
    });
  }

  render() {
    const { value } = this.state;
    //Pass down i18n object to localise labels
		const { i18n, onBlur } = this.props;
    const toolbarConfig = {
      // Optionally specify the groups to display (displayed in the order listed).
      display: ['INLINE_STYLE_BUTTONS', 'BLOCK_TYPE_BUTTONS', 'LINK_BUTTONS', 'BLOCK_TYPE_DROPDOWN', 'HISTORY_BUTTONS'],
      INLINE_STYLE_BUTTONS: [
        {label: 'Bold', style: 'BOLD'},
        {label: 'Italic', style: 'ITALIC'},
        {label: 'Underline', style: 'UNDERLINE'}
      ],
      BLOCK_TYPE_DROPDOWN: [
        {label: 'Normal', style: 'unstyled'},
        {label: 'Heading Large', style: 'header-one'},
        {label: 'Heading Medium', style: 'header-two'},
        {label: 'Heading Small', style: 'header-three'}
      ],
      BLOCK_TYPE_BUTTONS: [
        {label: 'UL', style: 'unordered-list-item'},
        {label: 'OL', style: 'ordered-list-item'}
      ]
    };
    //editorStyle={{background: 'black', height: '300px'}} in next version to allow resizing on the fly
    return (<RichTextEditor value={value} onChange={this.onChange} onBlur={onBlur} toolbarConfig={toolbarConfig} />);
  }
}

export default Wysiwyg;

@pbibalan
Copy link

pbibalan commented Jul 14, 2017

None of the above worked for me when I wanted to dynamically change the RichTextMarkdown after the form was initialized. The reason was onChange changes the state and also calls this.props.onChange which in turn triggers a props change and calls componentWillReceiveProps which would cause the cursor to jump. I ended up comparing nextProps value with the state value in componentWillReceiveProps and that works just fine. Here's the complete code:

import React, { Component, PropTypes } from 'react';
import RichTextEditor from 'react-rte';

class RichTextMarkdown extends Component {

  static propTypes = {
    onChange: PropTypes.func.isRequired,
    value: PropTypes.string
  }

  constructor(props) {
    super(props);
    this.state = {
      value: this.props.value === '' ? RichTextEditor.createEmptyValue() : RichTextEditor.createValueFromString(this.props.value, 'html'),
    };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.value !== this.state.value.toString('html')) {
      this.setState({
        value: nextProps.value ?
          RichTextEditor.createValueFromString(nextProps.value, 'html') :
          RichTextEditor.createEmptyValue()
      })
    }
  }

  onChange = (value) => {
    this.setState({ value })
    this.props.onChange(value.toString('html'))
  }

  render() {
    return (
      <RichTextEditor value={this.state.value} onChange={this.onChange} />
    );
  }
}

export default RichTextMarkdown;

@swp44744
Copy link

swp44744 commented Sep 29, 2017

@gsdean I am facing the same issue, if I use componentWillReceiveProps then cursor bounces around in the editor box. and serverside value is accessible only in nextProps.input.value and not in this.props.input.value. As componentWillReceiveProps always gets executed when value changes and set the state again and cursor bounce around.

did you had any solution for serverside data rendering with rte and redux-form?

@swp44744
Copy link

@pbibalan your solution worked to compare the current state and next props. thank you!!

@cantuket
Copy link

cantuket commented Oct 12, 2017

@pbibalan or anyone else, would you be able to provide a complete example of how you implemented and actually render the component in the form. I've literally spent 5 hrs on this now and am about to loose my mind.

Below attempt isn't recognized by redux-form. Initial value isn't available and value on submit...
<RichTextField name="overview" onChange={(value)=> value.toString('html')} />

Below attempt produces "Warning: Failed prop type: The prop onChange is marked as required in RichTextMarkdown, but its value is undefined"....

<Field
      component={RichTextField}
       type="text" 
       name="overview"
       onChange={(value)=> value.toString('html')}
/> 

I would really appreciate some help here.
Thanks!

@AlmasAskarbekov
Copy link

AlmasAskarbekov commented Oct 17, 2017

@cantuket Hey,
recently got solved almost the same issue
this solution probably solve your's:

<Field
      component={RichTextField}
       type="text" 
       name="overview"
       input={{ value: whateverState || yourValue, onChange:(value)=> value.toString('html') }}
/>

there are the Shape proptype in subj gist:

  static propTypes = {
    input: PropTypes.shape({
      onChange: PropTypes.func.isRequired,
      value: PropTypes.string
    }).isRequired

you should pass input object to Field component with value and onChange
onChange can be your custom function:
i.e. myFunc =(val)=>{ doWhatEverWith(val)}
input={{ value: whateverState || yourValue, onChange:myFunc* }} *or this.myFunc if you defined it inside a class

@shabaz-ejaz
Copy link

shabaz-ejaz commented Oct 31, 2017

None of the above work for me. The editor gets rendered but it is not being picked up in redux form.

I can't believe there is not a single example of the actual usage of this component.

Here is what I have:

RichTextEditor component:

import React, { Component, PropTypes } from 'react';
import RichTextEditor from 'react-rte';

class RichTextMarkdown extends Component {

    static propTypes = {
        input: PropTypes.shape({
            onChange: PropTypes.func.isRequired,
            value: PropTypes.string
        }).isRequired
    };
    constructor(props) {
        super(props);
        this.state = {
            value: this.props.input.value === '' ? RichTextEditor.createEmptyValue() : RichTextEditor.createValueFromString(this.props.input.value, 'html'),
        };
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.input.value !== this.state.value.toString('html')) {
            this.setState({
                value: nextProps.input.value ?
                    RichTextEditor.createValueFromString(nextProps.input.value, 'html') :
                    RichTextEditor.createEmptyValue()
            });
        }
    }

    onChange = (value) => {
        this.setState({ value });
        console.log(value.toString('html'));

        this.props.input.onChange(value);
    };

    render() {
        return (
            <RichTextEditor value={this.state.value} onChange={this.onChange} />
        );
    }
}

export default RichTextMarkdown;

Actual usage of the component:


import RTE from '../../../elements/RichTextMarkdown ';

// ... form omitted
 <Field
        component={RTE}
        type="text"
        name="comment_closing_page"
        value="test"
        input={{ value: 'TEST', onChange: value => value.toString('html') }} />

When I submit the form I get all of my other fields but I can't get hold of the values for the text editor. Any one know why this is?

@Dem0n3D
Copy link

Dem0n3D commented Nov 11, 2017

I can't believe there is not a single example of the actual usage of this component.

Yep, I'm too.
So, I fixed it.

import React, {Component} from 'react';
import RichTextEditor from 'react-rte';

class RichTextMarkdown extends Component {

    constructor(props) {
        super(props);
        this.state = {
            value: this.props.input.value === '' ?
                RichTextEditor.createEmptyValue() :
                RichTextEditor.createValueFromString(this.props.input.value, 'html')
        };
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.input.value !== this.state.value.toString('html')) {
            this.setState({
                value: nextProps.input.value ?
                    RichTextEditor.createValueFromString(nextProps.input.value, 'html') :
                    RichTextEditor.createEmptyValue()
            });
        }
    }

    onChange(value) {
        const isTextChanged = this.state.value.toString('html') != value.toString('html');
        this.setState({value}, e => isTextChanged && this.props.input.onChange(value.toString('html')));
    };

    render() {
        return (
            <RichTextEditor value={this.state.value} onChange={this.onChange.bind(this)} />
        );
    }
}

export default RichTextMarkdown;

The problem is concurrent updates of component internal state and redux state. So, we need to put onChange dispatch in setState callback.

@Dem0n3D
Copy link

Dem0n3D commented Nov 11, 2017

And you shouldn't specify input prop of Field, redux does it by itself. Use it like a simple input:

<Field name="description" component={RichTextMarkdown} />

@Hiroki111
Copy link

@smeijer
A bit old posting, but could you explain why you put const wasSubmitting?

In my case, putting the following part into OP's code solved the initialization issue with asynchronous API calls.

componentWillReceiveProps(nextProps) {
    const isPristine = nextProps.meta.pristine; 

    if (nextProps.input.value && isPristine) {
      this.setState({
        value: this.RichTextEditor.createValueFromString(nextProps.input.value, 'markdown')
      });
    }
  }

@riflemanIm
Copy link

@Dem0n3D
Thanks!

@probabble
Copy link

based on this thread, I changed the componentWillRecieveProps in order to support React 16

sstur/react-rte#242 (comment)

componentWillReceiveProps(nextProps) {    
    this.state.value.setContentFromString(nextProps.value, "html");
}

@DemiJiang33
Copy link

@Dem0n3D
Thank you so much!

@victorpavlenko
Copy link

@Dem0n3D
Thank you so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment