Search code examples
javascripthtmlweb-componentcustom-elementhtml5-template

Swapping "Custom Element" without calling connectedCallback


I am creating an election application that will require switching elements in a list. I have the following Custom Element Web Components. I have trimmed the irrelevant functions from the class for brevity.

// Position
// ---------------------------------------
class Position extends HTMLElement {
    constructor(title) {
        super();
        this.title = title
    }

    connectedCallback() {
        this.className = "portlet";
        // Copy the HTML template
        var template = document.querySelector("#template-e-position");
        this.appendChild(document.importNode(template.content, true));
        // Create the title tag
        var title = this.querySelector(".title");
        title.innerHTML = this.title;
        // Create event listener for swap links
        this.querySelector(".moveUp").addEventListener("click", function(e) {
            swapWithPrevSibling(that);
        });
        this.querySelector(".moveDown").addEventListener("click", function(e) {
            swapWithNextSibling(that);
        });
    }
}
customElements.define('e-position', Position);

// Candidate
// ---------------------------------------
class Candidate extends HTMLElement {
  constructor(name) {
    super();
    this.name = name;
  }
  connectedCallback() {
        // Copy the HTML template
        var template = document.querySelector("#template-e-candidate");
        this.appendChild(document.importNode(template.content, true));
        // Create the title tag
        var name = this.querySelector(".name");
        name.innerHTML = this.name;
        // Create event listener for delete link
        var a = this.querySelector("a.delete");
        var that = this;
        a.addEventListener('click', function(e) { return that.delete(e) }, false);
    }

    delete(event) {
        deleteNode(this);
    }

}
customElements.define('e-candidate', Candidate);

I have the swap functions:

function swapWithPrevSibling (elm) {
    elm.parentNode.insertBefore(elm,elm.previousSibling)
}

function swapWithNextSibling (elm) {
    elm.parentNode.insertBefore(elm.nextSibling,elm)
}

I use the following template to build the Custom Elements:

<template id="template-e-position">
    <div class="header">
        <span class="title"></span>
        <div class="edit-menu">
            <a class="moveUp">&uarr;</a>
            <a class="moveDown">&darr;</a>
            <a class="delete">X</a>
        </div>
    </div>
    <div class="candidate-list">
    </div>
    <form class="add-candidate">
        <input type="text" />
        <input type="submit" value="Add candidate">
    </form>
</template>

<template id="template-e-candidate">
    <span class="name"></span>
    <div class="edit-menu">
        <a class="moveUp">&uarr;</a>
        <a class="moveDown">&darr;</a>
        <a class="delete">X</a>
    </div>
</template>

Since I create the Custom Elements from the HTML templates, I need to clone the templates in the connectedCallback() (since adding children in the constructor is disallowed in v1). The result of this is when I call the swap function to the "positions" in the list, it ends up re-cloning the template and adding in unnecessary DOM elements to both Position and Candidate elements.

For example, the result after swapping should be:

<e-position title="Vice-President" class="portlet">
    <div class="header">
        <span class="title">Vice-President</span>
        <div class="edit-menu">
            <a class="moveUp">↑</a>
            <a class="moveDown">↓</a>
            <a class="delete">X</a>
        </div>
    </div>
    <div class="candidate-list">
<e-candidate>
    <span class="name">Evan</span>
    <div class="edit-menu">
        <a class="moveUp">↑</a>
        <a class="moveDown">↓</a>
        <a class="delete">X</a>
    </div>
</e-candidate>
<e-candidate>
    <span class="name">Steph</span>
    <div class="edit-menu">
        <a class="moveUp">↑</a>
        <a class="moveDown">↓</a>
        <a class="delete">X</a>
    </div>
</e-candidate>
    </div>
    <form class="add-candidate">
        <input type="text">
        <input value="Add candidate" type="submit">
    </form>
</e-position>

But it ends up being a jumbled:

<e-position title="Vice-President" class="portlet">
    <div class="header">
        <span class="title">Vice-President</span>
        <div class="edit-menu">
            <a class="moveUp">↑</a>
            <a class="moveDown">↓</a>
            <a class="delete">X</a>
        </div>
    </div>
    <div class="candidate-list">
    <e-candidate>
    <span class="name">Evan</span>
    <div class="edit-menu">
        <a class="moveUp">↑</a>
        <a class="moveDown">↓</a>
        <a class="delete">X</a>
    </div>

    <span class="name"></span>
    <div class="edit-menu">
        <a class="moveUp">↑</a>
        <a class="moveDown">↓</a>
        <a class="delete">X</a>
    </div>
    </e-candidate><e-candidate>
    <span class="name">Steph</span>
    <div class="edit-menu">
        <a class="moveUp">↑</a>
        <a class="moveDown">↓</a>
        <a class="delete">X</a>
    </div>

    <span class="name"></span>
    <div class="edit-menu">
        <a class="moveUp">↑</a>
        <a class="moveDown">↓</a>
        <a class="delete">X</a>
    </div>
    </e-candidate>
    </div>
    <form class="add-candidate">
        <input type="text">
        <input value="Add candidate" type="submit">
    </form>

    <div class="header">
        <span class="title"></span>
        <div class="edit-menu">
            <a class="moveUp">↑</a>
            <a class="moveDown">↓</a>
            <a class="delete">X</a>
        </div>
    </div>
    <div class="candidate-list">
    </div>
    <form class="add-candidate">
        <input type="text">
        <input value="Add candidate" type="submit">
    </form>
</e-position>

Is there a better way to clone the HTML templates so that I don't need to add elements in the connectedCallback? If not, how can I efficiently swap without bringing along all the extra elements? Note that I do not want to use jQuery as I want a lightweight application.

I have seen this and it doesn't work because it ends up calling the connectedCallback and inserting How to swap DOM child nodes in JavaScript?


Solution

  • There are several solutions:

    1. You can use a flag to see if it's the first time the callback is called, and insert the template only if the flag is not set yet.

    customElements.define('e-candidate', class extends HTMLElement {
      connectedCallback() {
        if (!this.init) {
          var template = document.querySelector('#template-e-candidate')
          this.appendChild(template.content.cloneNode(true))
          this.querySelector('.moveUp')
              .onclick = () => 
                  this.previousElementSibling && this.parentElement.insertBefore(this, this.previousElementSibling)
          this.init = true
        }
      }
    })
    e-candidate { display:block }
    <template id="template-e-candidate">
        <slot></slot>
        <button class="moveUp">&uarr;</button>
    </template>
    
    <e-candidate>First</e-candidate>
    <e-candidate>Second</e-candidate>

    1. You can use CSS flexbox with CSS order property to change the order of the elements without using insertBefore().