Building Web Components with Stencil

   JavaScript
Building Web Components with Stencil

Over the past few years, web development standards have evolved so much that many of us have had a hard time catching up. Even now, there’s a new javascript framework being created somewhere in the world that’ll go live in the next few months. Yet keeping up to date is crucial in the software industry and lagging behind is not an option.

As web standards have evolved, many new APIs, features, frameworks and tools have been introduced. Be it ES6, TypeScript, Webpack or libraries like Polymer, Stencil, etc. With evolving standards, one of the prominent targets of web and hybrid-mobile frameworks is to use Web Components.

What is Stencil?

Stencil is an open-source compiler that generates standards-compliant web components. Team Ionic announced Stencil during the Polymer Summit 2017. Stencil’s approach to web components specifically is using Custom Elements. The amazing thing about Stencil is that it works along with the best tools out there for developing amazing apps that can be shipped to production without much hassle. Stencil compiles components into pure web components which can be used in other frameworks like Preact, React, and even with no framework at all.

If you are familiar with React or Angular, you might find yourself at ease while developing apps with Stencil compared to those who are new to JSX and TypeScript.

Why use Stencil?

We can build web components using vanilla JS of course. But Stencil provides some syntactic sugar with TSX (JSX with TypeScript) that makes it a lot easier to build web components with cleaner, reduced code. Stencil’s core API can be used to create components, manage state with the component lifecycle methods, and inputs and outputs to pass attributes into components and emit events from the component respectively.

Here is what a stencil component looks like:

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

@Component({
  tag: 'nice-alert',
  styleUrl: 'nice-alert.scss'
})
export class NiceAlert {
  // input passed to the component
  @Prop() message: string;
  // event emitted from the component
  @Event() alertDismissed: EventEmitter;

  // triggers on `Yes` button click
  yes() {
    this.alertDismissed.emit(true);
  }
  // triggers on `No` button click
  no() {
    this.alertDismissed.emit(false);
  }
  render() {
    return (
      <div>
        <div class="message"> {this.message} </div>
        <div class="actions">
          <button onClick={() => this.yes()}>Yes</button>
          <button onClick={() => this.no()}>No</button>
        </div>
      </div>
    )
  }

Let’s discuss the elements from the core API used in the above component:

  1. tag in the @Component decorator defines the element tag to use this component in HTML.
  2. styleUrl points to the styles (file) of the component.
  3. @Component decorator wraps the class around and registers as a component for Stencil.
  4. @Prop decorator is used for handling input (attributes data) for the component
  5. @Event decorator is used to create event emitters to emit events outside from the component.

Building Web Components with Stencil

We are going to build a stop-watch-box component using Stencil. We will use the stencil-component-starter by the Ionic team to get started. This is a great way to start building web components with Stencil since it provides the tooling and configuration out of the box.

Clone the starter app from the repository:

git clone https://github.com/ionic-team/stencil-component-starter.git stop-watch

Navigate to the folder and install the dependencies:

cd stop-watch
npm install

Remove the repository origin as you’re going to push your code into your own git repository:

git remote rm origin

Run the dev server:

npm start

This should bring up the server at http://localhost:3333/

Creating the StopWatch Component

First, we will create a stop-watch component. Create a folder inside src/components named stop-watch. Create the tsx (TypeScript) and css files for the component. The folder structure should look like this:

Building Web Components using Stencil, Stop Watch Component
stop-watch component



The code below represents the StopWatch component and goes inside the stop-watch.tsx file:

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

@Component({
  tag: "stop-watch",
  styleUrl: "stop-watch.css"
})
export class StopWatchComponent {
  @Prop() hours: string;
  @Prop() minutes: string;
  @Prop() seconds: string;
  @Prop() milliseconds: string;

