By combining a lot of concepts from well known frameworks such as Angular and React, Stencil JS allows us to compile and create native web components using plain JavaScript and, in the process, build amazing stuff. In this tutorial, we will build a realtime score board component with Stencil JS and Pusher. With us to aid in this task are some developer tools:

  • Stencil JS: A compiler which generates Web Components and combines the best concepts of the most popular frameworks into a simple build-time tool.

  • Pusher: A free, realtime, easy to use pub/sub service. Pusher makes realtime as easy as using basic events. You will need to sign up for a free account to get started.

  • Bulma: A free and open source CSS framework based on Flexbox.

  • dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.

Here is a glimpse of what we will be working on:

The app is going to have a very simple server that emits Pusher events at intervals.

Setting up our Stencil Project

Ensuring that we already have npm installed, we execute the following commands in our command line:

    # Clone the Github repo
    git clone https://github.com/ionic-team/stencil-starter.git real-time-scoreboard-app 

    # Move into the repo
    cd real-time-scoreboard-app

    # Remove original remote URL
    git remote rm origin

    # Install npm dependencies
    npm install

    # install Pusher (client & server) SDK as well as dotevn
    npm install pusher-js pusher dotenv --save

When we are done, our project folder should look like this:

In our folder, we should have an index.html file. Empty the file and replace it with the following markup which includes Bulma’s CDN for our scoreboard app styling.

    <!DOCTYPE html>
    <html dir="ltr" lang="en">
    <head>
      <meta charset="utf-8">
      <title>Stencil Starter App</title>
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
      <meta name="theme-color" content="#16161d">
      <meta name="apple-mobile-web-app-capable" content="yes">

        <!-- Include Bulma -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css">
      <script src="/build/app.js"></script>
      <link rel="apple-touch-icon" href="/assets/icon/icon.png">
      <link rel="icon" type="image/x-icon" href="/assets/icon/favicon.ico">
      <link rel="manifest" href="/manifest.json">
    </head>
    <body>

      <app-site></app-site>     <!-- our app template in the DOM -->
    </body>
    </html>

Creating our Scoreboard Component

To create our scoreboard component, we create a new file and name it app-site.tsx. We place it in the src/components/app-site directory. In our app-site.tsx file, we insert the following code:

    import { Component, State } from '@stencil/core';
    @Component({
      tag: 'app-site',
      styleUrl: 'app-site.scss'
    })
    export class AppSite {

      @State() finished = false;
      @State() scores: Score = {
        arsenal: 0,
        chelsea: 0
      }

      render() {
        console.log(this.scores);
        return (
          <div class="hero is-info is-fullheight">
            <div class="hero-body">
              <div class="container ">
                {this.finished && <h1 class="has-text-centered title">Game Over!!!</h1>}
                <div class="level">
                  <div class="level-item has-text-centered" />
                  <div class="level-item has-text-centered">
                    <div>
                      <p class="title is-1">{this.scores.arsenal}</p>
                      <p class="title">Arsenal</p>
                    </div>
                  </div>
                  <div class="level-item has-text-centered">
                    <div>
                      <p class="title is-1">:</p>
                    </div>
                  </div>
                  <div class="level-item has-text-centered">
                    <div>
                      <p class="title is-1">{this.scores.chelsea}</p>
                      <p class="title">Chelsea</p>
                    </div>
                  </div>
                  <div class="level-item has-text-centered" />
                </div>
              </div>
            </div>
          </div>
        );
      }
    }
    interface Score {
      arsenal: number;
      chelsea: number;
    }

In the component above, we import the Component and State decorators from the Stencil JS core library. The Component decorator is responsible for configuring our scoreboard class component, AppSite. It configures the selector’s name (app-site) which is used to mount the component in the entry index.html:

    <body>
      <app-site></app-site>     <!-- our app template in the DOM -->
    </body>

The State decorator is responsible for managing the state of the scoreboard component. The component is rendered using the render function.

To style our component, create a file called app-site.scss, add our style to it and insert it in our src/components directory:

    .is-1 { 
      font-size: 10rem !important; 
    }

The component is linked to this style using the Component decorator’s object through the styleUrl property.

