javascriptsvgweb-componentnative-web-component

Slotting a SVG inside another SVG in a Web Component


Here's a CodePen: https://codepen.io/neezer/pen/VwbZNYB

I want to slot a SVG inside another SVG in a custom web component, but each time I get a blank screen. In the example above, you can comment out all of the JS and see the red square that should appear in my custom web component. I know my browser supports <slot> because the MDN examples work.

What am I doing wrong?


Solution

  • foreignObject only works when the user of the <svg-slot>Web Component (JSWC)
    includes a valid SVG:

    <svg-slot>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
        <circle cx="50%" cy="50%" r="15%" fill="green"></circle>
      </svg>
    </svg-slot>
    

    for the <template>:

    <template>
      <style>svg{width:40vw}</style>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
        <circle cx="50%" cy="50%" r="25%" fill="red"></circle>
        <foreignObject x="0" y="0" width="100%" height="100%">
          <slot></slot>
        </foreignObject>
      </svg>
    </template>
    

    The <svg-slot> Web Component code required:

      customElements.define('svg-slot', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode:'open'})
              .append(document.querySelector('template').content.cloneNode(true));
        }
      });
    

    But...

    you want your Web Component users to write minimal semantic HTML:

    <svg-slots>
      <circle slot="foo" cx="50%" cy="50%" r="15%" fill="green"></circle>
      <circle slot="bar" cx="50%" cy="50%" r= "5%" fill="gold"></circle>
    </svg-slots>
    

    That needs some extra work in the connectedCallback of the Web Component

    Because <circle> in lightDOM now are Unknown HTML Elements.

    So you need an extra step to turn everything in lightDOM into SVG (correct SVG NameSpace)

    Which can then be injected into the (template) SVG in shadowDOM

    <svg-slots>
      <circle slot="foo" cx="50%" cy="50%" r="30%" fill="green"></circle>
      <circle slot="bar" cx="50%" cy="50%" r="10%" fill="gold"></circle>
    </svg-slots>
    
    <template>
      <style>svg{ height:180px }</style>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
        <circle cx="50%" cy="50%" r="50%" fill="red"></circle>
        <slot name="foo"></slot>
        <slot name="bar"></slot>
      </svg>
    </template>
    
    <script>
      customElements.define('svg-slots', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode:'open'})
              .append(document.querySelector('template').content.cloneNode(true));
          setTimeout(()=>{ // make sure innerHTML is parsed
            let svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
            svg.innerHTML = this.innerHTML;
            svg.querySelectorAll("*")
               .forEach(el =>
                        this
                          .shadowRoot
                          .querySelector(`slot[name="${el.getAttribute("slot")}"]`)
                          ?.replaceWith(el)
                )
          })
        }
      });
    </script>

    notes: