DEV Community

Apiumhub
Apiumhub

Posted on • Originally published at apiumhub.com on

Web Components: Everything You Need to Know

Currently, and for quite a while now, most developments are done under the umbrella of a framework. If we focus on the front end and Javascript, we can find dozens of frameworks. It is challenging to reuse graphical interface elements like buttons or other components when you want to because each of them has distinctive qualities.

What are Web Components

Web Components are a set of elements from different standardized technologies, such as HTML, CSS, and Javascript, that form a structure that allows them to be used in other websites or applications. These technologies allow the creation of customized elements both in functionality and appearance. One of their strong points is that they are framework-agnostic, so they can be used in any Javascript framework. This makes it possible to have a library of shared components while also having various platforms and technologies. It can be very useful to unify and maintain a brand image in a simpler way.

The World Wide Web Consortium (W3C), also known as the organization that founded the internet as we know it today, developed this method in 2012 to standardize all of the web´s foundational technologies.

Why use Web Components?

Most web development is done under a Javascript framework such as Angular, Vue, or well-known libraries such as React JS. All these frameworks and libraries are very useful for developers, as they provide them with a series of tools that make development faster and more reliable.

However, it’s not all good news because developers frequently need to use the same components in different projects using different frameworks or libraries. They are consequently compelled to rewrite those parts with duplicate code. This poses a difficulty in terms of maintainability because the developer must make these changes as many times as the component has been replicated in order to address potential issues and integrate new functionalities or adjustments.

This can be resolved by using web components, which enable the development of individualized components using HTML, CSS, and Javascript that are not dependent on frameworks or libraries. In other words, this means the developer only needs to build them once to use them across all projects.

There is also another case in which the use of web components can be interesting. Let’s say that a company has a strong corporate image but uses different platforms or web tools. It can be difficult to unify the style of buttons or other elements with the same design. With the web components approach, designers can create a collection of pieces that match the corporate image, and developers just need to implement it once, thanks to the shared catalog of parts.

Web Components Specifications

Web Components are based on four main specifications, as explained below:

Custom elements

Custom elements are a set of APIs that allow the developer to create new HTML tags. You can define the behavior and how it should be created at a visual level. There are two types of Custom elements:

  • Autonomous custom elements: used to create completely new HTML elements.
  • Customized built-in element: used to extend existing HTML elements or other web components.

Shadow DOM

The shadow DOM API allows you to isolate fragments of the original DOM, so you can hide those internal elements that compose a larger element shown in the DOM. The internal behavior is similar to that of an iframe, which allows isolating its content from the rest of the document, but it has a difference: with the shadow DOM, total control over the internal content is maintained. This process of isolating elements from their environment is called encapsulation and prevents CSS and JavaScript code from leaking into other custom elements.

ES modules

Before ES modules existed, Javascript did not have a module system like other languages. In order to inject Javascript code into applications, tags such as <script/> were used, and later other ways to define modules appeared, such as CommonJS, but none of them became standardized.

ES modules appeared to provide a standard solution to this problem. Now we have it included in Javascript ES6, and it allows us to group some functionalities in a library and reuse them in other Javascript files.

HTML templates

These HTML templates allow you to create code snippets that are reusable as HTML but are not rendered immediately on page load. The templates can be inserted at runtime into the main document using JavaScript, and the internal resources are only executed at the time the elements are inserted into the document. No matter how many times a template is used, it is only read once, so good performance is assured.

This system initially creates an empty template so that it does not interfere with the rest of the application and only renders the content of that template when it is required, thus again ensuring good performance.

Compatibility

The compatibility of web components is very wide. All Evergreen browsers (Chrome, Firefox, and Edge) support it without any problem. They have support for all APIs (custom elements, HTML templates, shadow DOM, and ES modules).

Although the compatibility is wide, there are some exceptions, such as Internet Explorer and Safari. In the case of Internet Explorer, the incompatibility is due to its closure by Microsoft, which will remove access on February 14, 2023. As for Safari, there are certain functionalities that are compatible and others that are not. The autonomous custom elements that have been explained above are 100% compatible with Safari. However, the shadow DOM has not yet been implemented, and after a 2013 debate between Google and Apple engineers, it was decided that customized built-in elements were not going to be implemented either.

Challenges of Web Components

Web components have faced different challenges to find their place and make their implementation worthwhile. They have evolved a lot; however, there is still room for improvement.

Integration with general styles

How to handle the overwriting of general styles in the application is one of the challenges that web components have faced and for which they currently have somewhat complex solutions. For this, there are several options:

  • Do not use Shadow DOM: You can add the styles directly to the custom element, although this leaves the code open for some script to accidentally or maliciously change it.
  • Use the: host class: This class allows you to select a custom element from the shadow DOM and style it in a specific way.
  • Using CSS custom properties: The Custom properties or variables are connected in cascade in the Web Components, therefore if your element uses a variable you can define it inside_:root_ and it will be able to be used without problem.
  • Using shadow parts: With the new :part selector you can access a part of a shadow tree. Therefore this new method allows you to style a specific part of a custom element.
  • Pass styles as string: Styles can be passed as a parameter to be applied inside the block_._</li> </ul> <h3> <a name="integration-with-forms" href="#integration-with-forms" class="anchor"> </a> Integration with forms </h3> <p>All types of <input>, <textarea>, or <em><select></em> elements in the Shadow DOM are not automatically bound to the form that contains them. Initially, hidden fields were added to the DOM but this broke the encapsulation of the web component.</p> <p>Currently, the new <em>ElementInternals</em> interface, allows us to connect to the form with custom values and even define validations. It is implemented in Chrome but there is a polyfill available for other browsers.</p> <p>To demonstrate how this new interface works, we are going to create a basic component of a form. First of all, the class must have a static value called <em>formAssociated,</em> which determines if the form is connected or not. We can also add a callback to see when it is connected.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>class InputPwd extends HTMLElement { static formAssociated = true; formAssociatedCallback(form) { console.log('form is associated:', form.id); } } </code></pre></div> <p></p> <p>Then, in the constructor, the <em>attachInternals()</em> method is called, which allows the component to communicate with the form or other elements that require visibility on the value or validation. The <em>setValue</em> method is also implemented through which the value is set in the form. Initialized to an empty string.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>constructor() { super(); this.internals = this.attachInternals(); this.setValue(''); } setValue(v) { this.value = v; this.internals.setFormValue(v); } </code></pre></div> <p></p> <p>Once this is done you can create the <em>connectedCallback()</em> method, which creates a Shadow DOM and monitors the changes in order to propagate them to the parent form.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` &lt;style&gt;input { width: 8em; }&lt;/style&gt; &lt;input placeholder="Password" /&gt;`; // subscribe for changes shadow.querySelector('input').addEventListener('input', e =&gt; { this.setValue(e.target.value); }); } </code></pre></div> <p></p> <p>From this point on, you can create the HTML form that will contain the Web Component.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>&lt;form id="globalForm"&gt; &lt;input type="text" name="user-email" placeholder="email" /&gt; &lt;input-pwd name="user-pwd"&gt;&lt;/input-pwd&gt; &lt;button&gt;Login&lt;/button&gt; &lt;/form&gt; </code></pre></div> <p></p> <h3> <a name="dependency-injection" href="#dependency-injection" class="anchor"> </a> Dependency injection </h3> <p>Dependency injection is another challenge that Web Components have to face. In many cases, developers will need dependency injection while programming to make their components reusable.</p> <p>For this reason, one could try to do dependency injection through the constructor as illustrated below.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>class MyWebComponent extends HTMLElement { constructor(logger: Logger, translations: TranslationService) { supper(); this.logger = logger; this.translations = translations; } } </code></pre></div> <p></p> <p>Unfortunately, this is not valid because by specification when an element becomes a Custom element its constructor is called without arguments, therefore dependencies could not be passed. Even in this way, it could be solved in some alternative way, but it would only work if you create those components via Javascript and not directly in the HTML. In a real case, it would not make sense since what you want is to add the Web Components via HTML.</p> <p>In addition, there is another problem and that is that in case the constructor will be called, the component is not inserted in the DOM directly. Therefore, if a hierarchical dependency injection is needed it would be necessary to wait until the <em>connectedCallback</em> callback is executed which indicates that it is already positioned in the DOM.</p> <p>Fortunately, there is a solution to this problem. In order to solve it we have to focus on the <em>connectedCallback</em> method, since from that moment we know that we have the correct hierarchy to be able to inject the dependencies. In order to get the dependencies we can use the event system integrated into the browser. Luckily, they are synchronous. Thanks to these events we can request and provide the dependencies through them.</p> <p>If we request them from within the web component at the time of executing the <em>connectedCallback</em> method, and we provide them from another point of the code where we have those dependencies, the injection can be performed without problems.</p> <p>To make this process much easier and simpler at the time of implementation, there are some solutions already implemented in the form of libraries that provide us with everything we need to implement dependency injection without complications.</p> <p>In the documentation of the <a href="https://github.com/Christian24/webcomponents-di">WebComponents-DI</a> library, you can see how it is implemented in detail with examples. Although the operation is basically what has been explained before, from <em>connectedCallback</em> the dependency is requested and from the parent component, the dependency is provided using the synchronous events infrastructure.</p> <h2> <a name="life-of-a-web-component" href="#life-of-a-web-component" class="anchor"> </a> Life of a Web Component </h2> <p>The life of a Web Component goes through different stages, such as definition, construction, connection to the existing structure, and disconnection, among others. These methods are called lifecycles and are detailed below.</p> <h3> <a name="definition-of-custom-element" href="#definition-of-custom-element" class="anchor"> </a> Definition of Custom Element </h3> <p>In order to register the Custom Element, the <em>customElements.define()</em> method is used. This method allows registering a Custom Element that extends an HTMLElement. In order to execute it, some parameters are necessary, the first one is the name, the second one is the class that defines the element and the third one is optional, and it is an object with options that allows extending an already existing Custom Element.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>customElements.define( "custom-button", class CustomButton extends HTMLElement { // ... } ); </code></pre></div> <p></p> <h3> <a name="constructor" href="#constructor" class="anchor"> </a> constructor() </h3> <p>In web components, the constructor is the first method of the lifecycle and is executed once the web component has been initialized. In order to have the properties, events, and methods of the class it extends, HTMLElement, it is necessary to call <em>super().</em><br> </p> <div class="highlight"><pre class="highlight plaintext"><code>constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot.appendChild(template.content.cloneNode(true)); } </code></pre></div> <p></p> <p>Once <em>super</em> is called it is also necessary to join the new element to the Shadow DOM. If it is done as “open” it will be accessible with Javascript, and if it is done as “close”, it will be closed. Once added to the Shadow root you can access its contents or even add a child as in the code snippet above.</p> <h3> <a name="connectedcallback" href="#connectedcallback" class="anchor"> </a> connectedCallback() </h3> <p>This method will be called every time the component is added to the DOM. For example, if it is removed from the DOM but subsequently added again, it is also executed. This method is used to access certain attributes, children, or add listeners.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>connectedCallback() { this.addEventListener("click", this.onclick); } onclick() { console.log("clicked handled"); } } </code></pre></div> <p></p> <h3> <a name="attributechangedcallback" href="#attributechangedcallback" class="anchor"> </a> attributeChangedCallback() </h3> <p>This method is used to receive updates on concrete attributes. To do this, these attributes must first be defined within the static <em>observedAttributes()</em> method. Once defined, the <em>attributeChangedCallback()</em> method will be executed after any attribute modification. This method has three parameters, the first one indicates the name of the modified attribute, the second one indicates the old value and the last one indicates the new value. The attribute is only considered to have been modified if the method <em>this.setAttribute()</em> has been executed.</p> <p>Attributes are stored as serialized data so using getters and setters to deserialize them can be very useful.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>static get observedAttributes() { return ["disabled"]; } attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === "disabled") { this.shadowRoot.getElementById("button").disabled = newVal === "true"; } } set disabled(bool) { this.setAttribute("disabled", bool.toString()); } get disabled() { return this.getAttribute("disabled") === "true"; } </code></pre></div> <p></p> <h3> <a name="adoptedcallback" href="#adoptedcallback" class="anchor"> </a> adoptedCallback() </h3> <p>This method is used to notify that the Web Component has been moved from one document to another. It is only executed when the <em>document.adoptNode()</em> method has been called.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>adoptedCallback() { console.log("moved to a new document"); } </code></pre></div> <p></p> <h3> <a name="disconnectedcallback" href="#disconnectedcallback" class="anchor"> </a> disconnectedCallback() </h3> <p>This method is called only when the web component is removed from the DOM, to notify that it will no longer be displayed. It is normally used to remove listeners and unsubscribe.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>disconnectedCallback() { this.removeEventListener("click", this.onclick); } </code></pre></div> <p></p> <h2> <a name="integration-with-angular" href="#integration-with-angular" class="anchor"> </a> Integration with Angular </h2> <p>One of the strong points of Web Components is the versatility that offers us to integrate them with other frameworks and libraries, for this reason, to be able to see how easy it is to create a Web Component with a known framework, next, we will show a simple example of how to build a Web Component with Angular.</p> <p>In order to show how to create a Web Component with Angular we are going to create an Angular project with a Web Component that will be a todo list to make a very simple todo list and this Web Component is going to receive the name of the todo list as input.</p> <p>The first step is to create the project and a component where the to-do list will be. This component has nothing special or different from a normal Angular component. In addition, as already mentioned, this component will have an input that will be the name of the to-do list. Here is the <a href="https://stackblitz.com/edit/angular-ivy-igacub?file=src/app/todo-list-app/todo-list.component.ts">component</a>.</p> <p>Once the component is created, we have to modify the app.module file to allow exporting the component as a Web Component. For this, among other things, we will use the Angular Elements library, another of the many existing tools to create Web Components. All the actions that have to be done in this file are detailed below.</p> <ul> <li>We remove AppComponent from Bootstrap, because it is no longer necessary, as we are only going to use the project to export the Web Components.</li> <li>We add the created to-do list component as entryComponent.</li> <li>We install the @angular/elements package.</li> <li>Inside the ngDoBootstrap hook of the module, we use the <em>createCustomElement</em> method to compile the component as a standard Web Component.</li> </ul> <p>Here is the <a href="https://stackblitz.com/edit/angular-ivy-igacub?file=src/app/app.module.ts">file</a> to better understand the modifications.</p> <p>And from this moment on, when the project is compiled in the dist directory, the files named, runtime, main, scripts, and polyfills will appear. The Web Component created can be used in any other project.</p> <h2> <a name="tooling" href="#tooling" class="anchor"> </a> Tooling </h2> <p>In terms of tools, Web Components do not fall short either, since there are several options to choose from. To create Web Components in an easy way and so that their integration and maintenance are not a problem, several libraries have emerged to help the developer to be agile in the implementation of this technology.</p> <p>All these tools mainly provide an environment that allows a better development experience and makes development faster. Most of these tools have been used by major brands such as Apple, Porsche, and Amazon, among others.</p> <h3> <a name="stencil" href="#stencil" class="anchor"> </a> Stencil </h3> <p>Stencil.js is a tool created by the Ionic team with the intention of opening to other frameworks because until Stencil appeared Ionic only worked with Angular. Currently thanks to Stencil, Ionic can work seamlessly with React, Vue, or other frameworks.</p> <p>It is essentially a Web Components compiler with Vanilla Javascript, but it is much more. It has some of the best features of other frameworks; for example, to optimize custom elements, it makes use of a virtual DOM like React, and it allows server-side rendering, reactive data binding, or even asynchronous rendering inspired by React Fiber (the new React engine). It also supports typescript and JSX as template engines and can use lazy loading without requiring Webpack.</p> <p>You can read more information on this tool <a href="https://stenciljs.com/">here.</a></p> <h3> <a name="polymer" href="#polymer" class="anchor"> </a> Polymer </h3> <p>Another of the best-known tools for the development of Web Components is Polymer, which was created by Google, initially oriented for the development of internal projects but which finally saw the light of day at the public level. This library provides a series of polyfills (small pieces of Javascript code) that allow Web Components to be natively compatible with most browsers.</p> <p>With this library, we can create Web components easier and faster. Basically, they allow us to make the Web Components that we develop really compatible with most browsers. This allows you to have all of the resources that Web components already have without sacrificing functionality. For more information on this tool, check out the Polymer Project <a href="https://polymer-library.polymer-project.org/">site.</a></p> <p>Want to learn more about technical topics? I invite you to take a look at <a href="https://apiumhub.com/tech-blog-barcelona/">Apiumhub’s blog</a>, useful content in frontend development, backend development, software architecture and more gets published every week.</p>

Oldest comments (0)