scniro / react-codemirror2

Codemirror integrated components for React
MIT License
1.65k stars 192 forks source link

CodeMirror2 refresh #83

Open ghost opened 6 years ago

ghost commented 6 years ago

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;
cristiano-belloni commented 6 years ago

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

ghost commented 6 years ago

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.

scniro commented 6 years ago

@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?

ghost commented 6 years ago

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.

divefox commented 6 years ago

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

scniro commented 6 years ago

@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!

divefox commented 6 years ago

@scniro Thanks !

divefox commented 6 years ago

@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

scniro commented 6 years ago

@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

divefox commented 6 years ago

@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.

ghost commented 6 years ago

I'll test this today and update comment :)

scniro commented 6 years ago

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

ghost commented 6 years ago

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.

jo8937 commented 6 years ago

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.

yuki-takei commented 6 years ago

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 <yuki@weseek.co.jp>
     * @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}
        }}
      />
    }
divefox commented 6 years ago

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,
};
scniro commented 6 years ago

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

philipmjohnson commented 6 years ago

(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?

yuki-takei commented 6 years ago

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

SagaciousHugo commented 5 years ago

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.

rmelian commented 4 years ago

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

Zyst commented 4 years ago

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!

scniro commented 4 years ago

@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.

imvetri commented 4 years ago

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.

lorenzos commented 3 years ago

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);
  }

}
ghost commented 3 years ago

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);
  }
}));
injecteer commented 3 years ago

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!