react-codemirror2 icon indicating copy to clipboard operation
react-codemirror2 copied to clipboard

CodeMirror2 refresh

Open ghost opened this issue 6 years ago • 27 comments

Hello,

I'm currently building app based on react-codemirror2 and reactstrap. I have editor component inside Collapse component. Unfortunately with this setup, editor component is always blank until I click on it.

Based on this issues: https://stackoverflow.com/questions/8349571/codemirror-editor-is-not-loading-content-until-clicked https://stackoverflow.com/questions/10575833/codemirror-has-content-but-wont-display-until-keypress https://stackoverflow.com/questions/5364909/javascript-codemirror-refresh-textarea/5377029#5377029

I need to call .refresh() method on CodeMirror instance.

I've tried to set the instance with editorDidMount() and then calling this.instance.refresh() in componentDidMount() function, but this not helped.

Code:

import React, { Component }  from 'react';
import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import {faTrashAlt} from "@fortawesome/fontawesome-free-solid/index";
import {Button, ButtonGroup, Card, CardBody, Col, Collapse, Container, FormGroup, Label, Row} from "reactstrap";
import {Controlled as CodeMirror} from 'react-codemirror2'
require('codemirror/lib/codemirror.css');
require('codemirror/mode/javascript/javascript.js');
require('codemirror/mode/htmlmixed/htmlmixed.js');

class CodeBlock extends Component {
    constructor(props) {
        super(props);
        this.toggle = this.toggle.bind(this);
        this.state = {
            collapse: false,
            value: props.initialValue,
            mode: props.mode
        };
        this.instance = null;
    }

    toggle() {
        this.setState({ collapse: !this.state.collapse });
    }

    componentDidMount() {
        // setTimeout(() => {this.instance.refresh()}, 0); // Doesn't work
        this.instance.refresh();
    }

    render() {
        return (
            <div className="mb-3">
                <Button color="outline-secondary" onClick={this.toggle} className="w-100">Block {this.state.mode}</Button>
                <Collapse isOpen={this.state.collapse}>
                    <Card>
                        <CardBody>
                            <Container>
                                <Row>
                                    <Col xs="2">
                                        <FormGroup>
                                            <Label>Actions</Label>
                                            <div>
                                                <ButtonGroup>
                                                    <Button className="btn" color="danger"><FontAwesomeIcon icon={faTrashAlt}/></Button>
                                                </ButtonGroup>
                                            </div>
                                        </FormGroup>
                                    </Col>
                                </Row>
                                <Row>
                                    <Col>
                                        <CodeMirror
                                            value={this.state.value}
                                            options={{
                                                mode: this.state.mode,
                                                lineNumbers: true,
                                                autoRefresh:true
                                            }}
                                            editorDidMount={editor => { this.instance = editor }}
                                            onBeforeChange={(editor, data, value) => {
                                                this.setState({value});
                                            }}
                                            onChange={(editor, data, value) => {
                                            }}
                                        />
                                    </Col>
                                </Row>
                            </Container>
                        </CardBody>
                    </Card>
                </Collapse>
            </div>
        )
    }
}

export default CodeBlock;

ghost avatar Apr 28 '18 22:04 ghost

I guesst the component mounts before editorDidMount is called. Try to refresh as soon as you have the instance (ie in the editorDidMount callback).

cristiano-belloni avatar May 02 '18 18:05 cristiano-belloni

Nope. After some testing with console.log(), editorDidMount is first, second is componentDidMount.

Refreshing editor inside editorDidMount blocks editor, and I can't even type.

setTimeout(() => {this.instance.refresh()}, 3000); doesn't work too.

ghost avatar May 02 '18 19:05 ghost