  render() {
    return (
      <div class="watch-wrapper">
        <div class="watch">
          <div class="unit">{this.hours}</div>
          <div class="sep"> : </div>
          <div class="unit">{this.minutes}</div>
          <div class="sep"> : </div>
          <div class="unit">{this.seconds}</div>
          <div class="sep"> : </div>
          <div class="unit">{this.milliseconds}</div>
        </div>
      </div>
    );
  }
}



The below styles for the component go inside stop-watch.css:

.watch-wrapper {
  background: #2196F3;
  padding: 20px;
  display: block;
  font-family: monospace;
  box-shadow: 0 16px 16px 0 rgba(0,0,0,0.1);
}

.watch{
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-evenly;
  z-index: 2;
}

.watch .unit, .watch .sep{
  font-size: 32px;
  color: #FFEB3B;
}

We have created some markup for the watch component that will display milliseconds, seconds, minutes, and hours. We have used @Prop for the variables so we will be passing these from our container/parent component. Let’s create the parent component now.

Creating the StopWatchBox Component

This component will contain the logic for starting, stopping and resetting the stop watch. For this, we will have a button for each action. To create the component, create a folder named stop-watch-box in the components folder as we did earlier for the stop-watch component. Create the tsx and css files and paste the code snippets from the files below:

import { Component, State } from "@stencil/core";
import { WatchService } from "../../services/watch-service";

@Component({
  tag: "stop-watch-box",
  styleUrl: "stop-watch-box.css"
})
export class StopWatchBoxComponent {
  private hh = 0;
  private mm = 0;
  private ss = 0;
  private ms = 0;
  @State() hours = '00';
  @State() minutes= '00';
  @State() seconds= '00';
  @State() milliseconds= '00';
  timer: any = null;
  @State() isTimerRunning = false;
  watchService = new WatchService();
  /**
   * @author Ahsan Ayaz
   * @desc Starts the timer, updates ever 10 milliseconds
   */
  start() {
    this.isTimerRunning = true;
    this.timer = setInterval(() => {
      this.updateTime();
    }, 10);
  }

  /**
   * @author Ahsan Ayaz
   * @desc Updates the value of the units in for the watchf
   */
  updateTime() {
    this.ms++;
    if (this.ms >= 100) {
      this.ms = 0;
      this.ss++;
      if (this.ss >= 60) {
        this.ss = 0;
        this.mm++;
        if (this.mm >= 60) {
          this.mm = 0;
          this.hh++;
        }
      }
    }
    this.setTime();
  }

  /**
   * @author Ahsan Ayaz
   * @desc Updates the time for the watch component.
   * Applies the detected changes.
   */
  setTime() {
    this.hours = this.watchService.getTimeString(this.hh);
    this.minutes = this.watchService.getTimeString(this.mm);
    this.seconds = this.watchService.getTimeString(this.ss);
    this.milliseconds = this.watchService.getTimeString(this.ms);
  }

  /**
   * @author Ahsan Ayaz
   * @desc Stops the watch.
   */
  stop() {
    this.isTimerRunning = false;
    clearInterval(this.timer);
  }

  /**
   * @author Ahsan Ayaz
   * @desc Clears the time of the watch.
   */
  clear() {
    this.hh = 0;
    this.mm = 0;
    this.ss = 0;
    this.ms = 0;
    this.setTime();
  }

  render() {
    return (
      <div class="watch-box">
        <div class="watch-container">
          <stop-watch hours={this.hours} minutes={this.minutes} seconds={this.seconds} milliseconds={this.milliseconds}></stop-watch>
        </div>
        <div class="actions-container">
          <button onClick={ () => this.start()} disabled={this.isTimerRunning}>Start</button>
          <button onClick={ () => this.stop()} disabled={!this.isTimerRunning}>Stop</button>
          <button onClick={ () => this.clear()} disabled={this.isTimerRunning}>Clear</button>
        </div>
      </div>
    );
  }
}


.watch-box {
  display: block;
  height: 300px;
  width: 300px;
  margin: 0 auto;
  padding-top: 20px;
}

.watch-box .watch-container {
  padding: 20px;
  font-family: system-ui;
}

.watch-box .actions-container {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-evenly;
}

.watch-box .actions-container button {
  border-width: 0;
  padding: 10px;
  outline: none;
  border-radius: 2px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
  background-color: #333;
  color: #ecf0f1;
  cursor: pointer;
}

.watch-box .actions-container button:hover {
  background-color: #555;
}

.watch-box .actions-container button[disabled] {
  background-color: #dcdcdc;
  color: black;
  opacity: 0.3;
  cursor: not-allowed;
}

Notice that we are using @State for making sure that our updated variables get rendered. The difference between @State and @Prop is that the view is not re-rendered if something changes in a @Prop. But if any of the @State models change, the view is rendered again.

While the code (tsx) might seem self-explanatory, there are two important things to note:

