Search code examples
reactjstypescript

Wrap SVGElement in ReactElement on click


I am creating an app where users can edit SVGs. I have a library which is responsible for computing UI position for a very specific scientific diagram, which interacts with a React frontend. This library (which uses SVG.js) draws SVGElement's directly to the DOM and uses no React.

I want the user to be able to select an SVGElement and have it become resizable and draggable (I'm using React-rnd). The way I'm trying to do that is to:

  1. Find the SVGElement onClick
  2. Send the SVGElement to my own React Component which then inserts it as it's content
  3. Ideally, this new React FC is used to completely replace the SVGElement in the DOM, so it doesn't appear that there are two SVGs. (The alternative is to send a request to my library to simply delete the SVG to be moved)
interface IResizer { element: SVGElement }


const Resizer: React.FC<IResizer> = (props) => {
    return (
        <>
            <Rnd ...>
                <div className="content" dangerouslySetInnerHTML={{__html: props.element.outerHTML}}></div>
            </Rnd>
        </>
    )
}
const Canvas: React.FC<ICanvasProps> = (props) => {
    const [selectedElements, setSelectedElements] = useState<SVGElement[]>([]);

    
    function canvasClicked(click: React.MouseEvent<HTMLDivElement>) {
        // Get the SVGElement from the DOM.

        setSelectedElements([...selectedElements, node])

       // Delete the SVG from the DOM, or create the "Resizer" here and use it to replace the SVGElement?
    }


    return (
        <>

            <div id={DESTINATIONVCANVASID} onClick={(e) => canvasClicked(e)}>
                
            </div>

            {
                selectedElements.map((e) => {
                    return (
                        <Resizer element={e}>
            
                        </Resizer>
                    )
                })
            }
        </>
    )
}
  1. I don't want to have to use "dangerouslySetInnerHTML", even if it is apparently safe with XSS detection. I have tried to convert the SVGElement into a ReactElement so I can pass it to "Resizer" as a children prop, but it seems unreasonably difficult, any way to do this simply?
  2. Once I have my React FC with the correct SVG inside it, is it possible to replace a standard DOM element with it and expect it to work?
  3. Am I doing things completely wrong? Is it unreasonable to pull a standard element out of the DOM and expect to be able "Reactify" it?

Solution

  • One option is you can pass around the SVG elements as React components. Keep a list of SVG element components in an object and use the same object to render the SVG element or pass it as prop. So you don't have to pass around the SVG as text/html.

    Here is a demonstration: When the user clicks on an SVG element, then selected element will be passed as a prop to the SelectedIcon component and it will be rendered inside it.

    import React, {useState} from "react";
    
    export default ({ text }) => {
      const [selectedIcon, setSelectedIcon] = useState(null);
    
      // Store the SVG icons in an object
      const icons = {
        left: <LeftIcon />,
        right: <RightIcon />
      }
    
      return (
        <>
          <h3>SVG Icons</h3>
          <div className="flex gap-2 mt-4">
            <span className="icon" onClick={() => setSelectedIcon('left')}>
              {icons.left}
            </span>
            <span className="icon" onClick={() => setSelectedIcon('right')}>
              {icons.right}
            </span>
          </div>
    
          <h3>Selected Icon:</h3>
    
          // Here passing the SVG icon component as a prop
          <SelectedIcon icon={icons[selectedIcon]} />
        </>
      )
    }
    
    function SelectedIcon({icon}) {
      if (! icon) {
        return <></>;
      }
    
      return (
        <span className="icon">
          {icon}
        </span>
      )
    }
    
    function LeftIcon() {
      return (
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="20"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>
      )
    }
    
    function RightIcon() {
      return (
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="20"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
      )
    }
    

    Update based on comment:

    If you would like to programmatically create the React components from the <svg> node, then you can use React.createElement. So you can parse svg and dynamically construct the react components for the SVG elements. I believe this is how most svg icon libraries do. Example: FontAwesome