@dyzajash have you seen the bit on [autorefresh]?(https://codemirror.net/doc/manual.html#addon_autorefresh)

This addon can be useful when initializing an editor in a hidden DOM node, in cases where it is difficult to call refresh when the editor becomes visible. It defines an option autoRefresh which you can set to true to ensure that, if the editor wasn't visible on initialization, it will be refreshed the first time it becomes visible. This is done by polling every 250 milliseconds (you can pass a value like {delay: 500} as the option value to configure this). Note that this addon will only refresh the editor once when it first becomes visible, and won't take care of further restyling and resizing.

I looks like the second most StackOverflow answer goes this route with success. Try and let me know?

scniro avatar May 03 '18 12:05 scniro

I saw it, but anyone have idea how to use it with react & webpack?

Addon is not added to npm package, simply downloading & requiring from app folder is not working.

I think I need to edit plugin source or place it manually inside node_modules folder. I will update this comment, when I try these two options.

ghost avatar May 04 '18 13:05 ghost

Hi, @dyzajash I have the same problem with you. I think there have two method to solve it. One, according to this link https://codemirror.net/doc/manual.html#addon_autorefresh, add autorefresh to the options, like autoRefresh{delay:500}. But,the source of the autorefresh code :

CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
    if (cm.state.autoRefresh) {
      stopListening(cm, cm.state.autoRefresh)
      cm.state.autoRefresh = null
    }
    if (val && cm.display.wrapper.offsetHeight == 0)
      startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
  })

If offsetHeight is equal zero. It will go right. Otherwise...... Second the solve was like you said ,add refresh method to the source code of react-codemirror2

divefox avatar May 11 '18 09:05 divefox

@dyzajash @divefox I understand the issue more clearly now, thanks for the latest explanation. I looked at that plugin and have no problem baking that into the source. It's certainly small enough to pull off without much maintenance moving forward. I'll get a new release out this weekend. Thanks for the collaboration on this!

scniro avatar May 11 '18 11:05 scniro

@scniro Thanks !

divefox avatar May 12 '18 03:05 divefox

@scniro I think you needn't update the source code.I know where to refresh the code.

@dyzajash You can use this code instead of old

editorDidMount={editor => { this.instance = editor; this.instance.refresh() }}

and remove componentDidMount().

In my code, I do this, It's OK. I don't know why in componentDidMount to refresh codemirror is bad

divefox avatar May 14 '18 07:05 divefox

@divefox that's odd, didn't we essentially try to do that? Either way I didn't get the free time to get this in over the weekend anyways 😞 Are you suggesting this should work as-is? Would be stoked if @dyzajash could confirm

scniro avatar May 14 '18 13:05 scniro

@scniro In that way, It is works for me, I can debug into codemirror refresh method. Needs @dyzajash to confirm. @scniro In the post, first get editor and then in componentDidMount method to refresh editor.It is not worked. But get editor and then refresh. It is works.

divefox avatar May 16 '18 01:05 divefox

I'll test this today and update comment :)

ghost avatar May 21 '18 12:05 ghost

@dyzajash great look forward to it just keep us posted 😺

scniro avatar May 21 '18 12:05 scniro

Sorry for late update.

import React, { Component }  from 'react';
import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import {faTrashAlt} from "@fortawesome/fontawesome-free-solid/index";
import {Button, ButtonGroup, Card, CardBody, Col, Collapse, Container, FormGroup, Label, Row} from "reactstrap";
import {Controlled as CodeMirror} from 'react-codemirror2'
require('codemirror/lib/codemirror.css');
require('codemirror/mode/javascript/javascript.js');
require('codemirror/mode/htmlmixed/htmlmixed.js');

export default class CodeBlock extends Component {
    constructor(props) {
        super(props);
        this.toggle = this.toggle.bind(this);
        this.state = {
            collapse: false,
            editorValue: props.initialValue,
            mode: props.mode
        };
    }

    toggle() {
        this.setState({ collapse: !this.state.collapse });
    }

