Create a Custom Audio Player Element using Polymer

What is Polymer?

Polymer is a shim for a new web standard called Web Components, which enables us to create completely custom HTML elements.

There are native web elements that we use every day, such as <input>, <checkbox>, <audio>, and so on. Their number is limited and their design and behavior are being defined by browsers, not by developers. With Web Components, we can now create a custom <element> and define its style and behavior. Unfortunately, because this standard is very new, it will take some time until all browsers support it.

Luckily for us, there’s a couple of open source libraries being actively developed that allow creating and using Custom Elements today: Polymer (Google) and X-Tag (Mozilla). This summer, during the I/O summit, the Polymer team introduced v1.0 and announced that it is now production-ready.

What is a Custom Element?

Say, you have a blog and need to add audio tracks to some of your posts (for example, music tracks or podcasts). The most convinient way to do this would be to use a native <audio> element. It’s simple to use and can’t conflict with any surrounding code. Just paste <audio controls src="track.mp3">, and you would get a 100% controlled result that would look like this:

audio native element

But what if you’d like it to look differently? Or have some additional features, such as displaying timing and title? And on top of that, you want to keep it simple and modular, just like the native solution.

Say, we’d like to build something like this:

paper-audio-player polymer element

This is where Web Components and Polymer come into play. When we have a Custom Element, we can use it as simply as the native one:


<my-super-audio src="track.mp3"></my-super-audio>

How cool is that?

Also, imagine that you can encapsulate all your re-usable UI elements into custom HTML tags and use them across different applications. You can be sure that there won’t be any conflicts between the application’s code and your Custom Element’s code.

How is it possible?

Shadow DOM

When you insert a plain <input> element on any web page, it always looks the same and never conflicts with your code. But <input> has built-in styling, some public APIs, and it reacts on click events, meaning it has some JavaScript logic. Have you ever thought about how these native elements work?

Probably not, because when you inspect your page in the browser Developer Tools, all you can see is <input> and no additional <scripts>. So where does <input> hide its CSS and JS?

In Shadow DOM.

To see it:

  1. Open Developer Tools ( Alt+Cmd+i ) in Chrome.
  2. Open Settings ( Fn+F1 ) and enable Show user agent shadow DOM.
  3. Reload Chrome.
  4. Inspect any <audio> tag ( right-click on input field -> Inspect ).

Now you should see the hidden code in its Shadow DOM.

shadow DOM example

When you create a Custom Element, you hide its styling, behavior and markup in Shadow DOM. This ensures that nothing can reach it from Light DOM, where your application lives.

Let’s build!

Today we’re building a custom <my-super-audio> element from the screenshot above. To get a feeling of where we’re going, check out this open source player.

Prepare dev environment

As a boilerplate for our Custom Element we will use the seed-element project prepared by the Polymer team. It provides a proper structure and a number of extremely useful built-in features, such as a server for local development, testing and a documentation page with demo.

  1. Download and unzip the source code of the latest release here.
  2. Rename the folder from seed-project to your element’s name: my-super-audio.

Note: By convention, every Custom Element has to include at least one dash in its name.

Install dependencies

Make sure you have Node, npm, git and Bower installed on your machine. Seed-project manages all the dependencies using Bower. To install them, open my-super-audio project folder and run:


bower install

Local web server

Because we’re going to work with HTML imports, we need to run the project using a local server. If you don’t have one, seed-project has a nice option built-in. To work with it, install it globally:


npm install -g polyserve

When it’s installed, run:


polyserve

This starts a web server locally on port 8080. Now you can see the default seed-element in the browser here: localhost:8080/components/seed-element/demo.

Awesome! Now we can move on and code.

Create a Polymer element

First, we need to remove the seed-element and create our own my-super-audio element.

  1. In the root folder of your project, rename seed-element.html to my-super-audio.html.
  2. In the bower.json file, rename the project to my-super-audio too.
  3. Open my-super-audio.html and replace the existing <dom-module> code with this:

<dom-module id="my-super-audio">
  <template>
    <p>{{author}}</p>
  </template>

  <script>
    Polymer({
      is: 'my-super-audio',
      properties: {
        author: String
      }
    });
  </script>
</dom-module>

By creating a <dom-module> tag with the same id as the Polymer.is property, we declare a custom Polymer-based element with this name.

Now, let’s test it in our browser. To do this, we need to place our newly created element on the Demo page.

First, open index.html inside the /demo folder and edit the <link> element to import my-super-audio.html:


<link rel="import" href="../my-super-audio.html">

Now, place your element into the <body>, like this (change YOUR_NAME_HERE to your name):


<body>
  <my-super-audio author="YOUR_NAME_HERE">Hello World!</my-super-audio>
</body>

Open your Demo page in your browser: http://localhost:8080/components/my-super-audio/demo/, and you will see… your name.

Wait! Instead of the expected Hello World! we somehow display a string from the author attribute of our element. How did that happen?

This is how Custom Elements work. Only the code inside of <dom-module> matters, not the code in the Light DOM. Because in our element’s <template> we only display <p> which is bound to the author property of the Polymer element, this is the only visible data. Nothing else.

Polymer element structure

As you already know, Web Components let us create our own Custom Elements that encapsulate markup (HTML), styling (CSS) and behavior (JavaScript). Inside <dom-module> we have three sections:

  • <template> to create markup that will be rendered inside <my-super-audio>;
  • <style> to style this element;
  • <script> to declare custom public and private properties, methods and other logic.

Note: Starting with Polymer v1.1, it is recommended to place the <style> tag inside <template>.

First things first: layout.

Since we know how the My-Super-Audio element should look, let’s create a markup for it. Our main horizontal rectangle will have three sections inside:


<template>
  <div id="wrapper">
    <div id="left"><!-- ... --></div>
    <div><!-- ... --></div>
    <div id="right"><!-- ... --></div>
  </div>
</template>

To position all of them properly, let’s add the <style> tag and some CSS rules using flex-box:


<template>
  <!-- styling -->
  <style>
    :host {
      width: 100%;
    }

    #left,
    #right {
      height: 50px;
      width: 50px;
      position: relative;
    }

    #left {
      background-color: blueviolet;
    }

    /* Helpers */

    .layout-horizontal {
      display: flex;
      -ms-flex-direction: row;
      -webkit-flex-direction: row;
      flex-direction: row;
    }

    .flex {
      -ms-flex: 1;
      -webkit-flex: 1;
      flex: 1;
    }

    .self-start {
      -ms-align-self: flex-start;
      -webkit-align-self: flex-start;
      align-self: flex-start;
    }

    .self-end {
      -ms-align-self: flex-end;
      -webkit-align-self: flex-end;
      align-self: flex-end;
    }
  </style>

  <!-- markup -->
  <div id="wrapper" class="layout-horizontal">
    <div id="left" class="self-start"><!-- ... --></div>
    <div class="flex"><!-- ... --></div>
    <div id="right" class="self-end"><!-- ... --></div>
  </div>
</template>

If you open the Demo page right now, you won’t see much, so let’s display a title, just like we displayed the author name above.

First, edit the property name from author to title in the Polymer function:


Polymer({
  is: 'my-super-audio',

  properties: {
    title: String
  }
});

Next, bind it inside the middle <div> with class flex:


<div id="left" class="self-start"><!-- ... --></div>
<div class="flex">
  <!-- Title -->
  <div id="title" class="fit">{{ title }}</div>
</div>
<div id="right" class="self-end"><!-- ... --></div>

Add some styling inside <style>:


#title {
  position: absolute; 
  color: blueviolet; 
  font-size: 15px; 
  text-align: center; 
  line-height: 50px; 
  z-index: 2; 
}

#wrapper {
  position: relative; 
  box-shadow: 0 1px 2px rgba(0, 0, 0, .3); 
  cursor: pointer; 
}

.fit { 
  position: absolute; 
  margin: auto; 
  top: 0; 
  right: 0; 
  bottom: 0; 
  left: 0; 
}

Lastly, let’s edit our element on the Demo page, and pass title instead of author:


<my-super-audio title="My Podcast #17"></my-super-audio>

Now, reload the Demo page and you will see Podcast #17 on a nice white material element. When the mouse hovers over the element, the cursor now becomes a pointer. It’s time to play the audio!

Add the audio

To play a track, we will use the Web Audio API, which all browsers can work with. The most straightforward way to initiate this API is to declare an <audio> element.

First, let’s add a new public property to pass the URL of the audio track.

In my-super-audio.html:


<div class="flex">
  <!-- Title -->
  <div id="title">{{ title }}</div>
  <!-- Audio HTML5 element -->
  <audio id="audio" src="{{ src }}"></audio>
</div>

Polymer({
  is: 'my-super-audio',

  properties: {
    title: String,
    src: String
  }
});

Demo page:


<my-super-audio title="Podcast #17" src="track.mp3"></my-super-audio>

Note: Make sure you specify the path to your favorite track, because you will have to listen to it a million times during the development and testing! :)

If you now inspect the Demo page with Developer Tools, you will see that the audio element is available inside <my-super-audio>, but how do you play it?

Binding click event to custom method

What we would like to achieve here is that when the user clicks anywhere on our element, the audio starts/pauses playing.

To implement this behavior, we need to listen to the click event on the whole element and bind it to our custom method that will trigger <audio> to start (or pause) playing.

For events such as click, tap, mouseover, and so on, Polymer provides us with the convenient built-in listeners. To use it, simply add the on-click="playPause" attribute to #wrapper div:


<div id="wrapper" class="layout-horizontal" on-click="playPause">

Now, let’s extend our Polymer element by adding a custom method to it:


Polymer({ 
  is: 'my-super-audio',

  properties: { 
    title: String, 
    src: String 
  },

  playPause: function(e) { 
    e.preventDefault(); 

    var player = this; 

    if ( player.$.audio.paused ) { 
      player.$.audio.play(); 
    } else { 
      player.$.audio.pause(); 
    } 
  } 
});

What we’ve done here:

  1. By referring to player.$.audio (or this.$.audio), we refer to an internal node with the id="audio". This is an automatic node finding tool from Polymer that allows us to access frequently-used nodes without the need to query for them manually.
  2. Methods play() and pause() are native methods provided by the Audio Web API.

Now, reload the Demo page and click on the player to hear your track.

Using external Custom Elements

So far, we’ve been writing our own code, but we could also integrate external Custom Elements the same way we do with native ones. Actually, we’ve just done it with the native <audio> element. Now let’s add a couple of useful Polymer elements.

What we need at this point is the progress bar to indicate the progress of the audio track. Let’s use Paper-Progress element by Polymer for this.

First, let’s install it with Bower as dependency:


bower install --save PolymerElements/paper-progress

Here, we use the --save flag, so Bower saves this element as a dependency in our bower.json. This is critically important, because users who want to use the <my-super-audio> element in their projects will need to install all the dependencies at the time of installation.

To get access to the element after installation, we need to import it. Add the following lines at the very top of the my-super-audio.html file, right after the Polymer library import:


<link rel="import" href="../paper-progress/paper-progress.html">

Now let’s add the progress bar to the middle section:


<div class="flex">
  <!-- Title -->
  <div id="title">{{ title }}</div>
  <!-- Audio HTML5 element -->
  <audio id="audio" src="{{ src }}"></audio>
  <!-- Paper progress bar -->
  <paper-progress id="progress"></paper-progress>
</div>

To modify the default styling of the Paper-Progress element we use its custom external CSS properties, that have special prefix (--paper-progress-active-color). For each Polymer element its author can expose some styling options for easier customization. Add the following code to <style>:


paper-progress {
  position: relative;
  width: 100%;
  --paper-progress-active-color: blueviolet;
  --paper-progress-height: 50px;
  --paper-progress-container-color: rgba(255, 255, 255, .75);
}

In JS, we need to add two event listeners to the <audio> element.

  • The first one will trigger when <audio> metadata is loaded by the browser. At this point we know the duration of the track, therefore we can set the max property of the progress bar.
  • The second listener will fire when <audio> starts playing. At this point, we start the progress timer that will update the progress bar state every 120 milliseconds.

// Register event listeners

listeners: { 
  'audio.loadedmetadata': '_onCanPlay', 
  'audio.playing':        '_startProgressTimer' 
},

// When metadata is loaded and player can start playing

_onCanPlay: function() { 
  var player = this; 
  player.$.progress.max = player.$.audio.duration * 1000; 
},

// Start the progress timer

_startProgressTimer: function() { 

  var player = this; 
  player.timer = {}; 

  if (player.timer.sliderUpdateInterval) { 
    clearInterval(player.timer.sliderUpdateInterval); 
  } 

  player.timer.sliderUpdateInterval = setInterval( function(){ 
    if ( player.$.audio.paused ) { 
      clearInterval(player.timer.sliderUpdateInterval); 
    } else { 
      player.$.progress.value = player.$.audio.currentTime * 1000; 
      player.currentTime = player.$.audio.currentTime; 
    } 
  }, 120); 
}

Check out the Demo page! Looks good, doesn’t it? :)

Computed bindings

Since we now have access to track duration data, let’s display it in the #right section of our player. Lets create the duration property that will be a Number:


properties: {
  title: String,
  src: String,
  isPlaying: {
    type: Boolean,
    value: false
  },
  duration: {
    type: Number,
    value: 0
  }
},

Add the following markup inside <template>:


<div id="right" class="self-end">
  <!-- Duration -->
  <div id="duration" class="fit">
    <span class="fit">{{ duration }}</span>
  </div>
</div>

And styling inside <style>:


#duration {
  text-align: center;
  line-height: 50px;
  font-size: 11px;
  color: blueviolet;
}

Now we need to update the duration property when the <audio> element metadata is loaded in the browser. For this lets extend out _onCanPlay method:


_onCanPlay: function() {
  var player = this;
  player.$.progress.max = player.$.audio.duration * 1000;
  player.duration = player.$.audio.duration;
},

Ok, check out the Demo page to verify it. We can see the duration now, but it’s a long number.

The problem is that <audio> element provides its duration in seconds, which is not informative for our users. So how can we transform seconds to a good looking m:ss format? Computer bindings to the rescue!

A computed binding is similar to a computed property. Let’s add a private method _convertSecToMin:


// to convert seconds to 'm:ss' format
_convertSecToMin: function(seconds){
  if (seconds === 0) {
    return '';
  }
  var minutes = Math.floor(seconds / 60);
  var secondsToCalc = Math.floor(seconds % 60) + '';
  return minutes + ':' + (secondsToCalc.length < 2 ? '0' + secondsToCalc : secondsToCalc);
}

And modify our binding in the markup:


<!-- Duration -->
<div id="duration" class="fit">
  <span class="fit">{{ _convertSecToMin(duration) }}</span>
</div>

Awesome! It looks way better now.

Data binding helpers

The final thing we would want to implement in our player is the icons in the #left that will indicate whether the player can be played or paused, depending on its current state.

First, let’s save Iron-Icons as dependencies to our player:


bower install --save PolymerElements/iron-icon

bower install --save PolymerElements/iron-icons

In my-super-audio.html, import these two libraries right after the Paper-Progress:


<link rel="import" href="../iron-icon/iron-icon.html">
<link rel="import" href="../iron-icons/av-icons.html">

Add the following markup and styling:


<div id="left" class="self-start">
  <!-- Icons -->
  <iron-icon id="play" 
                class="fit" 
                icon="av:play-circle-outline" 
                hidden$="{{ isPlaying }}"></iron-icon>

  <iron-icon id="pause" 
                class="fit" 
                icon="av:pause-circle-outline" 
                hidden$="{{ !isPlaying }}"></iron-icon>
</div>

#play,
#pause {
  color: #fff; 
}

In the Polymer function, add a new property called isPlaying to record the state of the player, and extend the playPause method to toggle this property when the user clicks on the element:


Polymer({ 
  is: 'my-super-audio',

  properties: { 
    title: String, 
    src: String, 
    isPlaying: { 
      type: Boolean, 
      value: false 
    } 
  },

  playPause: function(e){ 
    e.preventDefault(); 

    var player = this; 

    if ( player.$.audio.paused ) { 
      player.$.audio.play(); 
      player.isPlaying = true; 
    } else { 
      player.$.audio.pause(); 
      player.isPlaying = false; 
    } 
  } 
});

Note that this time we declared a new property isPlaying as an object, with type Boolean and default value of false.

Congrats! You’ve created your first Custom Element!

enter image description here

This tutorial is created based on the open source Paper-Audio-Player element. Check it out to see how different Polymer features can play together.

Links

Nadi Dikun

Nadi designs and develops web and hybrid mobile applications using Angular, Ionic and Polymer. She is an active open source contributor and creator of her own open source projects.

In her blog Inspirational Interface Mechanics Nadi writes about her JavaScript and UI/UX experiments.