LitElement render problem using SVG strings to construct nested SVGs

Viewed 2784

I have a collection of SVG strings all with different viewBox attributes. I am trying to nest all of these SVGs within a parent SVG in a way that I can dynamically set their x and y attributes. Below is a minimal example of the solution I came up with. It doesn't render initially, however if in the browser devtools I add something like a blank line to the HTML, this seems to cause a re-render and the circles are rendered as expect. So the structure of the HTML produce is correct, it seems to be a problem LitElement/lit-html rendering before my code is "finished".

I suspect the problem boils down to the way I am using templates with lit-html, and the LitElement and SVG aspects are irrelevant.

<my-example></my-example>
import { LitElement, svg } from "https://unpkg.com/lit-element?module";

const circles = {
  big: `<svg viewBox="0 0 100 100" width="300px" height="300px">
          <circle cx="50" cy="50" r="50"/>
        </svg>`,
  small: `<svg viewBox="0 0 200 200" width="300px" height="300px">
            <circle cx="100" cy="100" r="60"/>
          </svg>`
};

class MyExample extends LitElement {
  render() {
    return svg`
      <div class="ingredients">
        <svg height="50vh" width="100%" viewBox="0 0 100 100">
          ${Object.keys(circles).map((circle, i) => {
            // create template from svg string
            const template = document.createElement("template");
            template.innerHTML = circles[circle];
            const clone = template.content.cloneNode(true);
            const svgNode = clone.querySelector("svg");
            const viewBoxAtt = clone
              .querySelector("svg")
              .getAttribute("viewBox");

            // create template from svg's contents (circle element)
            const template2 = document.createElement("template");
            template2.innerHTML = svgNode.innerHTML;
            const clone2 = template2.content.cloneNode(true);

            // place circle element in a new svg with dynamically set attributes
            return svg`<svg viewBox=${viewBoxAtt} x=${i * 20} y=${i *
              20} height="30" width="30">${svg`${clone2}`}</svg>`;
          })}
        </svg>
      </div>
    `;
  }
}

window.customElements.define("my-example", MyExample);
1 Answers

it really depends on where your circles come from... assuming all have a width and height of "300px" you can simply wrap your SVGs into another SVG that gets the position and size assigned in the loop. Then just add your SVGs with unsafeHTML (that is assuming you trust the source of your SVG strings)

import { LitElement, svg } from 'https://unpkg.com/lit-element?module'
import { unsafeHTML } from 'https://unpkg.com/lit-html/directives/unsafe-html.js?module'

const circles = {
  big: `<svg viewBox="0 0 100 100" width="300px" height="300px">
         <circle cx="50" cy="50" r="50"/>
       </svg>`,
  small: `<svg viewBox="0 0 200 200" width="300px" height="300px">
           <circle cx="100" cy="100" r="60"/>
         </svg>`
 }

class MyExample extends LitElement {
  doSomething(evt){
    const index = evt.target.getAttribute("data-index")
    // do something with circle i
  }
  render () {
    return svg`
      <div class="ingredients">
        <svg height="50vh" width="100%" viewBox="0 0 100 100">
          ${Object.values(circles).map((circle, i) => {
            return svg`<svg @click="${this.doSomething}" data-index="${i}" viewBox="0 0 300 300" x=${i * 20} y=${i * 20} height="30" width="30">
              ${unsafeHTML(circle)}
            </svg>`
          })}
        </svg>
      </div>
    `
  }
}

window.customElements.define('my-example', MyExample)

another way to do this is to use a DOMParser to parse your strings to a DOM, manipulate that DOM and then turn the result back to a string to render it in the render function (that is if your SVGs do not have the same width and heigh set, and it is not possible to remove the width and height Attributes):

import { LitElement, svg } from 'https://unpkg.com/lit-element?module'
import { unsafeHTML } from 'https://unpkg.com/lit-html/directives/unsafe-html.js?module'

const circles = {
  big: `<svg viewBox="0 0 100 100" width="300px" height="300px">
         <circle cx="50" cy="50" r="50"/>
       </svg>`,
  small: `<svg viewBox="0 0 200 200" width="300px" height="300px">
           <circle cx="100" cy="100" r="60"/>
         </svg>`
}

function setPosition (node, x, y, width, height) {
  node.setAttribute('x', x)
  node.setAttribute('y', y)
  node.setAttribute('width', width)
  node.setAttribute('height', height)
}

class MyExample extends LitElement {
  render () {
    const parser = new DOMParser()
    return svg`
      <div class="ingredients">
        <svg height="50vh" width="100%" viewBox="0 0 100 100">
          ${Object.values(circles).map((circle, i) => {
            const doc = parser.parseFromString(circle, 'image/svg+xml')
            setPosition(doc.documentElement, i * 20, i * 20, 30, 30)
            return unsafeHTML(doc.documentElement.outerHTML)
          })}
        </svg>
      </div>
    `
  }
}

window.customElements.define('my-example', MyExample)

it really depends on where the data is coming from and what steps you can do to prepare the data... the most elegant solution would be to turn your SVG strings into template-string-returning functions. then the render function becomes really simple:

import { LitElement, svg } from 'https://unpkg.com/lit-element?module'

const circles = {
  big: (x, y, w, h) => svg`<svg viewBox="0 0 100 100" x="${x}" y="${y}"" width="${w}" height="${h}">
         <circle cx="50" cy="50" r="50"/>
       </svg>`,
  small: (x, y, w, h) => svg`<svg viewBox="0 0 200 200" x="${x}" y="${y}"" width="${w}" height="${h}">
           <circle cx="100" cy="100" r="60"/>
         </svg>`
}

class MyExample extends LitElement {
  render () {
    const parser = new DOMParser()
    return svg`
      <div class="ingredients">
        <svg height="50vh" width="100%" viewBox="0 0 100 100">
          ${Object.values(circles).map((circle, i) => circle(i * 20, i * 20, 30, 30))}
        </svg>
      </div>
    `
  }
}

window.customElements.define('my-example', MyExample)

following the logic (of creating a new svg element and appending the circle from your svg strings) you use in your example will only work with the all new unsafeSVG() directive which is new in the unfinished lit-html version 1.2.0... i'm happening to host a build of that so you can try this as well:

import { LitElement, svg } from 'https://cdn.klimapartner.net/modules/lit-element/lit-element.js'
import { unsafeSVG } from 'https://cdn.klimapartner.net/modules/lit-html/directives/unsafe-svg.js'

const circles = {
  big: `<svg viewBox="0 0 100 100" width="300px" height="300px">
         <circle cx="50" cy="50" r="50"/>
       </svg>`,
  small: `<svg viewBox="0 0 200 200" width="300px" height="300px">
           <circle xmlns="http://www.w3.org/2000/svg" cx="100" cy="100" r="60"/>
         </svg>`
}

class MyExample extends LitElement {
  render () {
    const parser = new DOMParser()
    return svg`
      <div class="ingredients">
        <svg height="50vh" width="100%" viewBox="0 0 100 100">
          ${Object.values(circles).map((circle, i) => {
            const doc = parser.parseFromString(circle, 'image/svg+xml')
            return svg`<svg viewBox="${doc.documentElement.getAttribute('viewBox')}" x=${i * 20} y=${i * 20} height="30" width="30">
              ${unsafeSVG(doc.documentElement.innerHTML)}
            </svg>`
          })}
        </svg>
      </div>
    `
  }
}

window.customElements.define('my-example', MyExample)
Related