    render() {
        return (
            <div className="mb-3">
                <Button color="outline-secondary" onClick={this.toggle} className="w-100">Blok kodu {this.state.mode}</Button>
                <Collapse isOpen={this.state.collapse}>
                    <Card>
                        <CardBody>
                            <Container>
                                <Row>
                                    <Col xs="2">
                                        <FormGroup>
                                            <Label>Akcje</Label>
                                            <div>
                                                <ButtonGroup>
                                                    <Button className="btn" color="danger"><FontAwesomeIcon icon={faTrashAlt}/></Button>
                                                </ButtonGroup>
                                            </div>
                                        </FormGroup>
                                    </Col>
                                </Row>
                                <Row>
                                    <Col>
                                        <CodeMirror
                                            value={this.state.editorValue}
                                            options={{
                                                mode: this.state.mode,
                                                lineNumbers: true,
                                                autoRefresh:true
                                            }}
                                            editorDidMount={editor => { this.instance = editor; this.instance.refresh() }}
                                            onBeforeChange={(editor, data, value) => {
                                                this.setState({value});
                                            }}
                                        />
                                    </Col>
                                </Row>
                            </Container>
                        </CardBody>
                    </Card>
                </Collapse>
            </div>
        )
    }
}

EditorDidMount method is not working for me.

ghost avatar May 24 '18 21:05 ghost

Is this issue solved? I have same issue to use this component T_T


I just solve this problem with this trick.

<Col>
{
this.state.collapse && (<CodeMirror
                                            value={this.state.editorValue}
                                            options={{
                                                mode: this.state.mode,
                                                lineNumbers: true
                                            }}
                                            onBeforeChange={(editor, data, value) => {
                                                this.setState({value});
                                            }}
                                      />)
}
</Col>

but it has little bit slow rendering. waiting for graceful solution.

jo8937 avatar Jun 14 '18 11:06 jo8937

Problem

CodeMirror has autorefresh addon but the code below doesn't work.

import { UnControlled as CodeMirror } from 'react-codemirror2';
require('codemirror/addon/display/autorefresh');

(snip)

render() {
  return <CodeMirror
    options={{
      autoRefresh: true
    }}
  />
}

Cause

cm.display.wrapper.offsetHeight of autorefresh.js#19 may NOT become zero depending on the timing of initialization. In those case startListening will not be triggered.

Solution

  1. Create autorefresh.ext.js
    /**
     * extends codemirror/addon/display/autorefresh
     * 
     * @author Yuki Takei <[email protected]>
     * @see https://codemirror.net/addon/display/autorefresh.js
     * @see https://github.com/scniro/react-codemirror2/issues/83#issuecomment-398825212
     */
    /* eslint-disable */
    
    // CodeMirror, copyright (c) by Marijn Haverbeke and others
    // Distributed under an MIT license: http://codemirror.net/LICENSE
    
    (function(mod) {
      mod(require("codemirror"));
    })(function(CodeMirror) {
      "use strict"
    
      CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
        if (cm.state.autoRefresh) {
          stopListening(cm, cm.state.autoRefresh)
          cm.state.autoRefresh = null
        }
        if (val && (val.force || cm.display.wrapper.offsetHeight == 0))
          startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
      })
    
      function startListening(cm, state) {
        function check() {
          if (cm.display.wrapper.offsetHeight) {
            stopListening(cm, state)
            if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
              cm.refresh()
          } else {
            state.timeout = setTimeout(check, state.delay)
          }
        }
        state.timeout = setTimeout(check, state.delay)
        state.hurry = function() {
          clearTimeout(state.timeout)
          state.timeout = setTimeout(check, 50)
        }
        CodeMirror.on(window, "mouseup", state.hurry)
        CodeMirror.on(window, "keyup", state.hurry)
      }
    
      function stopListening(_cm, state) {
        clearTimeout(state.timeout)
        CodeMirror.off(window, "mouseup", state.hurry)
        CodeMirror.off(window, "keyup", state.hurry)
      }
    });
    
  2. Add force option
    import { UnControlled as CodeMirror } from 'react-codemirror2';
    require('path/to/autorefresh.ext');
    
    (snip)
    
    render() {
      return <CodeMirror
        options={{
          autoRefresh: {force: true}
        }}
      />
    }
    