  1. We’re passing the state properties to the stop-watch component as:
     <stop-watch hours={this.hours} minutes={this.minutes} seconds={this.seconds} milliseconds={this.milliseconds}></stop-watch>
    
  2. We have imported WatchService in our component file, so create the watch service in the src/services folder. Now the folder structure should look like this:

    Building Web Components using Stencil, WatchService Structure

The code below for the WatchService goes inside watch-service.ts:

export class WatchService {
  /**
   * @author Ahsan Ayaz
   * @desc Calculates the units and sets in string format.
   * @param unit value of the unit in numbers
   * @return {string} the string representation of the unit's value with at least 2 digits
   */
  getTimeString(unit: number): string {
    return (unit ? (unit > 9 ? unit : "0" + unit) : "00").toString();
  }
}

To see it working, open the index.html file and add the below script tag inside the head tag:

<script src="/build/stopwatchbox.js"></script>

Next, use the component inside the body tag as below:

<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
  <!-- using the stop watch box component -->
  <stop-watch-box></stop-watch-box>

The stop watch box should be working as follows:

Building Web Components with Stencil, Stop Watch



You may notice that there are some errors on the console saying GET http://localhost:3333/build/stopwatchbox.js 404 (Not Found).

By default, the component starter targets the mycomponent.js file since it uses my-component. We need to use the stop-watch-box component and target stopwatchbox.js. To do that, remove the my-component folder and its file and set the stop-watch component as the main component for the build. Follow the steps below:

  1. Open stencil.config.js and replace mycomponent with stopwatchbox.
  2. Open package.json and replace all instances of mycomponent.js with stopwatchbox.js.
  3. Remove the script tag with src="/build/mycomponent.js" from the index.html in the src folder.
  4. Change the name property in package.json to stop-watch-box.
  5. Remove the usage of my-component from the index.html.
  6. Stop and restart the dev server. The errors should be gone now.

Distributing the Component

To distribute the component, We are going to publish this on npm. If you’re writing your own components, you can publish them on npm as well. To do that, follow the steps below:

  1. Build the component for production by running npm run build from the project root.
  2. Once the build is done, we need to publish it. Make sure the name property of the package represents your component’s name. In my case, it is stop-watch-box.
  3. Follow the guidelines from npm docs to create a user if you don’t have it already.
  4. Publish the package by running npm publish --access=public

Viola! We have our web component published and we can now use it in any framework or with no framework at all. And this is not the end. We could still add features to it like adding laps and perhaps changing the clock’s background and text colors after each lap. The possibilities are limitless! Do try Stencil to build your own web components and paste the demo links in the comments. We can’t wait to see what you build!

What’s next?

Building a web component was a breeze for me using Stencil and I could get it completed and published within a few hours. Check out the code from this repository, and then view the demo here. It also contains the hover effect implementation.

Make sure to check out Stencil‘s docs for building more complex web components. The docs have information on state management, events, methods, forms and almost everything that should get you started.

While Stencil provides a great way to build web components, there are other choices as well. For example, StakeJS, Polymer, and other libraries as well as frameworks like Angular and Vue are also on the way to allow developers to build Web Components.


This website uses cookies

These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Please consent to the use of cookies before continuing to browse our site.

Like What You See?

Got any questions?


>