Home

Native Web Components - part III

Oct 04, 2022

Content

Events

In Part I we already talked about the lifecycle events like "connectedCallback" which is called every time the component is attached to the DOM or "adoptedCallback" which get fired when a new element is adopted into a new document.

But for this part we will talk about communicating with the outer world in general and how we can make it easier for the consumer of our web component to react to some status changes of the component.

Native Events

Native events are those that are a part of the official API and are typically triggered by users actions, such as clicks, hovers, blurs, etc.

We shall discuss this subject in this issue because there are essentially two ways to deal with events:

addEventListener: this method is used to connect a handler that will be called when the element causes a particular event.

connectedCallback() { //$shadow may be obtained in constructor this.$shadow.addEventListener("click", clickHandler); }

removeEventListener: used to remove a handler that would be run when an event was triggered by a certain element.

disconnectedCallback() { //$shadow may be obtained in constructor this.$shadow.removeEventListener("click", clickHandler); }

We didn't dig too deep in the APIs today, but if you want to know more you can check MDN Web Docs both for addEventListener and removeEventListener.

When working with Web Components, it is recommended to execute these methods in a specific life cycles.

addEventListener should be executed in connectedCallback meanwhile removeEventListener should be executed in disconnectedCallback.

It's important to notice that in order for the removeEventListener to take effect, the same handler (used in addEventlistener) must be used.

Custom Events

As we run some code based on the user action, native events are the primary means by which we may bring interactivity to the web. For example, we run some process when the user clicks somewhere (click event), or we check a field when the user types on it (on focus event).

Similar to this, when creating a web component, we must set off events for the following reasons: Events (and their consequent triggering) are in some way a part of the component's public API and enable the consumer of our web component, to better integrate the component on the website. Events are a strong tool that indirectly benefits the user because they give the developer the information they need to act on behalf of both the user and the rest of the page.

Events are used to inform about several states and behaviors of the component. for instance when:

  • a value of an element has changed
  • an element has been selected/removed/added
  • some data has been loaded
  • the component has been destroyed
  • in general, any state of our component has changed

We have two ways of creating an Event. The simple one, where we are not able to add payload (data) to the event, and the second one, where we can do so.

The second way is specially useful where we don't want to inform just about a state change but how this state has changed. For instance, if our property value has changed, we will probably want to inform about the new value. To do so we need to wrap our data in an object with a special structure, as you can see below.

// simple way const event = new Event('myComponentValueChanged');
// complete way const event = new CustomEvent('myComponentValueChanged', { detail: elem.value });

To trigger the event, we can do it the same way for both cases:

elem.dispatchEvent(event);

You may ask why is there these two types of Events when one practically can rewrite the detail property in the Event Object like that:

var e = new Event("someEvent"); e.detail = { username: "name" }; element.dispatchEvent(e);

The point is, that the detail property in customEvent is readOnly, so the argument of CustomEvent was supposed to set some internal data of the event. You may find a way of replacing this by using Object.defineProperty() for instance but this is not t

var event = new CustomEvent('someEvent', {detail: 123}); event.detail = 456; // Ignored in sloppy mode, throws in strict mode console.log(event.detail); // 123 var event = new Event('someEvent'); event.detail = 123; // It's not readonly event.detail = 456; console.log(event.detail); // 456

I think the best way is to use Event und CustomEvent to classify the events differently. Event for general changes in custom Elements and CustomEvent for changes with some extra details.

Let's check this example:


As you can observe in the example above that we trigger three different events:

  1. one Event when the popup is closed
  2. one Event when the user clicked on "cancel"
  3. one CustomEvent when the user submits the form. Here we need some more information like the selected value, that's why the custom event ☺️.

At the end of the day it's up to the developer to use Event or CustomEvent but I hope the information above could help you making a decision. It's important to have an uniform code along the whole project development.

Styles

Styling the web component is so easy. We only have to add the "style"-Tag to the shadow DOM.
Here is an example of a simple card:

customElements.define("my-component", class extends HTMLElement { constructor () { super(); this.attachShadow({ mode: "open" }).innerHTML = ` <div class="card"> <div class="card-header"> <slot name="title"></slot> </div> <div class="card-content"> <slot name="content"></slot> </div> </section> <style> .card { border: 1px solid black; border-radius: 4px;} .card-header { padding: 1em; background: #d08a27; color: #fff; font-weight: 700; } .card-content { padding: 1em; color: #554c4c; } </style>`; } });

The HTML:

<div> <my-component> <div slot="title">Card 1</div> <div slot="content">Content 1</div> </my-component> </div> <div style="margin-top: 1em;"> <my-component> <div slot="title">Card 2</div> <div slot="content">Content 2</div> </my-component> </div>

Let's try to use inheritance to add some configuration to our example. CSS variables can be inherited like font and color. So let's use this:

In the HTML document we define the variable card-title-bg to specify the background-color of our card title:

:root { --card-title-bg: blue; }

now this variable will be inherited in the custom element, so we can access it and make use of it:

... <style> .card { border: 1px solid black; border-radius: 4px;} .card-header { padding: 1em; background: var(--card-title-bg); color: #fff; font-weight: 700; } .card-content { padding: 1em; color: #554c4c; } </style>; ...

You can read more about CSS inheritance under this link.

special CSS for shadow DOM

:host select the shadow host (the element containing the shadow tree)\

:host { font-weight: 700; width: 100px; padding: 1em; color: black; border: 1px solid red; }

:host(selector) Same as :host, but applied only if the shadow host matches the selector.

<my-element> <slot></slot> </my-element> <my-element sticked> <slot></slot> </my-element>
:host { font-weight: 700; width: 100px; padding: 1em; margin: 2em 0; color: black; border: 1px solid red; display: block; } :host([sticky]) { border: 2px dashed blue; background-color: #e3e3e3; width: 100%; margin: 0; text-align: center; /* sticky styles */ position: sticky; top: 0px; }

The Result:


::slotted(selector): matches elements based on two conditions:

  1. That’s a slotted element, that comes from the light DOM. Slot name doesn’t matter. Just any slotted element, but only the element itself, not its children.
  2. The element matches the selector.
<my-element> <div slot="city"> <div>Sousse</div> </div> </my-element> <script> customElements.define('my-element', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> ::slotted(div) { border: 1px solid red; } </style> City: <slot name="city"></slot> `; } }); </script>

Please note, ::slotted selector can’t descend any further into the slot. These selectors are invalid:

::slotted(div span) { /* our slotted <div> does not match this */ } ::slotted(div) p { /* can't go inside light DOM */ }

In summary:
Local styles can affect:

  1. shadow tree
  2. shadow host with :host and :host() pseudo-classes,
  3. slotted elements (coming from light DOM), ::slotted(selector) allows to select slotted elements themselves, but not their children.

Document styles can affect:

  1. shadow host (as it lives in the outer document)
  2. slotted elements and their contents (as that’s also in the outer document)
  3. When CSS properties conflict, normally document styles have precedence, unless the property is labelled as !important. Then local styles have precedence.

References:

  1. https://open-wc.org/guides/knowledge/events/
  2. https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events
  3. https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
  4. https://javascript.info/web-components
  5. https://www.sitepoint.com/css-inheritance-introduction/
Made with ❤️ by Marouen Mhiri