Render HTML string in an isomorphic React application

As an input, there is a non-SPA script with a sanitized but random HTML string:

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

The string is taken from the WYSIWYG editor and contains nested regular HTML tags and a limited number of custom elements (components) that should be displayed in widgets.

Currently, HTML fragments like this should be displayed on the server side (Express) separately, but ultimately will be displayed on the client side also as part of an isomorphic application.

I intend to use React (or React-like framework) to implement the components, because it seems to be suitable for the case - it is isomorphic and displays partial well.

The problem is that substrings are kind of

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

should become

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

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

How can this case be resolved in React?

+5
source share
2 answers

Sanitary HTML can be turned into React Components, which can be run both on the server and on the client, by analyzing the html string and converting 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 ) ); 

Please note that you do not need, in your opinion, not JSX / TSX, but the React Nodes tree for the React renderer (ReactDOM in this case). JSX is just syntactic sugar, and converting it back and forth is not required unless you want to support React output in your code base.

Forgive the html simplified syntax markup. Its for illustrative purposes only. You might want to use a specification-compatible library to parse the input html or whatever suits your use case.

Make sure that the client-side package receives the same App component, otherwise you can respond to the client-side script to recreate the DOM tree and you will lose all the benefits of server-side rendering.

You can also take advantage of React 16 thread with the above approach.

Solving the problem of props

Details will be available to you from the tree as attributes and can be transferred as details (upon careful consideration of your use case).

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

Be careful with custom attributes, although you might want to follow this rfc . Stick with camelCase if possible.

+5
source

You can use the Babel API to convert a string to executable JavaScript.

You can make your life easier if you reject the agreement on the standard <lovercase> component, because in JSX they are treated as DOM tags, so if you can use your <Gallery> users instead of <Gallery> , you will save a lot amount of trouble.

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

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() 
+2
source

Source: https://habr.com/ru/post/1270805/


All Articles