2

I'm making a monopoly game for my website, and I have a problem that I can't add event listeners for "i" element in a custom element

Here's my custom element:

class Popup extends HTMLElement {
  constructor() {
    super();
    var that = this;

    var shadow = this.attachShadow({mode: 'open'});
    var wrapper = document.createElement('div');
    wrapper.setAttribute("class","popup-wrapper");
    var popup = document.createElement('div');
    popup.setAttribute('class','popup');

    let exitButton = document.createElement('i');
    exitButton.className = "fas fa-times fa-lg exit";

    exitButton.addEventListener("click", function () {
      console.log('a');
    });

    // styles
    var style = document.createElement('style');
    style.textContent = `
      @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css");
      @import url("https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css");

      .popup-wrapper {
        position: fixed;
        top: 0; left: 0; right: 0; bottom: 0;
        background-color: rgba(0, 0, 0, .6);
        z-index: 9999;
        visibility: visible;
      }

      .popup {
        position: fixed;
        top: 50%; left: 50%;
        transform: translate(-50%, -50%);
        border: 1px solid #d6d6d6;
        background-color: #fff;
        border-radius: .25rem;
        padding: 1rem 2rem;
      }

      /* .exit {
        position: absolute;
        top: .5rem; right: .5rem;
        cursor: pointer;
      } */
    `;


    popup.appendChild(exitButton);

    wrapper.appendChild(popup);

    shadow.appendChild(style);
    shadow.appendChild(wrapper);

    this.isOpen = false;
    this.popup_wrapper = wrapper;
    this.popup = popup;
    this.exitButton = exitButton;
  }

  close() {
    console.log('a');
    this.remove();
  }
}

class TextPopup extends Popup {
  constructor() {
    super();

  }

  show() {
    this.exitButton.addEventListener("click", function () {
      console.log('a');
    });

    this.heading = this.getAttribute("heading");
    this.text = this.getAttribute("text");

    this.popup.innerHTML += `
      <h1>${this.heading}</h1>
      <p>${this.text}</p>
    `;
  }
}

customElements.define('text-popup', TextPopup);

I had tried to place it in multiple locations but it is still not working

I also have another custom element where addEventListener working:

class Copy extends HTMLElement {
  constructor() {
    super();

    if (!this.hasAttribute('text')) return console.error("Text is not specified");

    this.text = this.getAttribute('text');
    this.style.cursor = "pointer";
    var that = this;
    this.addEvents(["click", "touchend"], () => {
      that.copy(that.text)
    });
  }

  copy(text) {
    navigator.clipboard.writeText(text).then(function() {
      // successfull
    }, function(err) {
      // unsuccessfull
    });
  }
}

customElements.define('copy-button', Copy);

PS: addEvents prototype:

Element.prototype.addEvents = Document.prototype.addEvents = Window.prototype.addEvents = function (events, callback) {
  for (var i = 0; i < events.length; i++) this.addEventListener(events[i], callback);
};

I'm on the last version of chrome

  • You should be attaching event listeners in the connectedCallback, not the constructor. – Jared Smith May 31 '20 at 12:43
  • This might be related? https://stackoverflow.com/questions/34896106/attach-event-to-dynamic-elements-in-javascript –  May 31 '20 at 12:47
  • Why attach events in `connectedCallback`? If the events are added in constructor, they should be garbage collected when the element is removed and no longer referenced, right? Plus, user interaction events, for example, won't fire anyway unless the element is attached to the DOM. Maybe it depends on the desired functionality, and for UX events nothing happens when not connected anyway (but for custom events that could fire on a timer from children regardless of connection, maybe that would make a difference). – trusktr Oct 08 '22 at 02:22

2 Answers2

0

So thanks to @Jared Smith and @Ken yo, I have this solution:

I have found that there is path, which is an array with all of the path of the click

class TextPopup extends Popup {
  constructor() {
    super();
  }

  connectedCallback() {
    document.body.addEventListener("click", function (e) {
      if (e.path[0].classList.contains("exit")) {
        console.log("exitButton was clicked!");
      }
    });
  }

  show() {
    this.heading = this.getAttribute("heading");
    this.text = this.getAttribute("text");

    this.popup.innerHTML += `
      <h1>${this.heading}</h1>
      <p>${this.text}</p>
    `;
  }
}
0

Some pointers:

  • FontAwesome and Bootstrap CSS inside a shadowDOM also require loading the CSS in the main document

  • For a single click inside a shadowDOM you don't have to use a Event Listener (tech for adding multiple listeners), setting the onclick Event Handler will do

  • using a <TEMPLATE> instead of creating HTML with JS is a lot less error prone (and simpler, and less code, and.. )

  • emit your own CustomEvent with dispatchEvent, saves you from handling path[0] data in a global Event Listener

Note: the CSS loading in this SO snippet causes a 3 second delay after clicks... only on StackOverflow

<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css">

<fa-button icon="fa-bars"></fa-button>
<div id=buttonbar>
  <fa-button icon="fa-address-card"></fa-button>
  <fa-button icon="fa-camera"></fa-button>
  <fa-button icon="fa-window-close"></fa-button>
</div>

<style>
  body {
    font-size: 48px; /* font settings cascade into shadowDOM */
  }
  #buttonbar {
    --buttonbackground: "grey"; /* CSS properties cascade into shadowDOM */
    --buttoncolor: black;
    float: right;
  }
</style>

<template id="FA-BUTTON">
  <style>
    @import url("//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css");
    :host {
      display: inline-block;
    }
    i {
      background: var(--buttonbackground, yellow);
      color: var(--buttoncolor, red);
    }
  </style>
  <i class="fas"></i>
</template>

<script>
  customElements.define('fa-button', class extends HTMLElement {
    constructor() {
      super() // returns 'this'
        .attachShadow({ mode: 'open' }) // returns AND sets 'this.shadowRoot'
        .append(document.getElementById(this.nodeName).content.cloneNode(true));
    }
    connectedCallback() {
      this.shadowRoot.querySelector("i").classList.add(this.getAttribute("icon"));
      this.onclick = () =>
        this.dispatchEvent(new CustomEvent("buttonclick", {
          detail: {
            clicked: this.getAttribute("icon")
          },
          bubbles: true,
          composed: true //escape shadowDOM
        }));
    }
  });

  document.body.addEventListener("buttonclick", (evt) => {
    console.log(evt.detail, evt.composedPath()); // full path into shadowDOM
  });

</script>
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49