In the past we talked about using Svelte for creating web components. In this article we will try to understand how web components work and how to create them in native/vanilla way. In the end we will compare them with Svelte or any other frontend framework.
Theory
First, a bit of theory, what are web components ? Web components are a standard for creating native html components/tags which we can use inside html. Browsers gives us that possibility by exposing different API-s through window object.
Three main API-s at the moment are:
- Custom Elements
- Shadow DOM
- HTML Templates
Custom Elements
A set of JavaScript APIs that allow you to define custom elements and their behaviour, which can then be used as desired in your user interface.
Shadow DOM
A set of JavaScript APIs for attaching an encapsulated "shadow" DOM tree to an element — which is rendered separately from the main document DOM — and controlling associated functionality. In this way, you can keep an element's features private, so they can be scripted and styled without the fear of collision with other parts of the document.
HTML Templates
The template and slot elements enable you to write markup templates that are not displayed in the rendered page. These can then be reused multiple times as the basis of a custom element's structure.
Example
Now lets move the theory into practice, and see on the example just how to use all of the APIs to create stylized image component, element that takes the source of the image and applies the image effects to them.
1class StylizedImageComponent extends HTMLElement {2 constructor() {3 super();4 }56 static get observedAttributes(): string[] {7 return [];8 }910 attributeChangedCallback(name: string, oldValue: string, newValue: string) {}1112 connectedCallback() {}1314 disconnectedCallback() {}1516 static register() {17 customElements.define("styled-image", StylizedImageComponent);18 }19}
This part of the code shows how to utilize the Custom Element API. Here we are extending class provided by windows object HTMLElement, which contains all the functionality we need to create our component. We have three lifecycle hooks/methods, if anyone have experience with frameworks like VUE or React, it works similar way to how they behave in those frameworks.
Method connectedCallback is function that is called the moment element enters the DOM and usually is used for adding listeners in the component.
Method disconectedCallback is called when element is removed from DOM and usually is used for cleanup of the listeners in the component.
Method attributeChangedCallback is fired when any of the attribute provided to our component is updated and it depends on static getter observedAttributes, so any of the attributes we will need to update, their name needs to returned in the array from getter. It receives three argument, name of the attribute that is updated, old value and new value, we can based on the name add switch statement and update what we would like according tho the name of the attribute.
1attributeChangedCallback(name, oldValue, newValue) {2 switch (name) {3 case 'src':4 this.hideElement();5 this.setSrc(newValue);6 this.showElement();7 break;8 case 'effect':9 this.effect = newValue;10 this.removeEffect(oldValue);11 this.setEffect(newValue);12 break;13}
This method is only called when attributes are updated on the attributes that are tracked, so any of the starting attributes passed that does not need to be tracked are going to be worked on in constructor or connectedCallback method.
1constructor() {2 super();3 const src = this.hasAttribute('src') ? this.getAttribute('src') : '';4 this.effect = this.hasAttribute('effect') ? this.getAttribute('effect') : '';56 if (src) {7 this.setSrc(src);8 }910 if (this.effect) {11 this.setEffect(this.effect);12 }13}
As we can see in the constructor we are checking if attributes exist and then setting them. Because our class extends HTMLElement it has all the methods element from DOM has, so we can use hasAttributes and getAttributes from our class. Of course we need to remember how to operate on DOM elements, after a long time working in React or similar frameworks :-).
In the static method register, we can see method define from customElements object, which is also provided to us from the window, in that method we define name of our tag that will be used inside of HTML and we provide Class component which extends HTMLElement, whether we extend it directly or use any of the other Classes that do that, (eg. HTMLParagraphElement). The name of the tag needs to contain two or more words, so it does not clash with already existing html tags.
1if (!this.shadowRoot) {2 this.attachShadow({mode: 'open'});3 this.shadowRoot.appendChild(stiliziraniTemplate.content.cloneNode(true));4 this.ImageContainer = this.shadowRoot.querySelector('.image');5}
Here we can see how we can use Shadow DOM, to open shadow DOM, we create shadowRoot by calling attachShadow method, we pass mode object as first property, it can be "open" or "closed", and automatically we get shadowRoot, but our new DOM is empty, We clone content from our template and that is passed to shadowRoot with method appendChild. Now every time we want to update DOM in our component we need to use shadowRoot as we can see in the last line where we are targeting image container element from our template.
1const template = document.createElement('template');2template.innerHTML = `3<style>4 .image {5 opacity: 0;6 display: flex;7 align-items: center;8 justify-content: center;9 padding: 10px;10 width: 400px;11 height: 400px;12 background-repeat: no-repeat;13 background-size: cover;14 background-position: center;15 border: 15px solid steelblue;16 transition: opacity 0.5s ease;17 }18 .show {19 opacity: 1;20 }21</style>22<div class="image">23 <slot name="title"></slot>24</div>25`;
We create template the same way we create any element, we just pass in the name of the tag as template, as any child of that tag is not rendered so we can put all the html content inside including styles and that will not be visible to the user. That variable is passed to sadowRoot. We can see new tag name slot, it is placeholder for any of the html passed from parent DOM to our component, we can have multiple placeholders with different names, if we omit name it will behave as default placeholder, it works in similar way to slots from VUE or Svelte.
Now we understand how we can send attributes to the component, but to send information from component, just like the regular DOM elements we use Events Api, similar way how it is used in Svelte or VUE, we just dispatch our custom event with desired information.
1connectedCallback() {2 this.dispatchEvent(new CustomEvent('onMount', {detail: "information"}));3}
Now sure this was very simplified, as with web components we can create and extend HTML as we want. In this repository you can the full code for the component and many more examples, like native forEach loop tag or lazy loaded image.
Conclusion
In the end I still prefer using Svelte for web components, as it gives better experience for writing html, CSS and as a bonus you get reactivity. Also Svelte does not give extra dependency as frameworks like VUE, React or Angular, so web components are smaller.
Now we can finally have styled checkbox :-)