5

There is non-SPA scenario with sanitized yet random HTML string as an input:

<p>...</p>
<p>...</p>
<gallery image-ids=""/>
<player video-id="..."/>
<p>...</p>

The string originates from WYSIWYG editor and contains nested regular HTML tags and a limited number of custom elements (components) that should be rendered to widgets.

Currently HTML snippets like this one are supposed to be rendered on server side (Express) separately but will eventually be rendered on client side too as a part of isomorphic application.

I intend use React (or React-like framework) to implement components because it presumably suits the case - it is isomorphic and renders partials well.

The problem is that substrings like

<gallery image-ids="[1, 3]"/>

should become

<Gallery imageIds={[1, 3]}/>

JSX/TSX component at some point, and I'm not sure what is the right way to do this, but I would expect it to be a common task.

How can this case be solved in React?

4
  • It seems to me that you are asking how to turn strings like <gallery image-ids="[1, 3]"/> into a React Component - doesn't sound very reasonable. May I know why? Can you please rephrase your question if need be? Commented Aug 14, 2017 at 8:08
  • The question already contains the information on the context of the problem. The string originates from WYSIWYG editor. Commented Aug 14, 2017 at 8:12
  • And you want to turn this into a React Component? Conventionally it has been the other way around - Components => (react + react-dom) => htmlString. Barring the case you mentioned, how are you doing it right now? What I fail to understand is are you looking for an automated way to turn html generated from the WYSIWYG to React Components? Or are there a list of custom elements that you want to port to React/React-like lib. Commented Aug 14, 2017 at 8:19
  • Yes, I'm looking for a way to turn HTML string into React component that contains a set of child React components (Gallery, etc) and supposed to be immediately rendered on server side. This wouldn't be a problem for template-based framework like AngularJS or Ember, but React is totally different in this regard. I've not implemented this yet because I'm not sure how this is supposed to be achieved. Commented Aug 14, 2017 at 9:28

2 Answers 2

6
+100

Sanitized HTML can be turned into React Components that can be run both on server and client by parsing the html string and transforming the resulting nodes into React elements.

const React = require('react');
const ReactDOMServer = require('react-dom/server');

const str = `<div>divContent<p> para 1</p><p> para 2</p><gallery image-ids="" /><player video-id="" /><p> para 3</p><gallery image-ids="[1, 3]"/></div>`;


var parse = require('xml-parser');

const Gallery = () => React.createElement('div', null, 'Gallery comp');
const Player = () => React.createElement('div', null, 'Player comp');

const componentMap = {
  gallery: Gallery,
  player: Player
};


const traverse = (cur, props) => {
  return React.createElement(
    componentMap[cur.name] || cur.name,
    props,
    cur.children.length === 0 ? cur.content: Array.prototype.map.call(cur.children, (c, i) => traverse(c, { key: i }))
  );
};

const domTree = parse(str).root;
const App = traverse(
   domTree
);

console.log(
  ReactDOMServer.renderToString(
    App
  )
);

Note however, it is not JSX/TSX that you really need, as you mentioned, but a tree of React Nodes for the React renderer (ReactDOM in this case). JSX is just syntactic sugar, and transforming it back and forth is unnecessary unless you want to maintain the React output in your codebase.

Pardon the over simplified html parsing. Its only for illustrative purposes. You might want to use a more spec-compliant library to parse the input html or something that fits your use case.

Make sure, the client side bundle get the exact same App component, or else you might React's client side script would re-create the DOM tree and you'll lose all the benefits of server side rendering.

You can take advantage of the React 16's streaming out too with the above approach.

Addressing the props problem

Props will be available to you from the tree as attributes and can be passed as props (on careful consideration of your use case ofcourse).

const React = require('react');
const ReactDOMServer = require('react-dom/server');

const str = `<div>divContent<p> para 1</p><p> para 2</p><gallery image-ids="" /><player video-id="" /><p> para 3</p><gallery image-ids="[1, 3]"/></div>`;


var parse = require('xml-parser');

const Gallery = props => React.createElement('div', null, `Gallery comp: Props ${JSON.stringify(props)}`);
const Player = () => React.createElement('div', null, 'Player comp');

