Reputation: 283
There is problem with render gallery component: I get string with html from server
let serverResponse = `
<h3>Some title</h3>
<p>Some text</p>
<p>
<img src="">
<img src="">
<img src="">
<br>
</p>
...
`
Now I render this response with dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: serverResponse }} />
But when I got 2 or more repeating <img>
tags I want to replace them with component.
How can I do that? I tried to do it with Regex
and replace them with <Gallery/>
but it doesn't work. I think that I need split string in array of tags and then replace images with <Gallery/>
component.
I tried do it with renderToString
...
getGallery = images => {
// Loop throw images and get sources
let sources = [];
if (images) {
images.map(img => {
let separatedImages = img.match(/<img (.*?)>/g);
separatedImages.map(item => sources.push(...item.match(/(https?:\/\/.*\.(?:png|jpg))/)));
});
}
if (sources.length) {
return <Gallery items={sources}>
}
return <div/>
};
...
<div dangerouslySetInnerHTML={{__html: serverResponse.replace(/(<img (.*?)>){2,}/g,
renderToString(this.getGallery(serverResponse.match(/(<img (.*?)>){2,}/g))))}}/>}
And this doesn't work, because I get just html without logic :(
Upvotes: 3
Views: 1505
Reputation: 193311
Fist of all, dangerouslySetInnerHTML
is not the way to go, you can't insert gallery into in it and have it processed by React. What you need to do is multistep procedure.
1. Parse HTML into document. In this stage you will convert string to valid DOM document. This is very easy to do with DOMParser:
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
I make this helper function to return container with your HTML nodes. It will be need in the next step.
2. Transform DOM document into React JSX tree. Now that you have DOM tree it's very easy to convert it to JSX by creating individual React elements out of corresponding DOM nodes. This function needs to be recursive to process all levels of the DOM tree. Something like this will do:
function getJSX(root) {
return [...root.children].map(element => {
const children = element.children.length ? getJSX(element) : element.textContent
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
This is enough to create JSX out of DOM. It could be used like this:
const JSX = getJSX(getDOM(htmlString))
3. Inject Gallery. Now you can improve JSX creation to inject Gallery into created JSX if element
contains more then 1 image tag. I would pass inject function into getJSX
as the second parameter. The only difference from above version would be is how children
is calculated in gallery case:
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
4. Create Gallery component. Now it's time to create Gallery component itself. This component will look like this:
import React from 'react'
import { func, string } from 'prop-types'
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
function getJSX(root, injectGallery) {
return [...root.children].map(element => {
let children
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
const HTMLContent = ({ content, injectGallery }) => getJSX(getDOM(content), injectGallery)
HTMLContent.propTypes = {
content: string.isRequired,
injectGallery: func.isRequired,
}
export default HTMLContent
5. Use it! Here is how you would use all together:
<HTMLContent
content={serverResponse}
injectGallery={(images) => (
<Gallery images={images} />
)}
/>
Here is the demo of this code above.
Demo: https://codesandbox.io/s/2w436j98n
Upvotes: 3
Reputation: 895
TLDR: You can use React HTML Parser or similar libraries.
While it looks very alike, JSX get parsed into a bunch of React.createElement
so interpolate React component into HTML string will not work. renderToString
won't do too because it is used to server-side render React page and will not work in your case.
To replace HTML tags with React component, you need a parser to parse the HTMl string to nodes, map the nodes to React elements and render them. Lucky for you, there are some libraries out there that do just that, like React HTML Parser for example.
Upvotes: 0