2

I try to define a custom field, containing either a SVG or a Canvas. But my example shows some strange rendering. I expect two boxes 400 pixel wide and 300 pixel high. But both boxes seem to collapse in different ways. How can I fix this?

class TestSvg extends HTMLElement
{
  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const container = document.createElement('svg');
    container.setAttribute('width', '400');
    container.setAttribute('height', '300');
    shadow.appendChild (container);
  }
}

class TestCanvas extends HTMLElement
{
  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const container = document.createElement('canvas');
    container.setAttribute('width', '400');
    container.setAttribute('height', '300');
    shadow.appendChild (container);
  }
}

customElements.define ('test-svg', TestSvg);
customElements.define ('test-canvas', TestCanvas);
test-svg, test-canvas {
  border: 1px solid black;
}
svg
<test-svg>
</test-svg>
canvas
<test-canvas>
</test-canvas>
end

Same without custom elements:

svg, canvas {
  border: 1px solid black;
}
svg
<svg width="400" height="300"></svg>
canvas
<canvas width="400" height="300"></canvas>
end

Why is there a difference between the version with custom elements and the version without custom elements?

ceving
  • 21,900
  • 13
  • 104
  • 178

3 Answers3

1
  1. Your SVG element is not being created correctly. It needs to be in the correct SVG namespace. Change it to this:

    const container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    
  2. By default, your custom elements will be display: inline. Set them to block or inline-block depending on your need.

    test-svg, test-canvas {
      display: inline-block;
      border: 1px solid black;
    }
    

Updated test:

class TestSvg extends HTMLElement
{
  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    container.setAttribute('width', '400');
    container.setAttribute('height', '300');
    shadow.appendChild (container);
  }
}

class TestCanvas extends HTMLElement
{
  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const container = document.createElement('canvas');
    container.setAttribute('width', '400');
    container.setAttribute('height', '300');
    shadow.appendChild (container);
  }
}

customElements.define ('test-svg', TestSvg);
customElements.define ('test-canvas', TestCanvas);
test-svg, test-canvas {
  display: inline-block;
  border: 1px solid black;
}
svg
<test-svg>
</test-svg>
canvas
<test-canvas>
</test-canvas>
end
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
1

Good to see more people are combining Custom Elements and SVG, they are a good match

Your core problem was the SVG NameSpace, so Paul his answer is correct.

Some additional comments:

Your constructor can be optimized:

constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const container = document.createElementNS('http://www.w3.org/2000/svg','svg');
    container.setAttribute('width', '400');
    container.setAttribute('height', '300');
    shadow.appendChild (container);
  }
  • super() returns the element scope
    Google documentation that says to use super() first is wrong,
    they mean: Call super() before you can use the 'this' scope reference.
  • attachShadow() boths sets AND returns this.shadowRoot for free
  • .appendChild() returns the created element

So you can chain everything:

constructor() {
    const container = super()
                        .attachShadow({mode:'open'})
                        .appendChild (document.createElementNS('http://www.w3.org/2000/svg','svg'));
    container.setAttribute('width', '400');
    container.setAttribute('height', '300');
  }

If you do not do anything special with this in memory Custom Element,
you can scrap the whole constructor and create the element with HTML in the connectedCallback

connectedCallback(){
  this.innerHTML = `<svg width='400' height='300' 
                         xmlns='http://www.w3.org/2000/svg' 
                         viewBox='0 0 20 20'></svg>`;
}

Note: I also ditched shadowDOM above; if you do want shadowDOM its:

connectedCallback(){
  this.attachShadow({mode:"open"})
      .innerHTML = `<svg width='400' height='300' 
                         xmlns='http://www.w3.org/2000/svg' 
                         viewBox='0 0 20 20'></svg>`;
}

There is another sizing problem

Depending on your Font family and size there will be a gutter below your inline-block SVG Custom Element (see RED below) to allow for pgjq characters that stick out below the base line.

To counter that you have to set vertical-align: top on the SVG element:

<style>
  body { font-size: 3em }
  svg-circle { background: RED }

  svg-circle svg {
    background: grey;
    display: inline-block;
    width: 80px;
  }
  #correct svg { vertical-align: top }
</style>
<div>
  <svg-circle radius="40%" color="green"></svg-circle>
  <svg-circle x="50%" y="100%" color="blue"></svg-circle>
  <svg-circle></svg-circle>
</div>
<div id="correct">
  <svg-circle radius="40%" color="green"></svg-circle>
  <svg-circle x="50%" y="100%" color="blue"></svg-circle>
  <svg-circle></svg-circle>
</div>
<script>
  customElements.define("svg-circle", class extends HTMLElement {
    static get observedAttributes() { return ["x", "y", "radius", "color"] }
    connectedCallback()             { this.render() }
    attributeChangedCallback()      { this.render() }
    render() {
      let [x = "50%",y = "50%",radius = "50%", color = "rebeccapurple"] = 
        this.constructor.observedAttributes.map(x => this.getAttribute(x) || undefined);
      this.innerHTML = `<svg viewBox='0 0 96 96'>
                          <circle cx='${x}' cy='${y}' r='${radius}' fill='${color}'/></svg>`;
    }
  });
</script>

Note: There is no shadowDOM attached to <svg-circle>, so the SVG inside can be styled with global CSS

Make it an IMG

If you do not want any CSS bleeding, and want to work with the SVG as if it is an image,
without pointer-events issues, then create an IMG:

this.innerHTML = `<img src="data:image/svg+xml,<svg viewBox='0 0 96 96'>
                       <circle cx='${x}' cy='${y}' r='${radius}' fill='${color}'/>
                       </svg>">`.replace(/#/g, "%23");

Note: The # is the only character that needs to be escaped here. In CSS url() usage you also need to escape the < and >

Add a grid

If you are going to create an Icon Toolbar or Chessboard like layout with SVGs, add a grid:

  #correct {
    display: grid;
    grid-template-columns: repeat(3, 80px);
  }

  svg-circle svg {
    background: grey;
    display: inline-block;
    width: 100%;
    height: 100%;
  }

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
0

In the first custom element TestSvg, change the element type from svg to canvas

const container = document.createElement('canvas');

HTML elements reference

https://developer.mozilla.org/en-US/docs/Web/HTML/Element

'canvas' is an HTML element. where as 'svg' is not. 'svg' is Container element, structure element

Pavan Chandaka
  • 11,671
  • 5
  • 26
  • 34