Make a Video Web Component, the Stencil Way
TL;DR
Stencil is a JavaScript tool that enables you to build framework-independent and standards-compliant web components using technologies, such as TypeScript and JSX.
Stencil provides APIs that makes writing fast components a breeze. These APIs — Virtual DOM, JSX, Async rendering and Reactive data-binding — equip developers with superpowers to create custom web components. Popular JavaScript frameworks, including React, Vue and Angular, all have something in common – the ability to create custom and reusable components. Check out this StencilJS Guide written by Christian Nwamba for a succinct primer on generating pure custom components.
Let’s start our journey by showing how to create a custom web component with Stencil. We’ll use one of my favorite APIs, Cloudinary, as the functional service for which we’ll craft a web component.
What’s Cloudinary?
Cloudinary is a cloud-based, end-to-end media management solution. As a critical part of the developer stack, Cloudinary automates and streamlines your entire media asset workflow. It handles a wide variety of media types, from images, video and audio to emerging rich and interactive media types. Cloudinary’s powerful APIs are used by developers to automate every stage of the media management lifecycle, including media selection, upload, analysis and administration, manipulation optimization and delivery.
Existing Components
Fortunately for developers, Cloudinary offers SDKs for different languages and frameworks, including Ruby on Rails, PHP, Angular, React, JQuery, Android, iOS, Python and JavaScript. Let’s look quickly at the Cloudinary components that exist for a couple of these frameworks: Angular and React.
Angular SDK
<cl-video> component.
<cl-transformation> component.
Repository
Usage:
<cl-video cloud-name="my_other_cloud" public-id="watchme" secure="true" class="my-videos">
<cl-transformation overlay="text:arial_60:watchme" gravity="north" y="20"></cl-transformation>
</cl-video>
React SDK
<Video> component.
<Transformation> component.
Repository
Usage:
<Video publicId="dog" >
<Transformation width="300" height="200" crop="crop" />
</Video>
In September 2017, David Walsh wrote an excellent article on how to create a Cloudinary video component. I skimmed through the blog post and decided to do the same with Stencil.
Stencil Component
Without further ado, we’ll go ahead to build our Stencil component. Let’s start with the component template.
Component Template
Let’s take a look at the JSX for the component. In Stencil, the HTML Skeleton resides in the render method.
render() {
return (
<div onMouseEnter={this.showPreview.bind(this)} onMouseLeave={this.hidePreview.bind(this)} class="cloudinary-video-item" style={{'width':`${this.width}px`,'height':`${this.height}px`}}>
<div class="cloudinary-video-item-active">
<video id="previewVideo" poster={this.poster} autoplay loop width={this.width} height={this.height}></video>
</div>
<div class="cloudinary-video-item-video">
<video id="fullVideo" autoplay controls width={this.width} height={this.height}></video>
</div>
<svg
onClick={this.play.bind(this)}
xmlnsDc="http://purl.org/dc/elements/1.1/"
xmlnsCc="http://creativecommons.org/ns#"
xmlnsRdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlnsSvg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="play-icon"
version="1.1"
height="50"
width="50"
viewBox="0 0 1200 1200">
<path
d="M 600,1200 C 268.65,1200 0,931.35 0,600 0,268.65 268.65,0 600,0 c 331.35,0 600,268.65 600,600 0,331.35 -268.65,600 -600,600 z M 450,300.45 450,899.55 900,600 450,300.45 z"
id="path16995" />
</svg>
</div>
);
}
In the code above, there is a root div that houses three child elements. The first two elements are divs, while the last is an SVG. Next, let’s configure the component and the props it will receive once it’s in use.
Component Properties
import { Element, State, Component, Prop } from '@stencil/core';
@Component({
tag: 'cloudinary-video',
styleUrl: 'cloudinary-video.scss'
})
export class CloudinaryVideo {
@Prop() account: string;
@Prop() width: string;
@Prop() height: string;
@Prop() alias: string;
@Element() videoEl: HTMLElement;
@State() fullVideo: string;
@State() preview: string;
@State() poster: string;
…..
In the code above, we specified the number of properties that a user can pass to the component. account, width, height, and alias. We can specify many more, but for the sake of this article, it will be limited to just four props. To do this we can use the @Prop()
decorator. @Prop()
is a decorator that enables components to explicitly declare props.
- account: Cloudinary account name of the user
- width: Width of the video container
- height: Height of the video container
- alias: Name of the video to be played
Next, we have the state attributes, fullVideo, preview, poster. To do this we can use the @State()
decorator, which enables components to manage data internally.
- fullVideo: URL of the complete video
- preview: URL of the video preview
- poster: URL of the video poster
@Element()
is a decorator that enables the component to get access to the host element within the class instance. Basically, it means we can get access to the component itself and manipulate it and its child elements to achieve whatever we want.
Component Methods
These are the methods that will perform the key operations when certain events trigger in our component. In the component template, we have the onMouseEnter, onMouseLeave and onClick events.
play() {
// Hide the preview
this.hidePreview();
// Set the state to "playing" for showPreview and hidePreview checks
this.videoEl.setAttribute('state', 'playing');
// Set the full video element src
this.videoEl.querySelector('#fullVideo').setAttribute('src', this.fullVideo);
// set the svg play button to disappear
this.videoEl.querySelector('svg').style.display = 'none';
}
showPreview() {
// If the full video is loaded and playing, ignore this event
if(this.videoEl.getAttribute('state') === 'playing') {
return;
}
// set the preview video to the src attribute of the video tag
this.videoEl.querySelector('#previewVideo').setAttribute('src', this.preview);
}
hidePreview() {
// If the full video is loaded and playing, ignore this event
if(this.videoEl.getAttribute('state') === 'playing') {
return;
}
// Set the video to go to the beginning
this.videoEl.querySelector('video').currentTime = 0;
// ..then pause the video
this.videoEl.querySelector('video').pause();
}
We have the play
, showPreview
, and hidePreview
methods.
play
: This method hides the preview, sets the state attribute of the component to playing
, assigns the full video URL to the video element and eliminates the SVG play button at the middle of the video container.
showPreview
: This method assigns the preview video URL to the video element. If the video is currently playing, it does nothing.
hidePreview
: This method resets video time to the beginning of the video and pauses it. If the video is currently playing, it does nothing.
Worthy of note is the Stencil Lifecycle method we invoked in our component.
componentDidLoad() {
this.fullVideo = `http://res.cloudinary.com/${this.account}/video/upload/${this.alias}.mp4`;
this.preview = `http://res.cloudinary.com/${this.account}/video/upload/so_0,du_2/l_video:${this.alias},fl_splice,so_12/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_24/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_36/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_48/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_80/du_2/fl_layer_apply/${this.alias}.mp4`;
this.poster = `http://res.cloudinary.com/${this.account}/video/upload/${this.alias}.jpg`;
}
Add Video Magic
The componentDidLoad
is a lifecycle hook that is invoked when a component is loaded. In this component, we assigned the on-the-fly Cloudinary transformation URLs to the variables above.
The preview video URL looks a little scary. What are all those URL parameters? Oh, that’s one of the superpowers of Cloudinary. Cloudinary encodes and deliver videos on-the-fly and also applies transformations to videos via URLs. In this URL, there is fl_splice
, fl_layer_apply
. These are flags you can use for overlaying videos and concatenating to the container video.
Check out the documentation for several transformation tricks for video.
Component CSS
The CSS for this component is fairly simple.
components/cloudinary-video/cloudinary-video.scss
cloudinary-video {
.cloudinary-video-item {
position: relative;
}
.cloudinary-video-item > div {
position: absolute;
top: 0;
left: 0;
}
.cloudinary-video-item svg {
position: absolute;
top: 40%;
left: 45%;
cursor: pointer;
opacity: 0.6;
}
.cloudinary-video-item svg:hover {
opacity: 0.9;
}
.cloudinary-video-item-active, .cloudinary-video-item-video {
display: block;
}
}
Using the Component
Using the component is as simple as:
<cloudinary-video
account="unicodeveloper"
alias="cartoon"
width="640"
height="360">
</cloudinary-video>
This code can simply be dropped into your HTML file.
The code for the Cloudinary Stencil Video Component is on GitHub.
There are many kinds of components that can be generated with Stencil. Stencil’s API makes it easy to create standards-compliant web components as shown above. With Cloudinary, you can create robust media web components that developers can latch onto for building media-heavy web software. I look forward to seeing you create standards-compliant web components with Stencil. In the next post, you will learn how to build an image gallery with Stencil Custom Components using Cloudinary.
Author Bio:
Prosper Otemuyiwa is a food ninja, open source advocate and self-proclaimed developer evangelist.