Mad Max
Mad Max

Reputation: 283

Replace img tags with Gallery in React

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

Answers (2)

dfsq
dfsq

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

Tr1et
Tr1et

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

Related Questions