简体   繁体   中英

How to parse a String containing HTML to React Components

I'm using this library: html-to-react as I found that it was possible to "compile" HTML as a String to React Components.

We're doing a system based on widgets and they might change over time (ie add / remove / update / etc), so we're planning to send the widget HTML from the database to the front which is done using React.

I've successfully done a simple widget using HTML, now I'm trying this widget to respond to click events, so I thought on adding onclick=method() method from HTML or onClick={method} from React. However I've not been able to get the desired output as I'm getting these errors in my browser's console.

Warning: Invalid event handler property `onclick`. React events use the camelCase naming convention, for example `onClick`.
    in label
    in span
    in div
Warning: Invalid event handler property `onclick`. Did you mean `onClick`?
    in label
    in span
    in div
    in DireccionComponent (at App.js:51)
    in div (at App.js:50)
    in GridItem (created by ReactGridLayout)
    in div (created by ReactGridLayout)
    in ReactGridLayout (at App.js:49)
    in App (at index.js:11)

The code I'm using is the following:

App.js

import React, { Component } from 'react';
import './App.css';
import DireccionComponent from './DireccionComponent.js';

class App extends Component {
    render() {
        return (
            <DireccionComponent />
        );
    }
}

export default App;

DireccionComponent.js

import React, {Component} from 'react';

var ReactDOMServer = require('react-dom/server');
var HtmlToReactParser = require('html-to-react').Parser;

let htmlInput = ''
            +   '<div>'
            +       '<center><label className="title">Widget</label></center>'
            +       '<span className="ui-float-label">'
            +           '<label htmlFor="float-input" onClick={this.myFunction}>Hello</label>'
            +       '</span>'
            +   '</div>';

var htmlToReactParser = new HtmlToReactParser();
var reactElement = htmlToReactParser.parse(htmlInput);
var reactHtml = ReactDOMServer.renderToStaticMarkup(reactElement);

class DireccionComponent extends Component {
    constructor(props) {
        super (props);

        this.myFunction = this.myFunction.bind(this);
    }

    render() {
        return (
            reactElement
        )
    }

    myFunction() {
        console.log('Hola');
        alert("Hola");
    }
}

export default DireccionComponent;

package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "html-to-react": "^1.3.3",
    "primereact": "^1.5.1",
    "react": "^16.3.2",
    "react-dom": "^16.3.2",
    "react-dom-server": "0.0.5",
    "react-grid-layout": "^0.16.6",
    "react-scripts": "1.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

Is it possible to get an alert or something similar using the above library to parse HTML to React Components? If so, how could I do it? Or what am I doing wrong?


Edit

It doesn't have to be specifically the library I'm using, if there's another one you've worked with and done something similar, I'm open to receive this recommendations (Note, I'm not asking for a software or library, but how to call the onClick method inside the String containing my HTML or a workaround)


Edit 2

After trying something I found that removing this line:

var reactHtml = ReactDOMServer.renderToStaticMarkup(reactElement);

Gets rid of one of the messages. The library uses that element to test if both the current HTML and the parsed one are the same. I didn't needed it for my case but that still leaves the current error in the console:

 Warning: Invalid event handler property `onclick`. Did you mean `onClick`?
    in label
    in span
    in div
    in DireccionComponent (at App.js:47)
    in App (at index.js:11)

Edit 3

After some investigation I found dangerousSetInnerHTML in this answer

Then I tried to follow this answer an placing an alert inside my HTML as follows:

'<label htmlFor="float-input" onclick="alert(\'Hello\')">Hello2</label>'

And it triggered the alert , however if I declared myFunction like in the code below (notice I tried to declare it as a function outside the class, inside it and also as var myFunction = function() { ... } all together and separated):

import React, {Component} from 'react';

var ReactDOMServer = require('react-dom/server');
var HtmlToReactParser = require('html-to-react').Parser;

let htmlInput = ''
            //+ '<div>'
            +       '<center><label className="title">Widget</label></center>'
            +       '<span className="ui-float-label">'
            +           '<label htmlFor="float-input" onclick="myFunction()">Hello2</label>'
            +       '</span>'
            //+     '</div>';

var htmlToReactParser = new HtmlToReactParser();
var reactElement = htmlToReactParser.parse(htmlInput);

class DireccionComponent extends Component {
    constructor(props) {
        super (props);

        this.myFunction = this.myFunction.bind(this);
    }

    render() {
        return (
            <div dangerouslySetInnerHTML={{__html: htmlInput}}>

            </div>
        )
    }

    myFunction() {
        console.log('Hola');
        alert("Hola");
    }
}

function myFunction() {
    console.log('Hola');
    alert("Hola");
}

export default DireccionComponent;

But this time I get a new error:

ReferenceError: myFunction is not defined[Learn More]

I found a similar question: Handling onClick of a <a> tag inside of dangerouslySetInnerHTML/regular html with this problem, and tried to investigate a little more and found this comment which says that the events from the dangerouslySetInnerHTML won't be triggered this way and links to the docs .