yuki-takei avatar Jun 20 '18 17:06 yuki-takei

This is my code. It is worked.

import React from 'react';
import PropTypes from 'prop-types';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/keymap/sublime';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/search/match-highlighter';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/python/python';
import 'codemirror/addon/display/placeholder';
import 'codemirror/lib/codemirror.css';
import 'styles/mixins/codemirror.less';

export default class CM extends React.PureComponent {
  render() {
    return (
      <CodeMirror
        {...this.props}
        editorDidMount={(editor) => {
          editor.refresh();
        }}
        options={Object.assign(
          {
            mode: 'python',
            lineNumbers: true,
            highlightSelectionMatches: true,
            indentUnit: 4,
            tabSize: 4,
            lineWrapping: true,
            dragDrop: false,
            keyMap: 'sublime',
            matchBrackets: true,
            autoCloseBrackets: true,
          },
          this.props.options || {}
        )}
      />
    );
  }
}
CM.propTypes = {
  options: PropTypes.object.isRequired,
};

divefox avatar Jun 27 '18 05:06 divefox

has there been any progress on this? Looking over the issue I see lots of code without a clear solution. Does the answer lie within a change to the repo or is this handled such as the various examples throughout this thread? Maybe identifying the best approach here and documenting in the readme? PR's always welcome

scniro avatar Jul 01 '18 18:07 scniro

(I am a new user of react-codemirror2 as of yesterday. Thanks for this great package!)

I just encountered this issue. CodeMirror's autorefresh addon did not work for me. @yuki-takei's autorefresh.ext did work for me. Perhaps the elegant solution is to request a fix to CodeMirror's autorefresh similar to autorefresh.ext so that it works in the case of a React component?

philipmjohnson avatar Jul 07 '18 22:07 philipmjohnson

I have posted a PR. Let’s keep our fingers crossed :innocent:

yuki-takei avatar Jul 08 '18 22:07 yuki-takei

I also encountered this problem. The direct cause of this problem can be referred to the following code.

import React, { Component } from 'react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/mode/python/python';
import 'codemirror/lib/codemirror.css';

class CmTest extends Component {
    state = {
        text : '#!/usr/bin/env python\n',
        showCm: false,
        showBlock: false,
    };
    changeCm = () => {
        this.setState({ showCm: true})
    };
    changeBlock = () => {
        this.setState({ showBlock: true})
    };
    render() {
        const { text, showCm, showBlock } = this.state;
        // solution 1:
        // if(this.instance) {
        //     setTimeout(()=> this.instance.refresh(), 200);
        // }
        return  (<div>
            react-codemirror2 test
            <div><button onClick={this.changeCm}>one</button></div>
            <div><button onClick={this.changeBlock}>two</button></div>
            <div style={ !showBlock ? {display: 'none'}: {}}>
                this is block
                {
                    // solution 2:
                    // showBlock && showCm && <CodeMirror
                     showCm && <CodeMirror
                        value={text}
                        options={{
                            mode: 'python',
                            lineNumbers: true,
                            autoRefresh:true
                        }}
                        editorDidMount={editor => {
                            this.instance = editor;
                        }}
                        onBeforeChange={(editor, data, value) => {
                            this.setState({ text: value });
                        }}
                    />
                }
            </div>
        </div>);
    }
}

export default CmTest;

If you click button 1 and then click button 2, the bug is reproduced.

There are two solutions here. One solution can add the following code to the render function.

if(this.instance) {
    setTimeout(()=> this.instance.refresh(), 200);
}

Another solution needs to control CodeMirror not to initialize too early.

SagaciousHugo avatar Dec 04 '18 09:12 SagaciousHugo

