Getting started with Web Components building a Video Chat widget

June 06, 2016
Written by
Phil Nash
Twilion

Component based UI libraries are a popular way of building modern applications. Angular and React are the heavyweights at the moment, but the humble browser and its native APIs are never far behind. Web Components were first introduced in 2011 and are the browsers’ attempt to bring componentisation to the web platform.

There are a few libraries available for writing Web Components, most notably Google’s Polymer, but also X-Tag and Bosonic. To really get a grip on what the platform can achieve on it’s own, I’m going to show you how to build a Web Component using the APIs available in browsers today. There are many “hello world” examples of Web Components, so we’re going to build something a bit trickier today, a video chat widget using Twilio Video. By the end of the post it will look a bit like this:

A video chat between Phil and Marcos

More importantly, once we’ve written our component, using it will only need the following HTML:

<link rel="import" href="/twilio-video.html">

<twilio-video identity="phil"></twilio-video>

So what are Web Components anyway?

Web Components are made up of four complementary browser capabilities, Custom Elements, HTML Imports, Shadow DOM and Templates. Together, they can create reusable user interface widgets that encapsulate a behaviour and can be used, as displayed above, by importing the component and placing a custom element in an HTML page. We’ll see how these technologies fit together as we build up our video chat component.

If you just want to see the completed component and how to use it, check out the repo on GitHub. Otherwise, let’s get building.

Tools for the job

To build our video chat component, we’re going to need the following:

Got all that? Let’s get started then.

Setting up our server

To get this project going, I’ve built up a basic server which you’ll need to get running. First of all, clone or download the repo.

$ git clone https://github.com/philnash/twilio-video-chat-web-component.git
$ cd twilio-video-chat-web-component

Once you’ve done that, you’re going to need some Twilio credentials so that the application works. Grab your Twilio Account SID from your account dashboard, generate a Video Configuration Profile and take note of the SID and finally generate an API Key and Secret. Once you have all of those we can add them to the project.

Make a copy of the .env.example file and call it .env. You can do this in the terminal like this:

$ cp .env.example .env

Open up .env and fill in all those credentials.

Now, install the dependencies for the project and start the server.

$ npm install
$ node index.js

The app is now running on localhost:3000. Open it in Chrome and check out the page we have to work with.

A blank page is shown. It may be blank, but it&#39;s a start.

Not much going on at the moment, right? Let’s change that; let’s build a Web Component.

HTML Imports

When you build a component, it lives in its own file and can be imported. This keeps all the code encapsulated nicely. Let’s start our component by creating the HTML import for it.

In the public directory, create a file called twilio-video.html. It doesn’t need anything in it just yet.

Now, in public/index.html add the following at the bottom of the element:

  <link href="/css/app.css" rel="stylesheet">
  <link rel="import" href="/twilio-video.html">
</head>

Refresh the page and nothing new happens! Well, that’s not strictly true. Open up the dev tools console on the Network tab (Cmd + Opt + I on a Mac or Ctrl + Shift + I on Windows) and you’ll see that we loaded our new HTML document.

To really get the most out of our HTML import let’s build our component in it.

Custom Elements

Our aim is to make creating a video chat as easy as dropping an HTML element onto the page, the element. Let’s add it to index.html now. Replace the

on the page with:

<twilio-video identity="phil"></twilio-video>

Refresh the page and the title has disappeared, replaced with our custom element. Except that element doesn’t do anything… yet. Let’s go into our HTML import and start building our custom element.

The first thing we need to do here is to register our element. To do this we need to write some JavaScript. In the twilio-video.html file write:

<script>
  var TwilioVideoPrototype = Object.create(HTMLElement.prototype);
  document.registerElement("twilio-video", {
    prototype: TwilioVideoPrototype
  });
</script>

document.registerElement takes the name of the element we want to register, in this case “twilio-video”. Note, we need to use a name with a hyphen in, this separates custom elements from browser defined elements.

It also takes an optional object of options. This object defines what our custom element is based on. We’re using prototype and we supply a new prototype based on the HTMLElement prototype, the base object for all HTML elements. We’ll be extending this prototype as we go on with this project.

Reload the page and… nothing else happens. All we’ve done so far is create a blank element. By default it is an inline element with no content and no behaviour. Using the prototype we defined earlier, we can add both content and behaviour. But first we need to learn about custom element lifecycles.

The lifecycle of a custom element

Custom elements have a number of functions that get called throughout their lifecycle. These callbacks are useful for adding and removing behaviour, markup and content. They are:

  • createdCallback for when an instance of the element is created
  •  attachedCallback for when the instance is inserted into the document
  • detachedCallback for when the instance is removed from the document
  • attributeChangedCallback for when an attribute on the element is updated

We’re going to use the createdCallback to define the contents and behaviour of the element and the detachedCallback to tear things down again.

To get into the contents of our element, we need to learn about the last two facets of Web Components.

Templates

It might be tempting to generate the HTML contents of our custom element in JavaScript, but in reality that’s unwieldy and there is a better way. HTML Templates are inert pieces of HTML that can be included onto a page and then instantiated in JavaScript. They are inert because inside of a template <script>s won’t execute, <link>s and resources, like <img>s, won’t be fetched until the template is instantiated.

We’ll set up the content that we need in our element with a template. Then, when we receive the createdCallback lifecycle function, we pour in the content. We start with a <template> element at the top of our twilio-video.html file.

<template id="twilio-video-template">
  <style type="text/css">
  :host {
    display: block;
  }
  #picture-in-picture {
    position: relative;
    width: 400px;
    height: 300px;
  }
  #caller {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
  }
  #caller video,
  #me video {
    width: 100%;
  }
  #me {
    position: absolute;
    width: 25%;
    bottom: 5%;
    right: 5%;
  }
  #hangup {
    position: absolute;
    bottom: 5%;
    left: 5%;
  }
  </style>

  <div id="picture-in-picture">
    <div id="caller"></div>
    <div id="me"></div>
    <button id="hangup">Hangup</button>
  </div>
</template>

The styles within the template will all be scoped to the element itself. Now we have our HTML, let’s add it to our custom element.

<script>
  var TwilioVideoPrototype = Object.create(HTMLElement.prototype);
  var importDoc = document.currentScript.ownerDocument;

  TwilioVideoPrototype.createdCallback = function() {
    var template = importDoc.getElementById('twilio-video-template');
    var clone = importDoc.importNode(template.content, true);
    this.appendChild(clone);
  }

  document.registerElement("twilio-video", {
    prototype: TwilioVideoPrototype
  });
</script>

We’ve used a couple of new things here.

document.currentScript.ownerDocument refers to the HTML file we’re writing this all in, rather than the document that imports it. This way we can refer to elements within our file, such as the template.

Then, using that document, we get a reference to our template and clone it using importNode which creates a clone of the HTML in the template. We clone so that the original HTML remains in the template. We then append that clone to our custom element.

Refresh the application and check the inspector.

When you inspect the custom element, you can see all its internals. Not what we want.

Our custom element now has some contents. This is a bit messy though, those contents are accessible from the rest of the page. We don’t really want that to happen, the beauty of Web Components is that we can encapsulate all the behaviour. So we turn to our final feature of Web Components, the Shadow DOM.

Shadow DOM

The best way to explain the Shadow DOM is to see it in action. Open up a page with an HTML5 video element on it, if you can’t think of one, this should do. Inspect the video element, all you see is the <Video> tag and the elements inside.

Inspecting an HTML5 Video element only shows the HTML you&#39;d expect, a source element and a fallback paragraph element.

Now, as we’re using Chrome, open up dev tools settings and find the “Show user agent shadow DOM” checkbox and tick it.

When you open dev tools settings there is an option to show user agent shadow DOM under the heading &#39;Elements&#39;. Check that.

Inspect that video again and you will see a #shadow-root element which you can open and inspect all the HTML inside.

Now when you inspect the Video element there is a shadow-root and a whole load of divs, inputs and other HTML within.

That is the shadow DOM. It keeps the internal structure of our custom element private and we’re going to update our custom element to use it. To do so, make  the following change to our JavaScript.

  TwilioVideoPrototype.createdCallback = function() {
    var template = importDoc.getElementById('twilio-video-template');
    var clone = importDoc.importNode(template.content, true);
    var shadowRoot = this.createShadowRoot();
    shadowRoot.appendChild(clone);
  }

Instead of appending our template clone to the element itself we create a shadow root for the element. Then we append our template clone to that. Refresh the page and check out our new shadow root.

With the update to the JavaScript we now see a shadow-root for our custom element in the inspector.

The video chat

We’ve done a lot of setup to get our Web Component together. All that’s left to do is implement our video chat. If you have read Sam’s post on getting started with the JavaScript Video SDK or gone through the JavaScript Video quickstart then you will recognise most of this. Let’s add the Twilio Video scripts to the top of our component:

<script src="https://media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
<script src="https://media.twiliocdn.com/sdk/js/conversations/v0.13/twilio-conversations.min.js"></script>

Then, within the <script> element we’ve been working in, we’ll need the following functions.

fetchToken which uses the Fetch API to generate an access token from our Node.js server, parse the json and return a Promise.

TwilioVideoPrototype.fetchToken = function(identity) {
    return fetch("/token?identity=" + identity).then(function(data){
      return data.json();
    });
  }

createClient sets up an AccessManager with the token we retrieve from the server, then instantiates a Conversations.Client and starts to listen for incoming connections.

TwilioVideoPrototype.createClient = function(obj) {
    var accessManager = new Twilio.AccessManager(obj.token);
    this.conversationsClient = new Twilio.Conversations.Client(accessManager);
    return this.conversationsClient.listen();
  }

setupClient runs once the conversation client is listening for incoming connections. It starts listening for incoming invites.

TwilioVideoPrototype.setupClient = function() {
    this.conversationsClient.on("invite", this.inviteReceived.bind(this));
  }

When an invite is received, inviteReceived is called we accept it, which returns a promise.

TwilioVideoPrototype.inviteReceived = function(invite){
    invite.accept().then(this.setupConversation.bind(this));
  }

When the promise resolves, we call setupConversation which shows the elements within our custom element, displays our local media stream, listens for clicks on the hangup button and handles connections and disconnections from other participants.

TwilioVideoPrototype.setupConversation = function(conversation) {
    this.currentConversation = conversation;
    conversation.localMedia.attach(this.me);
    this.chat.classList.remove("hidden");
    this.hangup.addEventListener("click", this.disconnect.bind(this));
    conversation.on("participantConnected", this.participantConnected.bind(this));
    conversation.on("disconnected", this.disconnected.bind(this));
  }

On receiving the participantConnected event we show the new participant’s media stream too.

TwilioVideoPrototype.participantConnected = function(participant) {
    participant.media.attach(this.caller);
  }

When a participant disconnects we hide the whole chat, remove our local media stream and stop listening to events on the hangup button.

TwilioVideoPrototype.disconnected = function() {
    this.chat.classList.add("hidden");
    this.currentConversation.localMedia.detach();
    this.hangup.removeEventListener("click", this.disconnect.bind(this));
  }

If the hangup button is pressed, we disconnect the call ourselves. This function is also used when the element is removed from the page, so we check to see if there is a live conversation at the moment.

TwilioVideoPrototype.disconnect = function() {
    if(this.currentConversation){
      this.currentConversation.disconnect();
      this.currentConversation = null;
    }
  }

Our createdCallback that we started earlier now handles setting up the template, adding it to the shadow root and querying the shadow root for the elements we’ve been using in the functions above. It also checks for the identity attribute on the component using this.getAttribute("identity"). As we saw earlier, I defined the element as <twilio-video identity="phil"></twilio-video>, so this will get the identity “phil” and send it to the server to generate an access token for that identity using fetchToken.

TwilioVideoPrototype.createdCallback = function() {
    var template = importDoc.getElementById("twilio-video-template");
    var clone = importDoc.importNode(template.content, true);
    var shadowRoot = this.createShadowRoot();
    shadowRoot.appendChild(clone);

    var identity = this.getAttribute("identity") || "example";

    this.me = shadowRoot.getElementById("me");
    this.caller = shadowRoot.getElementById("caller");
    this.chat = shadowRoot.getElementById("picture-in-picture");
    this.hangup = shadowRoot.getElementById("hangup");

    this.fetchToken(identity).
      then(this.createClient.bind(this)).
      then(this.setupClient.bind(this)).
      catch(function(err) {
        console.log(err);
      });
  }

Finally we have the detachedCallback which disconnects from any live conversations and stops the conversation client from listening to more incoming connections.

TwilioVideoPrototype.detachedCallback = function() {
    this.disconnect();
    this.conversationsClient.unlisten();
  }

Add all that to the <script> element in our component, refresh the page and wait for an incoming call. I’ve made that nice and easy for you, just open up http://localhost/caller.html. There’s no UI on this page, but it does generate a call to your component (as long as you kept the identity as “phil”, if you changed it, you can change the line conversationsClient.inviteToConversation("phil"); in caller.html to use the identity you chose).

When the page loads you will receive a permissions request for access to your video and microphone on each page. Granting the request will connect the call and you’ll see you Video Chat Web Component come to life.

The video connects and you can wave at your friend.

Check out all the code for this Web Component on GitHub.

Reduce, reuse, recycle

With just under 130 lines of HTML, CSS and JavaScript we have created, without using a framework, a reusable Web Component that can receive incoming video calls. Now, with just these two lines of code (and a /token endpoint to generate access tokens) we can use this anywhere.

<link rel="import" href="/twilio-video.html">

<twilio-video identity="phil"></twilio-video>

Well, OK, it will only work in Chrome today. But there are polyfills available that we can use to make it work in every browser (though do watch out for the slight differences in the API).

I’d love to hear about your uses of Web Components. Have you built your own or used someone else’s? Get in touch in the comments below or drop me a line on Twitter or email.