const componentMap = {
  gallery: Gallery,
  player: Player
};

const attrsToProps = attributes => {
  return Object.keys(attributes).reduce((acc, k) => {

    let val;
    try {
      val = JSON.parse(attributes[k])
    } catch(e) {
      val = null;
    }

    return Object.assign(
      {},
      acc,
      { [ k.replace(/\-/g, '') ]: val }
    );
  }, {});
};


const traverse = (cur, props) => {

  const propsFromAttrs = attrsToProps(cur.attributes);
  const childrenNodes = Array.prototype.map.call(cur.children, (c, i) => {

    return traverse(
      c,
      Object.assign(
        {},
        {
          key: i
        }
      )
    );
  });

  return React.createElement(
    componentMap[cur.name] || cur.name,
      Object.assign(
        {},
        props,
        propsFromAttrs
      ),
    cur.children.length === 0 ? cur.content: childrenNodes
  );
};

const domTree = parse(str).root;
const App = traverse(
  domTree
);

console.log(
  ReactDOMServer.renderToString(
    App
  )
);

Careful with custom attributes though - you might want to follow this rfc. Stick with camelCase if possible.

Sign up to request clarification or add additional context in comments.

3 Comments

I like this answer better than my own. :)
Thank you :) Updating my answer with props
Thanks, that's the thing I was looking for. It's great that the restriction on the set of allowed components results in easier traversing.
2

You can use Babel's API to transform the string into executable JavaScript.

You can make your life way easier if you ditch the <lovercase> custom component convention, because in JSX they are treated like DOM tags, so if you can make your users use <Gallery> instead of <gallery> you will save yourself from a lot of trouble.

I created a working (but ugly) CodeSandbox for you. The idea is to use Babel to compile the JSX to code, then evaluate that code. Be careful though, if users can edit this, they can surely inject malicious code!

The JS code:

import React from 'react'
import * as Babel from 'babel-standalone'
import { render } from 'react-dom'

console.clear()

const state = {
  code: `
  Hey!
  <Gallery hello="world" />
  Awesome!
`
}


const changeCode = (e) => {
  state.code = e.target.value
  compileCode()
  renderApp()
}

const compileCode = () => {
  const template = `
function _render (React, Gallery) {
  return (
    <div>
    ${state.code}
    </div>
  )
}
`
  state.error = ''
  try {
    const t = Babel.transform(template, {
      presets: ['react']
    })

    state.compiled = new Function(`return (${t.code}).apply(null, arguments);`)(React, Gallery)  
  } catch (err) {
    state.error = err.message
  }
}

const Gallery = ({ hello }) =>
  <div>Here be a gallery: {hello}</div>

const App = () => (
  <div>
    <textarea style={{ width: '100%', display: 'block' }} onChange={changeCode} rows={10} value={state.code}></textarea>
    <div style={{ backgroundColor: '#e0e9ef', padding: 10 }}>
    {state.error ? state.error : state.compiled}
    </div>
  </div>
)


const renderApp = () =>
  render(<App />, document.getElementById('root'));

compileCode()
renderApp()

8 Comments

Yes, this requires to use eval, hence it's not practical for user input in its current form. Can you explain what exactly you mean by 'way easier'? <Gallery> will be a string any way, won't it?
You would have to check the AST for function calls or something as a way to mitigate, there's really no other way. About <Gallery>, no, the JSX is transformed to React.createElement(Gallery, ...) whereas <gallery> is transformed to React.createElement('gallery', ...).
I don't see how can I make my life way easier then. Compiled HTML is a string that was retrieved from client side and stored in DB. It cannot be anything but a string. Hence the question.
Sure but that string can contain malicious JS. About "way easier", I mean React won't understand 'gallery' because it's not a native DOM element, wereas Gallery would be treated as a custom component that needs to be in scope (hence why its passed to the function). If you wanted to use gallery, you would have to replace every occurence of it with Gallery.
If it will always be pure HTML, you can use a HTML parser to transform the HTML to actual JSX, which can then be compiled using the method in this answer. That lets you transform the attribute and tag names as you see fit.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.