This is my code. It is worked.

import React from 'react';
import PropTypes from 'prop-types';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/keymap/sublime';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/search/match-highlighter';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/python/python';
import 'codemirror/addon/display/placeholder';
import 'codemirror/lib/codemirror.css';
import 'styles/mixins/codemirror.less';

export default class CM extends React.PureComponent {
  render() {
    return (
      <CodeMirror
        {...this.props}
        editorDidMount={(editor) => {
          editor.refresh();
        }}
        options={Object.assign(
          {
            mode: 'python',
            lineNumbers: true,
            highlightSelectionMatches: true,
            indentUnit: 4,
            tabSize: 4,
            lineWrapping: true,
            dragDrop: false,
            keyMap: 'sublime',
            matchBrackets: true,
            autoCloseBrackets: true,
          },
          this.props.options || {}
        )}
      />
    );
  }
}
CM.propTypes = {
  options: PropTypes.object.isRequired,
};

this worked for me

rmelian avatar Aug 12 '19 21:08 rmelian

I tried to do a very minimal example of how I'm working around this. The core of the matter is calling the refresh exactly once. This is done by abusing React state.

import React, { useState } from "react";
import { UnControlled as CodeEditor } from "react-codemirror2";

const WorkingExample = () => {
  const [editor, setEditor] = useState();

  if (editor) {
    editor.refresh();
    setEditor(undefined);
  }

  return (
    <CodeEditor
      editorDidMount={ed => {
        setEditor(ed);
      }}
    />
  );
};

export default WorkingExample;

I removed the irrelevant props here. Here's the logic behind this solution:

  1. We define a hook for some state called editor, with a corresponding setEditor. It's initialized to undefined
  2. If editor is a truthy value, we call editor.refresh, in this case editor is undefined, so we ignore this block.
  3. Inside our editorDidMount we call setEditor with the value of the editor coming from that function, that will trigger a re-render, we go back to the top of our render function
  4. We get to the if (editor) block again, this time around it does have a truthy value (An object), we call editor.refresh() inside here, and then call setEditor to set this to undefined again, we don't want to call editor.refresh() every time we re-render.
  5. Our editor was refreshed, and should display properly, since editorDidMount is only called once we don't re-set editor there, and life continues as usual.

Your console output, if you were try to log out the value of editor should be something like:

undefined
CodeMirror {options: {…}, doc: Doc, display: Display, state: {…}, curOp: null, …}
undefined
...

A fuller example would be something like:

import React, { useState } from "react";
import { UnControlled as CodeEditor } from "react-codemirror2";
import "codemirror/mode/htmlmixed/htmlmixed";

const WorkingExample = ({ preview }) => {
  const [editor, setEditor] = useState();

  if (editor) {
    editor.refresh();
    setEditor(undefined);
  }

  return (
    <CodeEditor
      value={preview}
      options={{
        lineWrapping: true,
        lineNumbers: true,
        mode: "htmlmixed",
        readOnly: true,
      }}
      editorDidMount={ed => {
        setEditor(ed);
      }}
    />
  );
};

export default WorkingExample;

Cheers!

Zyst avatar Jan 06 '20 18:01 Zyst

@dyzajash @Zyst @rmelian @yuki-takei @jo8937 @divefox @cristiano-belloni I am a lot shorter on time these days as when I started this project. Codemirror & React APIs are moving to quickly for me to keep atop of for the day-to-day. I am looking for a co-maintainer of this project. Please contact me directly if you are interested. Thank you for understanding.

scniro avatar Jan 19 '20 16:01 scniro

I use react-codemirror2 and wanted to report an issue. After seeing lot of labels "help wanted" I backed off. Just like react-codemirror2 solves a problem in web development, I'm working on a tool to solve problems created by web frameworks

React APIs are moving to quickly for me to keep atop of for the day-to-day