Start the app with npm start via your terminal and you should get the following at localhost, port 3333:

Realtime updates with Pusher

For realtime updates to our scoreboard, we are going to integrate Pusher. Pusher is a simple cloud based API for implementing easy and secure realtime functionalities on web and mobile apps.

To integrate Pusher, we need to install Pusher on both the client side and on the server side.

Installing Pusher on the Client

Start with including the Pusher Client Library in our index.html file:

    <head>
    ...
    <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
    ...
    </head>

Update the componentDidLoad lifecycle method to establish our connection with Pusher via a new Pusher instance:

    import { Component, State } from '@stencil/core';
    import Pusher from 'pusher-js';
    declare var Pusher: any
    @Component({
      tag: 'app-site',
      styleUrl: 'app-site.scss'
    })
    export class AppSite {

      @State() finished = false;
      @State() scores: Score = {
        arsenal: 0,
        chelsea: 0
      }
      componentDidLoad() {
        console.log('cs')
        var pusher = new Pusher('app-key', {
          cluster: 'eu',
          encrypted: true
        });
        var channel = pusher.subscribe('soccer');
        channel.bind('scores', (data) => {
          console.log(data)
          if(data === 'Match End') {
            console.log(data)
            this.finished = true;
            return;
          }
          this.scores = {...this.scores, ...data};
          console.log(this.scores);
        });
      }
      render() {
        return (
          <div class="hero is-info is-fullheight">
            ...
          </div>
        );
      }
    }
    interface Score {
      arsenal: number;
      chelsea: number;
    }

In the instance, we insert the free API key we get when signing up with Pusher and also set encrypted to true to ensure the traffic connection is encrypted.

Next, we subscribe to a soccer channel which could have multiple soccer related events. We then attach a scores event to listen to incoming data from the server.

Emitting Events from the Server

Create a server.js file then import and initialize Pusher:

    require('dotenv').config();
    const Pusher = require('pusher');
    const { PUSHER_APP_ID, PUSHER_KEY, PUSHER_SECRET, TIME } = process.env;
    const pusher = new Pusher({
      appId: PUSHER_APP_ID,
      key: PUSHER_KEY,
      secret: PUSHER_SECRET,
      cluster: 'eu',
      encrypted: true
    });

We are retrieving the Pusher credentials from the environment variables. You can do this using a .env file after configuring dotenv as seen above. Update the .env file with your Pusher credentials:

    PUSHER_APP_ID=APP-ID 
    PUSHER_KEY=APP-KEY 
    PUSHER_SECRET=APP-SECRET 
    TIME=5000

The TIME key is used in determining the interval at which the events are emitted.

Next, setup a setInterval logic to emit the scores of a match as object at every interval based on the TIME variable:

    let multiplier = 1;
    const interval = setInterval(() => {
      const scores = {
        1: {chelsea: 0, arsenal: 1},
        2: {chelsea: 1, arsenal: 1},
        3: {chelsea: 2, arsenal: 1},
        4: {chelsea: 2, arsenal: 2},
        5: {chelsea: 3, arsenal: 2},
        6: 'Match End'
      }

      multiplier = multiplier + 1;
      const scoreId = multiplier-1;
      if (multiplier > 6) {
        console.log('clearing');
        clearInterval(interval);
      }

      console.log(scores[scoreId]);
      pusher.trigger('soccer', 'scores', scores[scoreId]);
    }, multiplier * TIME);

The most significant part is using pusher to emit a score payload to the scores event via the soccer channel.

You can start the server by running the following command:

    node server.js

You should see the client updating with emitted values:

Conclusion

It’s really exciting to see where the web is headed. The ability to create shareable web components and build realtime apps such as this with Pusher shows how far the limit can be pushed with web and mobile development. The concepts you have seen in this example is applicable also to server monitoring, shopping counters, admin dashboards, etc. Should you want to add other features to this app, you can check it out on GitHub. You can take a look at Pusher’s documentation as well as that of Stencil’s while you’re at it.

About Chris Nwamba

Chris is a JavaScript preacher. He also strives to make something out of other languages. Tech Writer. Dev Evangelist. Speaker.