So, while this seemed to be a workaround when I got an alert when written directly from the onclick method, or probably I didn't do it in the correct way, so if someone with more experience could try it and clarify, would be great.


Edit 4

I found a way to show an alert in this way:

  • Write the HTML as a String
  • Set the HTML through dangerouslySetInnerHTML
  • On componentDidMound() function from React use pure Javascript to add onclick function to it.
  • Success

However, what we're trying to do is :

  • Create some methods, for example: addTwoNumbers or something like that
  • Send an onclick or onClick from within the HTML String calling addTwoNumbers function.
  • Repeat everytime we need to add a new component that needs to use the addTwoNumbers method and just have to write the HTML for it, save it on the database and retrieve it and then just use it.

Something like:

...
    <label onClick={myFunction}> My Label </label>
...

Or as I said above:

...
    <label onClick={addTwoNumbers}> My Label </label>
...

The code that allows me to get an alert is the following:

import React, {Component} from 'react';
import Parser from 'html-react-parser';
import {render} from 'react-dom';

var ReactDOMServer = require('react-dom/server');
var HtmlToReactParser = require('html-to-react').Parser;

let htmlInput = ''
            //+ '<div>'
            +       '<center><label className="title">Widget</label></center>'
            +       '<span className="ui-float-label">'
            +           '<label htmlFor="float-input" id="myLabel">Hello2</label>'
            +       '</span>'
            //+     '</div>';

var htmlToReactParser = new HtmlToReactParser();
var reactElement = htmlToReactParser.parse(htmlInput);

class DireccionComponent extends Component {
    constructor(props) {
        super (props);

        this.myFunction = this.myFunction.bind(this);
    }

    render() {
        return (
            <div dangerouslySetInnerHTML={{__html: htmlInput}}>

            </div>
        )
    }

    componentDidMount() {
        console.log('DONE');

        let myLabel = document.getElementById("myLabel");

        myLabel.onclick = function() {
            alert();
            console.log('clicked');
        }
        console.log(myLabel);
    }

    myFunction() {
        console.log('Hola');
        alert("Hola");
    }
}

function myFunction() {
    console.log('Hola');
    alert("Hola");
}

export default DireccionComponent;

With this in mind, do you know any other way to do what I'm trying to do?

The solution is a bit hacky , copy paste in codesandbox

class App extends Component {
  constructor(props) {
    super(props);
    this.myfunc = this.myfunc.bind(this);
  }
  componentDidMount() {
    window.myfunc = this.myfunc;
  }
  myfunc() {
    console.log("from myfunc");
  }
  componentWillUnmount() {
    window.myfunc = null;
  }
  render() {
    let inputhtml = "<a onclick=window.myfunc()>HelloWorld</a>";
    return (
      <div style={styles}>
        <div dangerouslySetInnerHTML={{ __html: inputhtml }} />
      </div>
    );
  }
}

I don't know if it would work for your particular case, but an idea is not to parse HTML at all. You can tell webpack to generate separated bundles for different sets of files. Now, I've never done this, but I've read several times that you can; for example, this .

Then you can have a main.bundle.js that has all the parts of your site that do not change. Also, create a widget1.bundle.js and so on for every widget that can change. Import all those on your index.html. Then, configure your webserver to never cache the smaller bundles (widget1, etc). Whenever the user enters the website, it will download those widgets again, effectively updating them. If you want to be extra fancy, you can provide an API with the current version of each widget, and put an extra field on the widget bundle with the version, so that you can periodically check if the components are up to date, in the event that the user haven't reloaded the page in a while. If you discover an outdated component, just force reload, and the browser will make sure to fetch the latest version.

I know this is totally different path from the one you are going, but if it's good for your particular case, I believe it will be simpler, easier to implement and guarantee stability, more maintainable and overall a better solution than hand parsing the html and handling all the requests yourself.

Why not use the browser DOMParser API to parse your HTML into an off-screen/detached DOM, then traverse that DOM and use React.createElement() to build up React's DOM? Something like

let str = "... your HTML ..."

function traverse(node) {
  let children = []
  for (let i = 0; i < node.children.length; i++)
    children.push(traverse(node.children.item(i)))
  return React.createElement(node.localName, null, children)
  // or maybe use the following instead (React's API doc
  // isn't very clear about this):
  // return React.createElement(node.localName, null, ...children)
}

let reactElementForStr =
  traverse(new DOMParser().parseFromString(str, 'text/html'))

Mind you, I have only a passing knowledge of React's inner workings, and haven't tested this at all.

Easiest way of doing this would be Dangerously Set innerHTML

function createMarkup() {
  return {__html: 'First &middot; Second'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;

But this comes with a price, Embedding HTML string into the template dynamically causes a security violation thats why it has that weird syntax __html and a scary name dangerouslySetInnerHTML. If you are using this make sure you sanitize your input params to prevent any XSS attracts. Alternatively you can use an iframe

Check the complete implementation from below pen (Recommended) https://codepen.io/varmakarthik12/pen/zaOzPw

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM