ducin
ducin

Reputation: 26467

HTML-based event listener for webcomponents/custom events (JS)

TL;DR; Is it possible to define an event listener in HTML (not JS) for a custom events?

Basing on this codepen, I'm trying to do the following:

<my-checkreport
  onclick="myFunction()"
  oncheck="myFunction1)"
  check="myFunction()"
></my-checkreport>

where myFunction does some console.log stuff that I can see in the browser console. Native onlick works, of course, but neither oncheck nor check work, for the custom event defined below (source taken from above linked codepen):

customElements.define('my-checkbox', class extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    this.checkEvent = new CustomEvent("check", {
      bubbles: true,
      cancelable: false,
    });
    shadowRoot.innerHTML = `
      <label>
        <input type="checkbox" id="my-checkbox"></input>
        Dummy Enabled
      </label>`;
    shadowRoot.querySelector('#my-checkbox').addEventListener('click', (e) => {
      console.log('checked', e.target.checked);
      this.dispatchEvent(this.checkEvent);
    });
  }
});

Is it possible to attach event listeners for custom events in HTML?

Upvotes: 4

Views: 4121

Answers (2)

ayushgp
ayushgp

Reputation: 5091

Without any JS code? No. But you could define a property in the constructor of your element to get the element attribute and eval it. For example,

constructor() {
    // Other code.
    const onCheckFunc = this.getAttribute('oncheck')
    this.addEventListener('check', () => eval(onCheckFunc))
    // Rest of your code.
}

EDIT: As mentioned by @Intervalia, you shouldn't inspect attributes of the element in constructor. You could do this in connectedCallback. But remember:

  • In general, work should be deferred to connectedCallback as much as possible—especially work involving fetching resources or rendering. However, note that connectedCallback can be called more than once, so any initialization work that is truly one-time will need a guard to prevent it from running twice.
  • In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.

Upvotes: 1

Intervalia
Intervalia

Reputation: 10945

This is the closest I can come to emulating the DOM declaration of an event handler. This code does everything, from what I can tell, that the DOM does for built in event handlers.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>On-Event Test</title>
    <script>
    function onCheckHandler(event) {
      console.log('onCheckHandler: %O', event);
      event.stopPropagation();
      event.stopImmediatePropagation();
      //event.preventDefault();
    }

    function eventListener(event) {
      console.log('eventListener: %O', event);
    }
    (function() {
      var template = `<div>Click to Check</div>`;
      class MyEl extends HTMLElement {
        constructor() {
          super();

          var rootEl = this.attachShadow({mode: 'open'});
          rootEl.innerHTML = template;
          rootEl.firstChild.addEventListener('click', () => {
            var checkEvent = new CustomEvent("check", {bubbles:true,cancelable:true});
            if (this.dispatchEvent(checkEvent)) {
              // Do default operation here
              console.log('Performing default operation');
            }
          });
          this._onCheckFn = null;
        }

        static get observedAttributes() {
          return ['oncheck'];
        }

        attributeChangedCallback(attrName, oldVal, newVal) {
          if (attrName === 'oncheck' && oldVal !== newVal) {
            if (newVal === null) {
              this.oncheck = null;
            }
            else {
              this.oncheck = Function(`return function oncheck(event) {\n\t${newVal};\n};`)();
            }
          }
        }

        get oncheck() {
          return this._onCheckFn;
        }
        set oncheck(handler) {
          if (this._onCheckFn) {
            this.removeEventListener('check', this._onCheckFn);
            this._onCheckFn = null;
          }

          if (typeof handler === 'function') {
            this._onCheckFn = handler;
            this.addEventListener('check', this._onCheckFn);
          }
        }
      }

      // Define our web component
      customElements.define('my-el', MyEl);
    })();
    </script>
  </head>
  <body>
    <my-el oncheck="onCheckHandler(event)"></my-el>
  </body>
</html>

To get this to work you need two steps:

Step One:

The component code must support an attribute change.

When the attribute is set to a string (The function to call) then the code creates a temporary function that attempts to call the function provided as the attribute value. That function is then passed on to step two.

When the attribute is set to anything else or the attribute is removed then the code passes a null on to step two.

Step Two:

The component code must support the oncheck property.

Whenever this property is changed it needs to remove any previously defined event handler for this property by calling removeEventListener.

If the new value of the property is a function then is calls addEventListener with that function as the handler.

This was fun to figure out. At first I thought that the DOM based oncheck handler would always be first. But by testing onclick and addEventHandler('click') I discovered that the event handlers are added in the order received. If onclick is provided in the DOM than that event listener is added first. If you then call removeAttribute('onclick') and then setAttribute('onclick', 'handler(event)') then that event handler is moved to the end of the list.

So this code replicates exactly what I same with the click event.

Upvotes: 3

Related Questions