Ex16: Using Web Components

In this example you will learn how to write Web Components style widgets for adapters.

We will be using the LitElement library as an example. The library has different pulgins (lit-plugin, lit-html) for formating and highlighting syntaxis in the Visual Studio Code.

You can download and install them before starting the tutorial, or can find analogies in your favourite IDE.

The initial code for this exmple is stored in this branch: ex16-web-components-exercise. You can pull it to go through the lesson.

Introduction to Web Components#

Web Components - is a way of creating reusable custom HTML-elements while encapsulating their logic and isolating CSS-styles. This method usually presumes the use of the following specifications:

  • Custom Elements allows the creation of custom HTML-elements with their own tag attribution. Technically, every custom element is a successor of the HTMLElement class that's declared in the web-page with the use of the *window.customElements.define('my-custom-element', MyCustomElement) function. After declaration the element becomes available for reuse in DOM, the same as normal HTML-elements <my-custom-element />.

  • Shadow DOM provides an isolation of CSS component styles from global styles of the parent web-page. It works in two modes (open and closed), that define whether the parent web-page has access to the component's content or not. Shadow DOM doesn't use JavaScript-context, which allows it to use general link type components between the parent page and the web-component.

  • HTML Templates are special HTML-elements under the <template> tag. They provide a convenient opportunity to clone the template's content into a new element, by using the template.content.cloneNode(true) function.

This approach integrates into the adapter architecture perfectly. It enables the creation of widgets by using any compatible libraries, linters, code formatting and other instruments that facilitate the development process.

Creating a widget#

Prerequisites#

  1. Clone the project template from this branch: ex16-web-components-exercise
  2. Install npm i dependencies. (this example differs from others by having a librarylit)
  3. Run the project npm start
  4. Add development servers to the extension with the following addresses:
    • http://localhost:3001/dapplet.json (dapplet)
    • http://localhost:3002/dapplet.json (adapter)
  5. On a Google search result activate the dapplet Example 16.

LP: 1. Import dependencies#

First let's import to the ./adapter/src/button.ts the necessary elements from the library Lit

import { LitElement, html, css } from 'lit';
import { property } from 'lit/decorators.js';

LitElement - is a basic class which we will be inheriting from. It expands the native HTMLElement and hides the extra template rendering, style application, and reactive state change logic.

html and css - are Tagged Template Literals, which you can use to write HTML and CSS component code. The code inside literals can be highlighted and automatically formatted with the use of the according plugins, which makes the development more convenient.

property - is a decorator. It can be used to make the properties of the component reactive and publically available to change from the outside. These properties will rerender the component when their values are altered.

Read more about other Lit possibilities in its official documentation.

LP: 2. Declare custom element class#

Inherit the widget class from the LitElement class, and implement the interface with public properties IButtonProps.

export class Button extends LitElement implements IButtonProps {
}

Most likey, your IDE will not like that unrealized class properties are missing. Do not worry, we will do this below.

LP: 3. Declare a map between contexts and insertion points#

Widgets can work in different contexts. The contexts can have various insertion points. The compliance between them must be declared transparently.

public static contextInsPoints = {
SEARCH_RESULT: 'SEARCH_RESULT',
};

LP: 4. Implement IButtonProps interface#

Add the properties that will be available to the dapplets developer. Every property is marked with a decorator @property, this makes them reactive.

1@property() state; // required
2@property() ctx; // required
3@property() insPointName: string; // required
4@property() img: string;
5@property() label: string;
6@property() loading: boolean;
7@property() disabled: boolean;
8@property() hidden: boolean;
9@property() tooltip: string;
10@property() isActive: boolean;
11@property() exec: (ctx: any, me: IButtonProps) => void;
12@property() init: (ctx: any, me: IButtonProps) => void;

Please be aware that there are several required properties that must be realized in your widget. They are system properties, which are defined by the Dynamic Adapter.

LP: 5. Add init callback#

All widgets are provided with an init hook so that dapplets developers can do something the moment a new context appears. This moment needs to be realized transparently.

1connectedCallback() {
2 super.connectedCallback();
3 this.init?.(this.ctx, this.state);
4}

An overload of the connectedCallback() method in LitElement allows us to subscribe to the component rendering event. We will use this.

LP: 6. Add a click handler function#

A dapplets developer should have the possibility to subscribe to the button click event. Beforehand, we will add a click handler with a callback from the exec property.

1private _clickHandler(e) {
2 this.exec?.(this.ctx, this.state);
3 e.stopPropagation();
4}

LP: 7. Write the HTML code of the widget#

Let's begin writing the HTML-template of the component.

The render() method is very similar to a method with the same name in a class-based approach to writing React.js components. Here you can also return null when it is not necessary to render the component. However, instead of JSX we use Tagged Template Literal - html.

1override render() {
2 if (this.hidden) return null;
3
4 return html`
5 <div
6 @click=${this._clickHandler}
7 class="dapplet-widget-results"
8 title="${this.tooltip}"
9 >
10 <img src="${this.img}" />
11 <div>${this.label}</div>
12 </div>
13 `;
14}

Learn more about all template possibilities in the official Lit documentation. There you can find information about using conditions in rendering, iterators, events, etc.

You can also look at an example of a complex widget from the Twitter Adapter.

LP: 8. Style the widget by CSS#

Declaration of CSS-styles is very similar to the approach used in the styled-components library, famous among React.js developers. Here the Tagged Template Literal - css function is used again.

1public static override styles = css`
2 .dapplet-widget-results {
3 display: flex;
4 align-items: center;
5 cursor: pointer;
6 }
7 .dapplet-widget-results > img {
8 width: 20px;
9 margin-right: 1em;
10 margin-bottom: 3px;
11 }
12 .dapplet-widget-results > div {
13 display: inline-block;
14 font-size: 1.1em;
15 color: #f5504a;
16 font-weight: bold;
17 }
18
19 .dapplet-widget-results:hover {
20 text-decoration: underline;
21 text-decoration-color: #f5504a;
22 }
23 :host {
24 border: 1px solid rgb(170, 170, 170);
25 display: table;
26 padding: 2px 10px;
27 border-radius: 4px;
28 }
29`;

Remeber that your Web Components are rendered inside Shadow DOM, which provides an isolation of the styles. This means that global styles of the parent web-page will not be able to redefine internal styles of your component and you will not be able to use them again.

Inside Shadow DOM you can style the shadow root element with the use of a CSS pseudo-class :host(). Sometimes this is useful for seamless website integration.

Pseudo-classes like :hover are also available here. You can use all of CSS power to style your components.

Final Result#

Together we have realized a button, that will be inserted into search results on the Google search page.

The final solution is available in this branch: ex16-web-components-solution.

The final initial code of the button is available here.