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.
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
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:
tag
in the@Component
decorator defines the element tag to use this component in HTML.styleUrl
points to the styles (file) of the component.@Component
decorator wraps the class around and registers as a component for Stencil.@Prop
decorator is used for handling input (attributes data) for the component@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:
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:
- 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>
- We have imported
WatchService
in our component file, so create the watch service in thesrc/services
folder. Now the folder structure should look like this:
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:
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:
- Open
stencil.config.js
and replacemycomponent
withstopwatchbox
. - Open
package.json
and replace all instances ofmycomponent.js
withstopwatchbox.js
. - Remove the script tag with
src="/build/mycomponent.js"
from theindex.html
in thesrc
folder. - Change the
name
property inpackage.json
tostop-watch-box
. - Remove the usage of
my-component
from theindex.html
. - 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:
- Build the component for production by running
npm run build
from the project root. - 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 isstop-watch-box
. - Follow the guidelines from npm docs to create a user if you don’t have it already.
- 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. 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.
Ahsan Ayaz
Related Posts
-
Web Components Introduction
Web Components Introduction Web Components is a standard that is getting really popular these days,…
-
Web Components Introduction
Web Components Introduction Web Components is a standard that is getting really popular these days,…