Events: Web Components Phone Home

Events: Web Components Phone Home

This is (probably) the final installment in my series on Web Components, which you should totally check out if you haven't already, not only because I wanted to use the phrase "totally check out" in print, but also because those previous couple of articles lay the groundwork for understanding this one.

Obligatory Recap

In the previous articles, we've built a reasonably capable component which let's us add a "loading" screen to a web page. You add the component to the page in a few lines of code:

<html>
<head>
  <script src="eh-loading-modal.js"></script>
</head>
<body>
  <main>some content here</main>
  <eh-loading-modal></eh-loading-modal>
  <script>
     const loader = document.getElementsByTagName('eh-loading-modal')[0]
     loader.show()
     setTimeout(() => loader.hide(), 5000)
  </script>
</body>
</html>

We include the components source in the <head>, drop the component on the page with the <eh-loading-modal> tag, and manipulate it with JavaScript.

Huh? Events?

Events are signals you can listen for and respond to in your code. For example, you've probably seen code like:

node.AddEventListener('click', somefunction);

This tells your browser that when a users "clicks" on that particular node, it should run your somefunction() code. DOM nodes "emit events" during interactions with the person on the other side of the screen, or when other "interesting" things happen. Different interactions result in different events, of course, and there are a handful of standardized events. Ok, it's LARGE handful.

You can, in your code, emit these standard events or create your own custom events.

Emitting an Event

Our component isn't a very good candidate for an event source. It intentionally doesn't interact with the user, and it doesn't much matter if the window is resized, or the battery goes low. But lack of solid reasons never stopped me before, so ... onward!

We're going to have our component emit an event whenever it is shown or hidden. I know, it's unlikely such a feature would be useful, but, like the plot in every RomCom ever put to film, let's just pretend it makes sense.

First, let's revisit our component's code, specifically the show() and hide() methods:

window.customElements.define('eh-loading-modal',
  class EhLoadingModal extends HTMLElement {
...
    show() { this.setAttribute("visible", "") }
    hide() { this.removeAttribute("visible") }
...

If you've forgotten what these are doing, please see Slots and Properties and Attributes, Oh My!. Basically, call show() to show the modal, and hide() to hide it.

We want some events to come out of the component when it goes from hidden to showing, and from showing to hidden (again, as in a RomCom, it's best not to question it), so let's revise those methods slightly:

window.customElements.define('eh-loading-modal',
  class EhLoadingModal extends HTMLElement {
...
    show() { 
      this.setAttribute("visible", "")
      this.dispatchEvent(new CustomEvent("show"))
    }
    hide() {
      this.removeAttribute("visible")
      this.dispatchEvent(new CustomEvent("hide"))
    }
...

Pretty straightforward. We do the work needed to effect the transition we want, and then we call dispatchEvent() to send out a custom Event. The dispatchEvent() method is inherited from HTMLElement, so we don't need to worry about it.

Now, let's update our POC page, crack open the Console window in your favorite browser and test it out:

<html>
<head>
  <script src="eh-loading-modal.js"></script>
</head>
<body>
  <main>some content here</main>
  <eh-loading-modal visible>
    <h2>Please Wait</h2>
    <p>We're loading your report. This could take a moment.</p>
  </eh-loading-modal>
  <script>
     const loader = document.getElementsByTagName('eh-loading-modal')[0]
     loader.addEventListener('show', e => console.log("Modal is showing"))
     loader.addEventListener('hide', e => console.log("Modal is hidding"))
     loader.show()
     setTimeout(() => loader.hide(), 5000)
  </script>
</body>
</html>

You should get something very like this in your Console:

console-screenshot.png

Why not "Standard" Events?

When you use addEventListener(), you specify a string naming the event, which matches what we send via dispatchEvent(). And, as shown above, you create a CustomEvent object to carry your event. But you can ALSO use a "standard" event:

        this.dispatchEvent(new Event("change"))

Events like "change" and "click" are in common usage. We all understand what they mean in the context of standard HTML nodes, like an <input> or a <button> and so if our component behaves in a similar way, it should probably emit the same events.

Let's make an adjustment to our component to reflect that.

window.customElements.define('eh-loading-modal',
  class EhLoadingModal extends HTMLElement {
...
    show() { 
      this.setAttribute("visible", "")
      this.dispatchEvent(new Event("change"))
    }
    hide() {
      this.removeAttribute("visible")
      this.dispatchEvent(new Event("change"))
    }
...

And our POC page:

...
  <script>
     const loader = document.getElementsByTagName('eh-loading-modal')[0]
     loader.addEventListener('change', e => console.log(`Modal is ${loader.visible ? 'showing' : 'hidding'`))
     loader.show()
     setTimeout(() => loader.hide(), 5000)
  </script>
...

Same result, and we're now using an event everybody already knows. And since we often place such event listeners higher up in the DOM (such as on the document), let's see what happens when we do this:

...
  <script>
     ...
     document.addEventListener('change', e => console.log(`Modal is ${loader.visible ? 'showing' : 'hidding'`))
     ...
  </script>
...

Oh Event, where art thou?

So the page will show the modal, then 10 seconds later hide it, as you'd hope, but our console messages have gone. Why?

You'll recall -- especially if you go back to read Component Styling For Fun and Profit -- that there is a boundary between the page's DOM and the "mini-DOM" (shadowRoot) which comprises a component on that page. Things like CSS selector and selector queries won't cross that boundary. Same for Events, except when we tell the Event that it SHOULD cross that boundary:

window.customElements.define('eh-loading-modal',
...
    show() { 
      this.setAttribute("visible", "")
      this.dispatchEvent(new Event("change", {bubbles: true}))
    }
...

And now we get our events back again.

That's a wrap

There's SO much more to explore, but I think, for now, we're done. I hope you've enjoyed this series, and learned something. Please feel free to share your constructive comments!

And also feel free to check out and use this component, and a (slowly) growing collection of others I find myself using more and more frequently, at my EH-WebComponents Demo site, and find the source on GitHub!

Did you find this article valuable?

Support Todd Esposito by becoming a sponsor. Any amount is appreciated!