Stencil @Element

Accessing and Adding Styles to the Host Element in Stencil



·

In the previous tutorial, we rebuilt a component that was originally designed with Angular. However, instead of using Angular to build it we used Stencil instead to create a generic web component that could work anywhere.

Since we were no longer using a framework like Angular, we had to make a couple of changes to the component, but it still ended up functioning mostly the same. One feature we did leave out is the animated bar that indicates how long the flash message will be displayed for. We are going to add that now:

Stencil Time Bar

In the Angular version, this bar was its own component that was then added inside of the flash message component. We could also do this inside of our own Stencil application – we could just have two separate components and use them together – but in the interest of creating a single self-contained component we are just going to extend the flash component we built with Stencil to include this functionality.

In this tutorial, we will be walking through building this animated time bar in Stencil, and along the way we are going to learn how to use the @Element decorator to access the host element of the web component.

Before We Get Started

This tutorial assumes a basic level of knowledge about Stencil. You should already be reasonably comfortable with what the purpose of Stencil is and the basic syntax. If you are not yet familiar with Stencil, this is a good place to start.

NOTE: This tutorial is a continuation of Using Stencil to Create a Custom Web Component with a Public API Method. If you want to follow along step-by-step you will need to have complete that tutorial first.

Modify the Template

In order to add the time bar to the component, we are going to have to make a couple of modifications to the template. We will add those and then talk through the changes.

Modify the render() function in src/components/my-flash/my-flash.tsx to reflect the following:

  render() {
    return (
      <div onClick={() => this.dismiss()} class={'flash-container ' + (this.active ? 'show ' : 'hide ') + this.activeClass}>
        
        <div class="message">
          {this.message}
        </div>

        <p class="dismiss">tap to dismiss</p>

        <div class="time-bar-container">
          <div class="time-bar"></div>
        </div>

      </div>
    );
  }

We have just added a time-bar-container and a time-bar. The concept of the time bar is reasonably simple, the time-bar element just animates between 100% and 0% in order to achieve the effect. In order for it to display properly, we also need to add a couple of new styles.

Add the following styles to src/components/my-flash/my-flash.css:

.time-bar-container {
    position: absolute;
    top: 0;
    width: 100%;
    height: 8px;
}

.time-bar {
    height: 100%;
    width: 100%;
    background-color: #fff;
}

We will be using a CSS transition to animate the change, but since the time of the transition is going to depend on the duration that the user supplies for the flash message, we will need to add the transition programmatically.

Animating the Time Bar

As I just mentioned, in order to animate the time bar we need to make some style changes programmatically. This means we need to get a reference to the time bar elements, and then add styles to them with JavaScript. In order to do this, we will be making use of the @Element decorator available in Stencil.

Modify src/components/my-flash/my-flash.tsx to reflect the following:

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

@Component({
  tag: 'my-flash',
  styleUrl: 'my-flash.css'
})
export class MyFlash {

  @Element() flashElement: HTMLElement;

  @State() active: boolean = false;
  @State() message: string;
  @State() activeClass: string = 'primary';

  private timeout: any;
  private timeBar: HTMLElement;

  componentDidLoad(){
    this.timeBar = this.flashElement.querySelector('.time-bar');
  }

  @Method() show(message: string, activeClass: string, duration: number): void {

    this.message = message;
    this.activeClass = activeClass;
    this.active = true;

    this.timeBar.style.opacity = '0.3';
    this.timeBar.style.transition = 'width ' + duration + 'ms linear';
    this.timeBar.style.width = '0%';

    this.timeout = setTimeout(() => {
      this.dismiss();
    }, duration)

  }

  dismiss(){
    this.active = false;
    this.timeBar.style.opacity = '0';
    this.timeBar.style.transition = 'none';
    this.timeBar.style.width = '100%';
    clearTimeout(this.timeout);
  }

  render() {
    return (
      <div onClick={() => this.dismiss()} class={'flash-container ' + (this.active ? 'show ' : 'hide ') + this.activeClass}>

        <div class="message">
          {this.message}
        </div>

        <p class="dismiss">tap to dismiss</p>

        <div class="time-bar-container">
          <div class="time-bar"></div>
        </div>

      </div>
    );
  }
}

Most of this code is the same as before, but there are a few important changes. We are setting up a class member using the @Element decorator as follows:

@Element() flashElement: HTMLElement;

This will set up a class member so that we can access the host element using this.flashElement throughout the class. When using this component, we would add <my-flash></my-flash> somewhere in our code, and this is what the host element is (basically, the element that contains the rest of the web component).

Then in the componentDidLoad lifecycle hook, we set up a reference to the time-bar element that is inside of our component’s template. We do this by using a querySelector on the host element. Basically, we are saying “find an element with a class of time-bar inside of the host element”. The benefit to referencing the host element here rather than using something like document.getElementById, is that everything remains scoped to our web component. If we do something like, theHostElement.querySelector we know that we are only going to get elements inside of our web component.

Inside of the show method, we modify some styles on the time bar. We set the transition duration to the duration that is supplied to the method, and we also modify the width and opacity. In the dismiss function we “reset” these values. It is important that we set the transition to none because otherwise, the time bar has to “recharge” its way back to 100% width over the supplied duration. If you were to then trigger another message before that finished, the bar would not yet be at 100% width. It is also important that we set the opacity of the bar to 0 – since the time bar slowly fades out when it is dismissed, if the time bar remains visible you will see it go back to full width before the flash message is completely dismissed (which looks kind of silly).

Many of my readers will have a background in Angular, and the stuff we are doing here is exactly the kind of thing you should avoid when using Angular. In Angular, you should not modify DOM properties directly like we are doing here. Angular is supposed to be platform agnostic, and so it is best to let Angular decide how best to perform certain actions. However, we aren’t building an application to run on a particular framework, we are building web components for the browser and so it makes sense to use methods that are specific to the browser.

If you test out the code now, you should have something that looks like this:

Stencil Time Bar

Summary

Again, most of the code is very similar to the code that we used for the Angular component, we just don’t have the Angular framework “stuff” to build with, so we use plain JavaScript methods instead. The main benefit to having built this component as a generic web component is that we can now use it just about anywhere (in any framework, or without using a framework) just by dropping in <my-flash></my-flash>.

However, there is still the matter of how to make the web component available to your project (rather than it just being available in the project you have just built it in). In a future tutorial, I will cover how we could package up this web component and make it installable as a standalone web component through something like npm.

What to watch next...