16

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 (i.e. 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?

Community
  • 1
  • 1
Frakcool
  • 10,915
  • 9
  • 50
  • 89

4 Answers4

7

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>
    );
  }
}
Rahil Ahmad
  • 3,056
  • 1
  • 16
  • 21
  • This executes the function before any click, and not at all on a user-click. – Frakcool May 28 '18 at 13:51
  • Sorry , I couldn't reproduce it (function execution before click and not getting clicked on all clicks) .. Please check the link to codesandbox https://codesandbox.io/s/61qn31lwyr and let me know if this issue is still there. – Rahil Ahmad May 29 '18 at 06:13
  • Could you post the complete code from the sandbox inside your answer? And the link as well? Sometimes they get deleted and it would be worth to keep it. – Frakcool May 29 '18 at 14:17
0

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.

Luan Nico
  • 5,376
  • 2
  • 30
  • 60
-1

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.

imhotap
  • 2,275
  • 1
  • 8
  • 16
-1

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

karthik
  • 1,100
  • 9
  • 21
  • While your example renders an HTML, I'm trying to call a function from the HTML inside the String. If it's possible to do so, could you add it to your pen? – Frakcool May 28 '18 at 13:57
  • Then i'm pretty sure you are using dangerouslySetInnerHTML which wont accept any return types. Did you try checking https://codepen.io/varmakarthik12/pen/zaOzPw where i even have a button to ? – karthik May 28 '18 at 14:07
  • Yes, if you check **Edit 3** in the question, there I already said I found that using it might help. And yes, I saw you have a button there `Test`, but it does nothing on click. That button is the one I want to do something `onclick` – Frakcool May 28 '18 at 14:08
  • can you check it now opening console(refresh it i just added onclick handler). Check like line 70 JS – karthik May 28 '18 at 14:10
  • Yes, it works, but as I said, the functionality is going to be bigger, calling a `function` on its own. Is it possible? I mean something like `onclick='myFunction()'` and `myFunction = function() { console.log('Hello'); }` or something like that. – Frakcool May 28 '18 at 14:12
  • OK let me clear you. you can call a function something like that and it should not be in react context but in a global context once you execute with the string you are out of react scope and in an iframe that why we user HTML event handler onclick rather than reacts onClick. – karthik May 28 '18 at 14:20
  • So how to achive what you are looking for ... easy create a fucntion with global instance (which is not recommended if so make sure its uniques with name so that any internal vaible cant over write it). Simple example will be in yout main html file where you have the div container where you inject REACT and script tag `` – karthik May 28 '18 at 14:20
  • Could you post an example? So that I can reproduce it? We will already have the function in the code when the HTML is injected. This is a widget-based app. So that user can drag & drop some of these widgets, the board is empty at start until user drops any component. Some of these components share functionality so we only need to write them once and just place the HTML and call these functions on-demand. I'm not sure if I'm clear enough of what we're trying to do. – Frakcool May 28 '18 at 14:48
  • now check https://codepen.io/varmakarthik12/pen/zaOzPw?editors=1010 what changed give an id to element like how i did at line 70 and check the method _updateIframe where i call the getImplentation method. This way you can even call a method from within the component should not be a global instance :) – karthik May 28 '18 at 15:01
  • Now we reached **Edit 4** where we still need to call `document.getElementById` and add the `onclick` right on the file. We want to call them without it. Just an `onclick=myFunction()` and that's it. If not possible, well, I'll have to think of another strategy for it. – Frakcool May 28 '18 at 15:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/171928/discussion-between-karthik-and-frakcool). – karthik May 28 '18 at 15:35