6

React newbie here. I have a contenteditable div which has dangerouslySetInnerHTML as the child, since I need to format whatever the user enters,at runtime. On a particular span click inside the HTML, I want to setState one of the variables of the containing component.

Can this be done?

If not, how should I change my structure ?

Here's the code:

updateText:function(){

    var txt = $('#text_Box').text();

    if(txt.indexOf('@Name') > -1)
    {
        txt = txt.replace('@Name','<span class=\'tagged\' contenteditable = \'false\' onclick=\'doSomething()\'>:Name</span>');

    }
    this.setState({userText:txt});
}, 
render:function(){
  return <div className="inputDiv" contentEditable="true" type="text" className="form-control" id="text_Box" dangerouslySetInnerHTML={{__html:this.state.userText}} onInput = {this.updateText} />

}

the doSomething() method is what I'm taking about.

user4773422
  • 75
  • 1
  • 2
  • 6

3 Answers3

9

If you want your spans to respond to click events, you should assign event handler(doSomething) only after your component is rerendered, because when you assing new value to innerHtml, all event handlers within this component are cleaned. Another solution is using event delegation like this:

onClick: function(e) {
    var $el = $(e.target);
    if ($el.is('span.tagged')) {
        this.doSomething($el);
    }
},

render:function(){
    return (
        <div 
            className="inputDiv form-control" 
            contentEditable="true"
            onClick={this.onClick}
            type="text" 
            id="text_Box" 
            dangerouslySetInnerHTML={{__html: this.state.userText}} 
            onInput={this.updateText} />
    );
}

Another possible solution is to work with DOM tree directly using createElement, createTextNode and appendChild methods.

beshanoe
  • 222
  • 2
  • 8
  • the dangerous HTML that is going to be displayed is dynamic and might change depending on options selected by the user previously. In such a case, there might not be any in it at all, but the same function will be called on click of whatever DOM is present – user4773422 May 29 '15 at 10:57
  • Yes, in case of having no span elements there will be event handler on div, but I don't understand how the marcel's answer helps solving the problem, because it just invokes this.doSomething method inside updateText method, and span's onclick field receives the result of doSomething invocation. Anyway you can't assing span's click handler which will have direct access to this.doSomething this way – beshanoe May 29 '15 at 11:37
  • I see your point. Maybe I need to redo this entire thing. Thanks for pointing this out ! – user4773422 May 29 '15 at 14:57
  • 1
    @beshanoe - your answer is the correct one. As you point out correctly in the accepted solution (and I don't get why it is accepted rather than yours) this.doSomething is going to get executed immediately and its result will be concatenated with the rest of the string. There is no way that approach will allow you to install a handler to be invoked in onClick... – Moonwalker Aug 11 '17 at 13:23
3

Try this:

updateText: function() {
    var txt = $('#text_Box').text();

    if (txt.indexOf('@Name') > -1) {
        txt = txt.replace('@Name', '<span class="tagged" contenteditable="false" onclick="' + this.doSomething() + '">:Name</span>');
    }
    this.setState({userText: txt});
}, 

render:function(){
    return (
        <div 
            className="inputDiv form-control" 
            contentEditable="true" 
            type="text" 
            id="text_Box" 
            dangerouslySetInnerHTML={{__html: this.state.userText}} 
            onInput={this.updateText} />
    );
}
marcel
  • 2,967
  • 1
  • 16
  • 25
1

I had a similar requirement recently. The react app was being given a block of html with href attributes that needed to be converted into onClick events so that we could resolve and route the links within the react app.

My solution was to use regex and then register my own event listeners

//given a block of html as a string
myHtml = '<div href="/goSomewhere"></div>'

//match all href attributes
let reg: RegExp = /href=".*?"/g

//replace the href attribute with a custom "resolve" attribute
myHtml.replace(reg, (href: string) => {
  //then match any characters between the quotation marks in the href attribute to extract the uri itself
  let uri = href.match(/(?<=href=").*?(?=")/g)
  return `resolve="${uri}"`
})


//Render the html
render() { 
   return <div dangerouslySetInnerHTML = {{__html: myHtml}} />
}

//helper function to return an array containing all nodes with a "resolve" attribute
getElementByAttribute(attr: string) {
  let nodeList = document.getElementsByTagName('*')
  let nodeArray = []
  for (let i = 0; i < nodeList.length; i++) {
    if (nodeList[i].getAttribute(attr)) nodeArray.push(nodeList[i])
  }
  return nodeArray
}

//once rendered find the tag that require resolving 
componentDidUpdate() {
  let nodes = this.getElementByAttribute('resolve')
  for (let i = 0; i < nodes.length; i++) {
    //create pointer outside of the onclick event allowing closure
    let href = nodes[i].getAttribute('resolve')

    nodes[i].addEventListener('click', (e) => {
      if (href) this.linkResolver(href);
    })

    //remove the custom attribute to cleanup
    nodes[i].removeAttribute('resolve')
  }
}

//linkResolver is a function within react
//you now have an onclick event triggered from dangerouslySetInnerHTML
linkResolver(href: string) {
  console.log(href)
}
user3203348
  • 312
  • 3
  • 7