skip to Main Content

I created a custom HTML element:

class PieChart extends HTMLElement {
    constructor() {
        super();
    } 

    init() {
        this.#render();
    }

    #render() {
        const circleElement = document.createElement("circle");
        circleElement.setAttribute("cx", 100);
        circleElement.setAttribute("cy", 100);
        circleElement.setAttribute("r", 80);
        circleElement.setAttribute("fill", "none");
        circleElement.setAttribute("stroke", "green");
        circleElement.setAttribute("stroke-width", 40);
        circleElement.setAttribute("stroke-dasharray", `502.85 502.85`);

        const svgElement = document.createElement("svg");
        svgElement.style.display = "block";
        svgElement.style.width = "inherit";
        svgElement.style.height = "inherit";
        svgElement.setAttribute("viewBox", `0 0 200 200`);
        svgElement.append(circleElement);

        this.appendChild(svgElement);

        this.style.display  = "block";
        this.style.width = "200px";
        this.style.height = "200px";
    }

    connectedCallback() {
        const event = new CustomEvent("init", {
            bubbles: true
        });
        this.dispatchEvent(event);
    }
}

customElements.define(
    "pie-chart",
    PieChart
);
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <link rel="stylesheet" href="./css/site.css" />
</head>
<body>
    <p>Hello!</p>
    <pie-chart></pie-chart>
</body>
</html>
<script>
    document.body.addEventListener("init", (pEvent) => {
        const element = pEvent.target;
        element.init(this);
    });
</script>

This code is executed without any problems and the custom element is rendered in the DOM. However, the circle is not rendered on the screen.

enter image description here

PS: if I open DOM in a browser and click "Edit As HTML" on the PieChart element and change anything inside and save it, the circle appears. I used this pattern several times before and there were not problems, but this time is the first one when I use SVG element.

2

Answers


  1. This is not really how you create a custom element. Create a shadow DOM in the connectedCallback, and the browser does the rest, including "activating" any custom elements that already exist on the page by the time you register their tags. No custom event dispatching, the custom element system was designed to take care of this for you.

    const createSVGElement = tag => document.createElementNS(`http://www.w3.org/2000/svg`, tag);
    
    class PieChart extends HTMLElement {
      constructor() {
        super();
      } 
    
      connectedCallback() {
        console.log(`let's go`);
        const shadow = this.attachShadow({ mode: "open" });
    
        const circleElement = createSVGElement("circle");
        circleElement.setAttribute("cx", 100);
        circleElement.setAttribute("cy", 100);
        circleElement.setAttribute("r", 80);
        circleElement.setAttribute("fill", "none");
        circleElement.setAttribute("stroke", "green");
        circleElement.setAttribute("stroke-width", 40);
        circleElement.setAttribute("stroke-dasharray", "502.85 502.85");
    
        const svgElement = createSVGElement("svg");
        svgElement.setAttribute("viewBox", "0 0 200 200");
        svgElement.append(circleElement);
    
        shadow.appendChild(svgElement);
          
        const style = document.createElement("style");
        style.textContent = `
          svg {
            display: block;
            width: 200px;
            height: 200px;
          }
        `;
        
        shadow.appendChild(style);
      }
    }
    
    customElements.define("pie-chart", PieChart);
    <pie-chart></pie-chart>

    And since you’re using SVG: remember that even though SVG is "part of HTML" ever since HTML5, SVG elements on the JS side still require the use of document.createElementNS with the SVG namespace. It’s super weird, super inconvenient, but we’re stuck with it.

    Login or Signup to reply.
  2. Krusty, your replies in the comments are rude, very rude!.

    Had your question not been about Web Components, I would have downvoted and ignored any of your questions or answers in the future.

    Now I downvoted your question, and wrote this answer because others reading Stack Overflow should not be presented with (partially) incorrect information, because your were rude.

    Have I said yet your behavior is very very very rude?

    Now on to the answers.

    • Mike is very right when he says the way you init your Web Component is wrong.
      The connectedCallback executes when the Web Components enters the DOM, so at that time it can render/create DOM. No need to dispatch an Event to yet another render() method

    • Mike is not completely right when he says to create shadowDOM in the connectedCallback. You can; but you don’t want to because the connectedCallback can run multiple times.
      The constructor is the perfect place to create shadowDOM

    • SVG Namespaces are a pain. But the Browser will set the correct NameSpace when you add content as HTML (instead of DOM nodes)

    That gives us:

    <script>
    customElements.define("pie-chart", class extends HTMLElement {
      static get observedAttributes(){
        return ["stroke","size","gap"];
      }
      constructor() {
        super()
          .attachShadow({mode:"open"})
          .innerHTML =
              `<style>` +
                `:host{background:grey;display:inline-block}`+
                `svg{--s:120px;width:var(--s);height:var(--s);vertical-align:top}` +
              `</style>` +
              `<svg viewBox="0 0 200 200">` +
                `<circle cx="50%" cy="50%" r="40%"` +
                       ` fill="none" stroke="pink" stroke-width="15%"` +
                       ` pathLength="360" stroke-dasharray="30 15">`+
                `</circle>`+
              `</svg>`;
           this.circle = this.shadowRoot.querySelector("circle");
      }
      attributeChangedCallback(name,oldValue,newValue) { this[name] = this.getAttribute(name) }
      get gap()     { return this._gap || 15 }
      set gap(v)    { this._gap = v;this.size = this._size }
      set stroke(v) { this.circle.setAttribute("stroke",v) }
      set size(v)   { this.circle.setAttribute("stroke-dasharray", (this._size = v) + " " + this.gap) }
    });
    </script>
    <pie-chart></pie-chart>
    <pie-chart stroke="red"></pie-chart>
    <pie-chart stroke="yellow"></pie-chart>
    <pie-chart stroke="blue" size="55"></pie-chart>

    Notes

    • Now I don’t care less if you spend the rest of your career fixing alignment issues;
      but others shouldn’t suffer from your rudeness; so I added inline-block and vertical-align

    • for the same reason I added pathLength="360" because others shouldn’t have to deal with weird stroke-dasharray fraction values because you are rude.

    • added handling of attributes because showing off cool Web Components is cool, despite a rude OP.

    • I am not going to tell you I did https://pie-meister.github.io
      So all you need to do is:

    <script src="https://pie-meister.github.io/PieMeister.min.js"></script>
    
    <h3>Was Krusty rude in his answers?</h3>
    
    <pie-chart pulltext="120">
      <style>
        text {
          font: 2em Arial;
          text-anchor: middle;
          fill: white;
        }
      </style>
      <slice size="60%" stroke="#4b77a9">Very much! $size</slice>
      <slice size="15%" stroke="#5f255f">Yes $size</slice>
      <slice size="15%" stroke="#d21243">Certainly $size</slice>
      <slice size="10%" stroke="#B27200">Who is Krusty? $size</slice>
    </pie-chart>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search