Skip to content

POC Report: WebComponents

Stéphane Brunner edited this page Nov 10, 2021 · 43 revisions

Introduction

We archive a POC with those libraries:

We choose to write the new components in TypeScript.

We use the Chromatic service to review and test the examples.

Lit element / HTML

The main addition of this POC is the use of Web Components on top of services. For the needs of templating and code structure, we choose write Web-Components using a framework: Lit. This is a small footprint library.

Lit (historically called lit-element) is build around the standards of Web Components.

This is well supported in modern frameworks or even native JavaScript. Using such design insures us to get some interoperability for the future and keep the existing code functioning with the rapid evolution of the front-end ecosystem. "More standards, the better".

Web components and it's Lit implementation consists of three main technologies, which can be used together to create versatile custom elements with encapsulated functionality that can be reused wherever you like without fear of code collisions:

  • Custom elements
  • Shadow DOM
  • HTML Templates

Here is a basic example to create a Web Component using Lit (auth.ts):

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

@customElement('auth')
export class Auth extends LitElement {
  static styles = css`p { color: blue }`;

  @property()
  login = 'Username';

  render() {
    return html`<p>Hello, ${this.login}!</p>`;
  }
}

Then we have a HMTL file (index.html):

<!DOCTYPE html>
<head>
  <script type="module" src="./auth.ts"></script>
</head>
<body>
  <auth login="Someone"></auth>
</body>

Advantage against using HTMLelement in vanilla JavaScript

If we go on this page https://webcomponents.dev/blog/all-the-ways-to-make-a-web-component/ and compare the HTMLelement example and the Lit (new) TypeScript example we can see that the Lit example is a little shorter but not enough to be a good argument, if we see the changes, in the vanilla example we can see:

  <button id="dec">-</button>

...

    this.shadowRoot.getElementById("dec").onclick = () => this.dec();

And in Lit:

      <button @click="${this.dec}">-</button>

With is more convenient for the developer, and it prevents some issues.

We will have the same thing with data-binding.

See: Expression.

We have some available decorator for the properties_

  • @state to define a property that should be listened to trigger a new render of the component.
  • @property state and expose the property an attribute in the HTML, with some converter functions.

The static style is provided in a separate property, and it can be provided as an array, then we can do something like that:

  static style = [
    ...BaseElement.styles,
    css`
      <my custom styles>
    `
  ];

With is also more convenient.

Integration with AngularJS

The attributes are a little bit renamed:

ng-on-close-panel="mainCtrl.ngeoAuthActive = $event.detail" => Code called, then the close-panel event is dispatched from the component.

ng-prop-login_info_message="mainCtrl.loginInfoMessage" => set the property loginInfoMessage of the WebComponent with custom object.

Refer to ngOn and ngProp in the AngularJS documentation for full details.

Store RXJS

A store, not a state. Mainly based on https://rxjs.dev/api/index/class/BehaviorSubject, a multicasted observable with a default value. But There are other "Subject" possibilities.

Works well and without bad surprise. Easy to access, easy to set. Light to implement.

The code must be simple. One set function should emit one thing.

The integrity of a stored value must be well checked by the store.

RXJS offer a lot of operators to play with Observable (see https://rxmarbles.com). From experience, it's better not to use too much of them (the code becomes quickly hard to follow and to change).

Using observable allows use to remove our old events and centralize the state of the application.

I18n

Evaluate i18next and lit-localize

Choice for i18next because with lit-localize we can’t translate text out of the component, and we need to translate some part of the HTML like the page title.

Translate the tag content in the HTML:

<title data-i18n="Alternative Desktop Application">GeoMapFish</title>

Translate a tag attribute in the HTML:

<button data-i18n="[tooltip]Draw and Measure"/>

Translate just a string in the HTML: Not possible

Translate just a string in the JavaScript(WebComponent):

import i18next from 'i18next';
html`${i18next.t("Loading themes, please wait...")}`

Plural in the JavaScript(WebComponent):

import i18next from 'i18next';
const i18nextConf = {count: themes.length};
html`${i18next.t("Loading {{count}} themes, please wait...", i18nextConf)}`

But this can't be used because it breaks the English translating who actually is in the sources... Same issue for context: https://www.i18next.com/translation-function/context

Config

The configuration will be dispatched by RXJS, and be fully typed, see src/state/config.ts.

Usage:

import configuration, {Configuration} from 'ngeo/store/config';

this.subscriptions_.push(
  configuration.getConfig().subscribe({
    next: (configuration: Configuration) => {
      // Use the configuration
    },
  })
);

TypeScript

We choose TypeScript to write the new component as it's largely accepted by lit-element, lit-element itself is written in TypeScript. Other motivation is that we get some limitations in typing in the JSdoc.

Translate JavaScrypt to TypeScript, see: https://github.com/camptocamp/ngeo/blob/poc_2.7/docs/codeshift.md

Service migration

The principle is to have a class instantiate (a single time) at the end of the file. Here is the canvas of a example service:

export class MyService {

  property: string;

  constructor() {
    this.property = 'something';
  }

  someFunction(param: string): string {
    this.property = param;
    return this.property;
  }
}

const ngeoMyService = new MyService();
export default ngeoMyService;

As it is instantiate in the service file itself then exported in a ECMAScript module, we can now import and use it in other files as follow:

import ngeoMyService from 'path/file';

ngeoMyService.someFunction('hello world');

Separate build

TODO: Finish

We provide the following element throw a Singleton available in window, then we can access to the in a separate build. In this way, by default the HTML page will be compiled by the ngeo build (not modifiable in the project).

The states are exposed to be able to use the out of the script (currently: user, config, OpenLayer map).

The goal of the simple application mode is to simplify the migration.

The general idea for the simple application is to completely separate the project and the GeoMapFish code. For the backend by creating a separate container. For the frontend by providing in the config container a JavaScript and a CSS, in the addition of what's already in the vars.yaml in particular the CSS variable.

See: https://github.com/camptocamp/demo_geomapfish/pull/255

The main things that's not possible in this architecture is the authentication (LDAP, ...), and extending the GeoMapFish data model (but it's difficult to maintain). For having a separate class that didn't have to be visible in the admin interface, it can be done in the additional container. But for that, we didn't need to have an advance application we can just extend the image and just override some files.

For the authentication, we can imagine to support SAML 2.0 SSO, and for LDAP we should investigate if it's possible to have a generic implementation.

Component extension

TODO

Example / StoryBook

We will use StoryBook for the new examples. StoryBook is more than providing the example, with that, we can also do test and review with Chromatic It make a little documentation of a web component. It will also a base for the tests.

Tests

Finally, Cypress is implemented. It has a good end-to-end tests as good unit tests possibilities, embeded ui, good cli... and gives a good feeling (Even if the doc is not so clear if you don't use React).

Testing models and services are mandatory.

As we use Storybook, we don't need the (beta) extension to test components, we can use "the standard way to test" on Storybook pages. Using Chromatic, we have a good check on examples. Therefore, testing our component with Cypress is not so useful. But If the evolution of the examples make them more complex, like including a real authentication for the auth component, we should test that with Cypress, via the example. Otherwise, we should use Cypress for more end-to-end tests directly on the apps.

The alternatives would have been Jest, Web Test Runner...

Clone this wiki locally