@scniro @dyzajash @Zyst @rmelian @yuki-takei @jo8937 @divefox @cristiano-belloni I'm looking for feature requests for my project https://github.com/imvetri/ui-editor. It solves the problem I wanted to solve but I'm not sure how to re-design it so that developers can start using it.

Thanks for your time, -Vetrivel.

imvetri avatar Apr 05 '20 04:04 imvetri

For me, the cause of this issue is that the CodeMirror component is mounted before the parent is added to the DOM, i.e. it is mounted on a detached DOM node. This is because I'm inside a React portal. Delaying a refresh by 100ms, for example, worked, but to have something more reliable I'm conditionally rendering CodeMirror to make sure it's mounted on an already attached DOM node. For example, in a Portal, you can use state as suggested in the official docs:

class Modal extends React.Component {

  constructor(props) {
    super(props);
    this.state = { mounted: false };
    this.portal = document.createElement('div');
  }

  componentDidMount() {
    document.body.appendChild(this.portal);
    this.setState({ mounted: true }); // Now it's safe to mount CodeMirror
  }

  componentWillUnmount() {
    document.body.removeChild(this.portal);
  }

  render() {
    return ReactDOM.createPortal((
      <div>
        {this.state.mounted &&
          <CodeMirror />
        }
      </div>
    ), this.portal);
  }

}

lorenzos avatar Nov 30 '20 16:11 lorenzos

Below solution works for me

Steps:

  1. Create a new file called autorefresh.ext.js
  2. Call that file in Your component
  3. use autoRefresh: { force: true } in options of CodeMirror
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function (mod) {
  mod(require('codemirror'));
}((CodeMirror) => {


  CodeMirror.defineOption('autoRefresh', false, (cm, val) => {
    if (cm.state.autoRefresh) {
      stopListening(cm, cm.state.autoRefresh);
      cm.state.autoRefresh = null;
    }
    if (val && (val.force || cm.display.wrapper.offsetHeight == 0)) { startListening(cm, cm.state.autoRefresh = { delay: val.delay || 250 }); }
  });

  function startListening(cm, state) {
    function check() {
      if (cm.display.wrapper.offsetHeight) {
        stopListening(cm, state);
        if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight) { cm.refresh(); }
      } else {
        state.timeout = setTimeout(check, state.delay);
      }
    }
    state.timeout = setTimeout(check, state.delay);
    state.hurry = function () {
      clearTimeout(state.timeout);
      state.timeout = setTimeout(check, 50);
    };
    CodeMirror.on(window, 'mouseup', state.hurry);
    CodeMirror.on(window, 'keyup', state.hurry);
  }

  function stopListening(_cm, state) {
    clearTimeout(state.timeout);
    CodeMirror.off(window, 'mouseup', state.hurry);
    CodeMirror.off(window, 'keyup', state.hurry);
  }
}));

ghost avatar Mar 22 '21 10:03 ghost

Hi, I'm not sure if my issue with not refreshing of the text in CM is the same as yours, but I managed to solve it with simple shake-it-baby style re-render. Here's my code:

CodeMirrorEditor:

export default ({ value, onChange }) => {
  if( !value ) return null // this line helps to re-render!!
  const [ val, setVal ] = useState( value || '' )
  useEffect( () => onChange( val ), [ val ] )
  const setValue = ( _, __, v ) => setVal( v )
  return <CodeMirror value={val} options={opts}  onChange={setValue} onBeforeChange={setValue} />
}

The usage:

render() {
  const { text } = this.state
  return <div>
     <button onClick={this.changeText( 'AAAAAAAAA' )}>click</button>
     <CodeMirrorEditor value={text} onChange={console.info}/>
  </div>
}

changeText = text => _ => this.setState( { text:null }, _ => this.setState( { text } ) )

so, I set the text in the state to null 1st, and then to the desired value.

Works like a charm!

injecteer avatar Jul 08 '21 22:07 injecteer