Web Components with StencilJS compiler

by Javier Puerto
Tags: Open Source , Web , Web Components

We have created Web Components with X-Tag, Vue.js and Polymer. Now we are going to create Web Components with a new but popular JS tool, StencilJS.

Tools to build something bigger. Building with Web Components. (Foto: pexels.com)

What is StencilJS.

StencilJS is a Web Component compiler that simplifies the creation of cross platform Web Components. As main features we can highlight:

  • Typescript support
  • Asynchronous rendering
  • Fast and tiny virtual DOM
  • Reactive data binding
  • Support of JSX

Transition from popular JS frameworks as React or Angular is pretty easy and the generated Web Components can be used with them too. StencilJS also allows us to create complete web applications based on Web Components.

Creating Web Components with StencilJS

To start building a new Web Component using Stencil, follow the instructions below:

git clone https://github.com/ionic-team/stencil-component-starter.git my-component
cd my-component
git remote rm origin

and run:

npm install
npm start

To build the component for production, run:

npm run build

To run the unit tests for the components, run:

npm test

Creating be-media component

We now have created a base project to start creating our be-media component. The folder structure is like:

├── LICENSE
├── package.json
├── package-lock.json
├── readme.md
├── src
│   ├── components
│   │   └── my-component
│   │       ├── my-component.css
│   │       ├── my-component.e2e.ts
│   │       ├── my-component.spec.ts
│   │       └── my-component.tsx
│   ├── components.d.ts
│   └── index.html
├── stencil.config.ts
└── tsconfig.json

At this point I recommend to run the application with npm start because the compiler will keep watching any changes and it will update the browser's view automatically. Furthermore, any issue or problem with the sources will be visible direclty in the terminal.

  • index.html contains the demo page for component testing.
  • components.d.ts is an autogenerated file by StencilJS that will update automatically when the sources files are modified. It will keep the definitions of the components so we can easily work with any Typescript compatible IDE.
  • components is the directory where we should create our new components. The components consists in a file with the template and the logic and another with the styles. Furthermore we can define our spec and e2e test cases, the first will test the component itself while the other will test the implementation in the browser.
  • stencil.config.js contains information for the StencilJS compiler. The file is not needed but it is very convenient for fine grain configuration of the build process.
  • tsconfig.json is the well know file for Typescript compiler configuration.

Now that we have created a base project, we are going to start customizing the stencil.config.js file to change the namespace. By default the value is set to mycomponent that is not very helpful. As we are BeCompany, we are going to update the namespace to becompany. This value indicates to the compiler the output name of our build. After the change we have to update also the demo page to reflect the changes loading the new becompany.js file from build directory.

We can now safely remove the sample component created and create our be-media directory and component's files instead.

import {Component, Prop} from '@stencil/core';

@Component({
  tag: 'be-media',
  styleUrl: 'be-media.css'
})

export class BeMedia {

  @Prop() src: string;

  @Prop() class: string;

  isVideo() {
    if (this.src.endsWith('.mp4')) {
      return true;
    } else {
      return false;
    }
  }

  render() {
    if (this.isVideo()) {
      return (
        <video class={this.class} autoPlay>
          <source src={this.src} type="video/mp4"/>
        </video>
      );
    } else {
      return (
        <img class={this.class} src={this.src}/>
      );
    }
  }
}

The code is pretty self explanatory but we are going to explain it with more detail. StencilJS uses Typescript as language, therefore we need to import the Component and Prop decorators. With the @Component annotation we indicate to the compiler that the next class found in code will be a Web Component. It accepts several configuration options but tag and styleUrl are the most basic ones and determines the name of the generated Web Component and the url of the associated stylesheet. Inside the BeMedia class, we configure the attributes that our new Web Component will expose. For our usecase they are src to indicate the URL of the resource to load and class to include the styles in it. If you are curious, you can see more information about the StencilJS decorators.

The render() method is called when the component has been initialised or updated and need to be rendered. StencilJS uses JSX templating system to provide the view logic. For our component, the logic is pretty simple, we will render a video tag for URLs that ends with .mp4 extension and an image tag for the other URLs. For the task we have created a simple method isVideo() that will return true or false based in the src attribute value.

We only need to update the demo page and that's all folks! We can see the results in the browser directly.

Creating be-dog and be-cat components

Unfortunately I have found an important limitation while working on this tutorial. It seems that the inheritance is not yet supported in StencilJS, when I tried to extend the be-media component I saw the following output message throwed by the compiler:

Classes decorated with @Component can not extend from a base class. Inherency is temporarily disabled for stencil components.

I hope that the StencilJS team can solve this limitation soon as it could be a problem in some cases. For our demo we can just create a composed implementation so we are going to show how to create the be-dog implementation and be-cat will be the same.

import {Component, Prop, State} from '@stencil/core';

@Component({
  tag: 'be-dog',
  styleUrl: 'be-dog.css'
})

export class BeDog {

  @State() src: string = '';
  @Prop() class: string;

  refresh() {
    return fetch('https://random.dog/woof.json')
      .then(response => {
        if (response.ok) {
          response.json().then(json => {
            this.src = json.url;
          });
        }
      })
      .catch(error => console.error(error));
  }

  componentWillLoad() {
    if (this.src === '') {
      return this.refresh();
    }
  }

  render() {
    return <be-media onClick={() => this.refresh()} src={this.src} class={this.class}></be-media>;
  }
}

Notice that the implementation will use the be-media Web Component previously defined. We use it directly in the render() method.

We also define a method componentWillLoad() that will be called automatically by StencilJS based in their component lifecycle. StencilJS define five states in a component lifecycle:

  1. componentWillLoad() will be called when the component is going to be loaded.
  2. componentDidLoad() will be called when the component has been loaded already.
  3. componentWillUpdate() will be called when the compoenent is about to update and re-render.
  4. componentDidUpdate() will be called when the component has just re-rendered.
  5. componentDidUnload() will be called when the component has been destroyed.

In this case we have created a new method refresh() that will fetch a new random dog resource and update the src state. Notice that now we have used the decorator @State to define the src attribute. The @State attribute defines an internal attribute that will internally be used to re-render the component on change but will not be exposed as a tag attribute.

Using JSX syntax we attach a click event to the be-dog element and associate it with our refresh() method. When the user clicks on the element, it will update the src attribute with a promise and as it is annotated with the @State decorator, a re-render will be triggered.

To fetch the data from the random dog service, instead of use Axios, we use the new Fetch API standard. We can use it directly with StencilJS as it includes polyfill in case that our browser does not support it.

For the be-cat component, we have the same implementation as be-dog component but just changing the service used to feed the be-media src attribute.

Thanks for reading

StencilJS compiler experience was really good except for the inheritance limitation that hopefully will be fixed soon. You can checkout the code for this tutorial as always in our GitHub repository.

As always, I hope you enjoyed and learnt something from this tutorial. If not you may at least had fun with all the dogs and cats. See you in the next blog post.