In order to view this website properly and use all its capabilities, please activate JavaScript.
Here are the instructions how to enable JavaScript in your web browser.

Building an audio player with CSS Flexbox and JavaScript Promises

By Thibaut Gilbert
new

Online audio players proposed by music service providers - like Deezer or Spotify - offer many features and have attractive UIs. As a great music fan and eager to use new techniques I decided to implement a small audio player backed by JavaScript, HTML5 and Rails.

I organized my work in two stages. The first one deals with the JavaScript implementation of the player and its interface. The second stage targets playlist management, communication with the server and offline detection. Each stage is explained in a post. You're currently on the first one.

Thus, here you'll find how to:

  • use CSS Flexbox for a simple responsive layout
  • use the Mediator pattern to build a maintainable audio player
  • simplify tedious callbacks management by using Promises

If you're the type with no time for an introduction, feel free to check the final source code right away, berliozJS. You can also give the demo application a try.

Compatibility note: Throughout this post, we're going to use features from evergreen browsers like flexbox, latest JS API... For the sake of performance (and simplicity ;) ) I won't use polyfills.

And the beat goes on, with Yeoman

As this post is entirely dedicated to front-end JavaScript, we're going to work in a node environment. From now on, I assume that you have a recent version of node installed in a local way. Local means that all node's modules are copied under user's local directory. This avoids to use sudo, which is tedious and can lead to issues. Here is a useful gist to install node and npm without sudo.

Running in node, Yeoman, Bower and Grunt are JS developers' best friends. Yeoman handles code scaffolding for various type of projects, Grunt is a tasks runner (kind of Rails' Rake on steroids) and Bower is a package manager (essential for handling and keeping up to date external JS libraries).

Let's install them along with Yeoman's classical webapp generator. The look and feel of our player is defined by unstable and vendor prefixed css properties (like -webkit-flex). But we can use the neat Compass' mixins to write a more readable and maintainable stylesheet. Thus, head to your terminal and install the grunt-contrib-compass module.

Then create the app directory, jump in it and generate the boilerplate. When Yeoman will ask for it, don't install Modernizr, neither Bootstrap.

Once the installation is done, uninstall jQuery and remove the corresponding <script> tag in app/index.html. Indeed as we're dealing with the latest browsers we don't need this awesome library.
Finally start the local server with Grunt. From now on all modifications in the source will trigger a refresh.

The final source code of this application is hosted on Github: berliozJS.

$ npm install yo bower grunt-cli -g
$ npm install -g generator-webapp
# compass compiler:
# require the sass and compass gems to be installed
$ npm install grunt-contrib-compass -g

# initialize project
$ mkdir berliozjs; cd berliozjs; yo webapp
# ... yeoman @ work ...

# uninstall jQuery
$ bower uninstall jquery
# server on 127.0.0.1:9000
$ grunt serve

Flexible layout with... Flexbox

In app/index.html delete body's markup except the <script> element which includes main.js.
We're using the Compass compiler, right? So, don't forget to go to app/styles/ and rename main.css to main.scss (also delete generated content).

Our application's layout is based on this simple sketch.

Audio player's sketch. Playlist's position changes depending on device's type.

A little bit of markup...

Now put a gist of HTML5 elements in the <body> of index.html to create the app's skeleton. The player is divided into two sections: a panel which displays current track's data and offers playback controls on the <audio> element and a playlist containing all tracks. I tried to use as many HTML5 elements as possible:

  • a <time> tag, for displaying current time
  • the <input> of type range is perfect for volume control

Moreover, I thought it was interesting to put metadata into headers elements (eg <h4>), which give them some hierarchical structure.
Playback icons are embedded fonts (credits to Entypo), a neat solution for saving bandwidth and adjust icons' styles (color, size...). I put those fonts in a distinct font.css file in app/styles/.

<article id="player">
  <section id="panel">
    <audio></audio>
    
    <section id="metadata">
      <h3 id="title">Back in black</h3>
      by <h4 id="artist">AC/DC</h4>-<h4 id="album">The black album</h4>
      <time>20:00</time>
    </section>

    <input type="range">

    <nav id="controls">
      <a id="prev-track" class="icon-prev-track" href="javascript:void(0);"></a>
      <a id="play-pause" class="icon-play" href="javascript:void(0);"></a>
      <a id="next-track" class="icon-next-track" href="javascript:void(0);"></a>
    </nav>
  </section>

  <nav id="playlist">
    <a href="javascript:void(0);">track</a>
    <a href="javascript:void(0);">track</a>
    <a href="javascript:void(0);">track</a>
    <a href="javascript:void(0);">track</a>
  </nav>
</article>

... and a touch of CSS3

I feel this markup lacks a little bit of style. I bet the main.scss file is the perfect place to achieve these goals...
First things first, include the Compass' mixins which give us access to a ton of CSS3 macros. For an exhaustive documentation and lots of examples, go to the Compass' website.

To make the player's layout responsive, I settled a breakpoint at 47.9375em (more or less 767 pixels in a 16 pixels base) and assigned it to a variable $break-small. The css rules are splited in three parts:

  • the first one applies to all devices, independently of their screen resolutions
  • the second covers only small screens
  • the last one targets large screens

By pinpointing small devices, I don't have to overwrite those rules for larger screen resolutions, which results in more maintainable code.

This code snippet presents the global rules which define application's base layout :

  • the player has a maximum width of 800px and is centered in the page
  • player's panel and playlist's tracks share the same appearance (background and text)
  • <time> and <input type="range"> elements are displayed as block

With the global rules in place, let's put some responsive flavor in the design.

/* app/styles/main.scss */

@import "compass/css3"; /*include Compass' mixins*/

$break-small: 47.9375em;

/* ---- Global styles, all devices ---- */

a {
  text-decoration: none;
}

#player {
  margin: 0 auto;
  max-width: 800px;
}

/* --- Panel */
#panel {
  background: grey;
  text-align: center;
}

/* Metadata */
#metadata {
  color: white;
  h4 {
    display: inline;
    color: #463F3F;
  }
}
time {
  display: block;
}

/* Controls */
input[type="range"] {
  display: block;
  margin-top: 30px;
  margin-left: 50px;
}
#controls {
  margin-top: 20px;
  a {
    color: white;
  }
}

/* ---- Playlist */
#playlist {
  a {
    color: white;
    background: grey;
    border: 1px solid #575757;
    @include border-radius(4px);
  }
}

/* ---- iPhone-like devices ONLY ---- */
@media screen and (max-width: $break-small) {
}

/* ---- "wider than" iPhone-like devices ONLY ---- */
@media screen and (min-width: $break-small) {
}

On smartphones, the panel doesn't occupy all space, whereas the playlist spreads on the entire screen. Thus, I set width values to 80% and 100% respectively. On the opposite, widths are limited under large screens, with 40% and 20%.

Specific font sizes are also a constraint in responsive design. In that case, playback icons are larger on small screens (due to touch constraints). At the opposite, metadata are enlarged on large devices to help readability.

If you resize your browser's window you'll notice the way how player's width change.

/* ---- iPhone-like devices ONLY ---- */
@media screen and (max-width: $break-small) {
  /* --- Panel */
  #panel {
    width: 80%;
  }
  #controls {
    font-size: 3.2em; /*playback icons*/
  }
  /* --- Playlist */
  #playlist {
    width: 100%;
  }
}

/* ---- "wider than" iPhone-like devices ONLY ---- */
@media screen and (min-width: $break-small) {
  /* --- Panel */
  #panel {
    width: 40%;
  }
  /* Metadata */
  #metadata {
    h4 {
      font-size: 1.35em; /*increase font-size*/
    }
  }
  #title {
    font-size: 2em;
  }
  time {
    font-size: 2.3em;
  }
  /* Controls */
  #controls {
    font-size: 3em; /*playback icons*/
  }

  /* ---- Playlist */
  #playlist {
    width: 20%;
  }
}

Flexible boxes, no more float

As pointed by the above diagram, player's UI follows a vertical arrangement on small screens (ie smartphones') then follows an horizontal flow on larger devices. We traditionally achieve that via the float property, but it's old fashioned and quirky per se. The article on MDN dealing with flexboxes precises:

Flexbox layout is most appropriate for the components of an application, and small-scale layouts.

No doubt, flexbox is definitely the right candidate. How can we use it?

The <section> panel and the <nav> playlist have to be aligned vertically on small screens and horizontally on large devices.
Reopen main.scss and define the <article> element (with id #player) as a flex container by using the display-box macro and the display: flex; declaration (for latest browsers). This implies that its children, the panel and playlist are flex items and are distributed along the horizontal axis, from left to right. Cool, this is the alignment we want for large screens! Note: this horizontal axis is the main-axis, perpendicular to a cross-axis.

To change the alignment for small displays, we have to change the main-axis direction, from horizontal to vertical. For that simply append the declaration flex-direction: column; to the player's rules that target small devices.

/* ---- Global styles, all devices ---- */
#player {
  margin: 0 auto;
  max-width: 800px;

  /* define '#player' as a flex container */
  @include display-box;
  display: flex;
}

/* ---- iPhone-like devices ONLY ---- */
@media screen and (max-width: $break-small) {
  #player {
    flex-direction: column; /*vertical direction for main axis*/
  }
}

Our elements now lay out correctly, but are left-aligned . This alignment is done along the main-axis for large screens but along the cross-axis for small screens. The corresponding flexbox properties are justify-content (main-axis) and align-items (cross-axis). Let's give them the center value.

/* ---- iPhone-like devices ONLY ---- */
@media screen and (max-width: $break-small) {
  #player {
    flex-direction: column; /*vertical direction for main axis*/
    /*cross axis layout*/
    @include box-align(center);
    align-items: center;
  }
}

/* ---- "wider than" iPhone-like devices ONLY ---- */
@media screen and (min-width: $break-small) {
  #player {
    /*main axis layout*/
    @include box-pack(center);
    justify-content: center;
  }
}

There is one point left in our flexbox model, the playlist. Indeed, the playlist has a vertical orientation on wide screens but lays out horizontally on smartphones. By defining the playlist as a flex container, its items (the tracks) instantly laid along a row. Then, don't forget to set main-axis direction to column for large screens with the flex-direction property. Playlist appearance is in place but it's behavior will be implemented in a next post.

Bravo, our flexbox model is complete!

/* ---- Global styles, all devices ---- */
/* ---- Playlist */
#playlist {
  @include display-box;
  display: flex;
  a {
    color: white;
    /*...*/
  }
}

/* ---- "wider than" iPhone-like devices ONLY ---- */
@media screen and (min-width: $break-small) {
  /* ---- Playlist */
  #playlist {
    flex-direction: column;
    width: 20%;
  }
}

JavaScript, with a pattern and some promises

We can modelize an audio player as an aggregate of UI elements (seekbar, volume slider...) wrapped around the main audio element. In this ecosystem, communication between those elements ( or modules) is a key constraint. As an example, quickly consider the event stack which takes place when a user clicks on the 'play/pause' button:

  • the click event is catched and a play() directive is sent on the <audio> element
  • on the assumption that all went as expected (file downloaded on a reliable network, valid file format) the audio element will start playback
  • the 'play/pause' button is informed that the audio element is in playback state, so toggles its appearance from 'play' to 'pause'
  • other UI elements, as the label displaying current time, have to be aware of player's state too

We see in this scenario that communication channels are bidirectional, indeed each module has the ability to both send and receive messages. When it comes to design communication infrastructure, the Observer or Mediator Behavioral Patterns often come to rescue. However, by choosing the Observer pattern we have to define modules as both observer and subject, which will create many to many communication channels between those modules. Clearly not the best solution.
Here comes the Mediator pattern, which is the right contender, as its centralized architecture coerces modules to communicate through it.

[The Mediator] can help us decouple systems and improve the potential for component reusability.

So far so good, we've picked up a pattern to design our player. But what about event management? Technically, it's easy to implement by using addEventListener() and chaining callbacks. But this raw implementation of event handling will rapidly lead us to nested callbacks and tigh coupling between elements. Worse, this implies losing control on our code's flow. Indeed by making callbacks statements part of the program flow, we hand control of our code over to external methods whose execution is unpredictable: will they execute too early, too late, too many times, not at all? JavaScript gurus call that Callback Hell...

But a superhero is there to help us, Promises.

A promise represents the eventual result of an asynchronous operation.

In a nutshell, instead of basing our code on expectations, promises allow us to be notified when an asynchronous task is completed and then decide what to do next. Sounds good in the context of our audio player...

How Berlioz should work once implemented

Berlioz's interface is fairly simple. The opposite code snippet shows how to create an audio player based on the HTML markup of index.html.
Berlioz is based on the concept of connectors. First the init() static method returns a singleton instance of the pseudoclass Berlioz. This object is the main connector. It creates an audio element (by loading a remote audio file) and wraps it into the #panel tag of our markup.
Next we have to connect UI elements to our main connector, via the... connect() method. This method takes a DOM element and a connector identifier (string) as parameters and returns an object of type Connector.

As we'll see later, both Berlioz and Connector instances respond to the subscribe() and publish() methods.

Now, enough of chit chat, let's code!

var
  // initialize main connector, load remote audio file
  berlioz = Berlioz.init('http://tcamp.fr/berliozjs/', document.querySelector('#panel')),

  // connect a connector of type 'playPauseButton' to the main connector
  connector1 = berlioz.connect( document.querySelector('#play-pause'), 'playPauseButton'),
  // 'metadataPanel' connector (track's data, current time...)
  connector2 = berlioz.connect( document.querySelector('#metadata'), 'metadataPanel'),
  // 'volumeSlider' connector
  connector3 = berlioz.connect( document.querySelector('input[type=range]'), 'volumeSlider');

The Mediator pattern to rule them all

Let's create a app/scripts/berlioz.js script and include it in the Grunt build:js scripts/main.js building block of index.html. Note: As usual, an immediate function wraps the whole code. For the sake of brievity, local variables declarations and some basic functions' definitions are omitted.

Most of the application functionalities are put into a Berlioz object (more precisely a function, as we'll see later on) which isn't accessible in the global scope.
The Mediator pattern is based on two methods, subscribe() and publish(). subscribe will register one or many callbacks for a given event (identified by a string) and publish will trigger all callbacks associated to this event. Regarding subscribe, notice how the callback (fn parameter) is bound to the subscriber's context.
The events object is a hash which associates each event to its callback(s). For a better overview, I decided to organize events by connectors (including the main one) and store them in an eventsByConnector object, accessible via the property of the same name. You might observe that connector number 4 is not listed, indeed the volumeSlider didn't subscribe to any event.

Throughout the code we'll also refer to a _B variable, it's simply a shortcut to the Berlioz object.

// ** usage
subscribe('event_foo', function(){console.log('first callback');} );
subscribe('event_foo', function(data){console.log(data + ' callback');} );
publish('event_foo', 'second');
//=> 'first callback'
//=> 'second callback'

// given that the previous set of instructions has just taken place,
// (ie `connector3` instanciated)
Berlioz.eventsByConnector;
/*
=>
  {
    '1': { // main connector, id = 1
      'play_pause': [fn1, fn2...]
      'volume': [fn3, fn4...]
    },
    '2': { // playPauseButton connector
      'pause': ...
      'play:': ...
    },
    '3': { ... }
  }
*/

// ** implementation
_extend( Berlioz,
  {
    // events map: map eventType <> fns
    events: {},

    // for information purpose, list events for each connector
    // => "public" access
    eventsByConnector: {},

    // filled each time a new subscription occurs
    eventsList: [],
    
    // build the eventsByConnector map first, then the events map.
    // fn is bound to subscriber's context
    subscribe: function( eventType, fn ) {
      var connectorEvents,
        connectorId = this.id;
      if ( _B.eventNotListed( eventType ) ) {
        _B.eventsList.push( eventType );
      }
      if ( !(connectorId in _B.eventsByConnector) ) {
        _B.eventsByConnector[ connectorId ] = {};
      }
      connectorEvents = _B.eventsByConnector[ connectorId ];
      if ( !(eventType in connectorEvents) ) {
        connectorEvents[ eventType ] = [];
      }
      connectorEvents[ eventType ].push( fn.bind( this ) );
      
      // rebuild events map
      _B.buildEventsMap();
    },

    // the events map is parsed on each publication
    publish: function( eventType, data ) {
      var fns;
      if ( eventType in _B.events ) {
        for ( i = 0, fns = _B.events[ eventType ], l = fns.length; i < l; i +=1 ) {
          fns[ i ]( data );
        }
      }
    }
  });

The Connector pseudoclass, give control to user

The Connector constructor first checks for the validity of the connector type. If invalid, it will return an empty object. Otherwise it will set up the connector's id, store a reference of the DOM element and call the appropriate initializer (a word on this in the next part).

Then, we augment the constructor's prototype with the subscribe and publish properties, which encapsulate the corresponding functions of the Berlioz object.

Connector = function( elem, connectorType ) {
  if ( (connectorType in _B.connectorTypes ) ) {
    // valid type
    this.valid = true;

    this.id = ++idCount;
    this.type = connectorType;
    this.elem = elem;

    // initialize connector
    _B.connectorTypes[ connectorType ].call( this );
  }
};

// Connector interface
Connector.prototype = {
  subscribe: function( eventType, fn ) {
    _B.subscribe.call( this, eventType, fn );
  },
  publish: function( eventType, data ) {
    _B.publish( eventType, data );
  }
};

Now, what about connectors' initializers, like playPauseButton? They will bind gesture events to the DOM element referenced in the connector instance (the elem attribute).

In order to address DOM gesture events for various devices we're using Hammer.js. Load the library via Bower and include the script in Grunt's scripts/vendor.js building block. Then update the <input[type="range"]> volume tag with a precision="float" attribute (I noticed that in Chrome's Shadow Dom) and a step attribute.

$ bower install hammerjs

<!-- build:js scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/hammerjs/dist/hammer.min.js"></script>
<!-- endbower -->
<!-- endbuild -->

<input type="range" min="0" max="1" precision="float" step="0.01">

As an example we'll focus on the volumeSlider and playPauseButton initializers.

The volumeSlider sets a default value to the <input[type="range"]> and registers a listener for the change event. This listener will publish the 'volume' event, passing the input's current value.

The playPauseButton will first publish a 'play_pause' event, each time the user 'touch' the corresponding element (ie our <a id="play-pause"> tag). But it will also subscribe to the 'play' (audio element is playing) and 'pause' events in order to update its appearance accordingly.

I guess the trackSeeker is a good place to handle a <progress> tag, to display the current playback position and allow user to seek into the track.

// berlioz.js

_extend( Berlioz,
  {
    // Berlioz' connectorTypes= wrap UI components,
    // called on Connector's context
    connectorTypes: {
      volumeSlider: function() {
        var self = this;
        // default volume
        this.elem.value = 0.7;

        this.elem.addEventListener( 'change', function() {
          self.publish( 'volume', this.value );
        });
      },
      playPauseButton: function() {
        var self = this;
        Hammer( this.elem ).on( 'touch', function() {
          self.publish('play_pause');
        });
        this.subscribe( 'play', function() {
          this.elem.classList.remove('icon-play');
          this.elem.classList.add('icon-pause');
        });
        this.subscribe( 'pause', function() {
          this.elem.classList.remove('icon-pause');
          this.elem.classList.add('icon-play');
        });
      },
      trackSeeker: function() {
        // TODO
      },
      nextTrackButton: function() {
        // ...
      },
      prevTrackButton: function() {
        // ...
      },
      metadataPanel: function() {
        // ...
      }
    }
  });

The Berlioz pseudoclass, listen to the music

The Berlioz constructor creates an empty audio element, identified by the audioElem attribute and appends it to our panel tag. We can now delete the audio tag in index.html, as this tag is going to be generated dynamically.

Berlioz's instance subscribes to the 'volume' event (sent by volumeSlider) and will regularly publish 'position' event as long as the audio element is playing. Note: under Safari the volume property is not settable in JavaScript.

The connectors attribute keeps references of each new connector connected to the Berlioz's instance (see the connect() method in Berlioz's prototype).

_B = Berlioz = function( host, containerElem ) {
  var self, promiseGenerator;

  this.host = host;
  this.id = ++idCount;
  this.connectors = [];

  this.audioElem = containerElem.appendChild( document.createElement('audio') );

  self = this;

  // volume
  this.subscribe( 'volume', function( volume ) {
    this.audioElem.volume = parseFloat( volume );
  });
  // default volume when player initialized
  this.publish( 'volume', 0.7 );

  // update currentposition
  this.audioElem.addEventListener( 'timeupdate', function() {
    self.publish( 'position', _formatTime( this.currentTime ) );
  });
};

The magic of Promises

An implementation of the Promises/A+ specification is the Q library, thus install it with Bower and load the script in the web page.
Moreover, in order to parse and display the ID3 metadata of the audio file, we're also using the ID3 Reader library (copied in scripts/).

$ bower install q

<!-- build:js scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/hammerjs/dist/hammer.min.js"></script>
<script src="bower_components/q/q.js"></script>
<script src="scripts/id3-minimized.js"></script>
<!-- endbower -->
<!-- endbuild -->

Here we're going to implement two promises generators. As their name suggests, these are functions whose role is to initialize and return promises. Each of these functions handle a unique asynchronous task. Basically, we can point out three steps in the asynchronous operation's lifetime :

  • resolve: we estimate that the operation ended successfully and we return the result of the process
  • notify: the operation is in progress
  • reject: the operation has ended badly

Those steps can be programmatically called thanks to a deferred object, created by Q.defer();. However, we're ultimately dealing with a promise; that's the role of the deferred.promise last statement.

So, what are those asynchronous tasks we have to deal with?

  • initialize():

    • will create two <source> elements in order to point to the files audio.ogg and audio.m4a hosted on the server. Note the use of DocumentFragment for optimization.
    • will rely on the ID3 library to asynchronously load file's id3 tags (XHR) and pass them to the deferred object.
  • togglePlay():

    • will react to a touch on the play/pause button by sending play() or pause() signals to the audio element.
    • will register to the proper events to detect if the player has started playback.
      The asynchronous task will be marked as resolved anyhow, but won't transmit the same boolean value.

Finally, according to the w3c specs, the 'playing' event means that the player is potentially playing, so we use the handy notify() method to keep us informed.

_extend( Berlioz,
  {
    // --- Audio element's promises generators
    // called in Berlioz instance context
    aEP: {
      initialize: function( trackName ) {
        var sources,
          oggSource, oggSourceURL = this.host + trackName + '.ogg',
          m4aSource, m4aSourceURL = this.host + trackName + '.m4a',
          deferred = Q.defer();

        ID3.loadTags('meta.m4a', function() {
          var metadata = ID3.getAllTags('meta.m4a');
          deferred.resolve( metadata );
        },
        {tags: ['artist', 'title', 'album', 'year', 
           'comment', 'track', 'genre', 'lyrics', 'picture']});

        sources = document.createDocumentFragment();
        oggSource = document.createElement('source');
        oggSource.src = oggSourceURL;
        oggSource.type = 'audio/ogg';
        sources.appendChild( oggSource );
        m4aSource = document.createElement('source');
        m4aSource.src = m4aSourceURL;
        m4aSource.type = 'audio/mp4';
        sources.appendChild( m4aSource );
        this.audioElem.appendChild( sources );

        return deferred.promise;
      },

      togglePlay: function() {
        var deferred = Q.defer(),
          playing = function() {
            this.removeEventListener( 'playing', playing );
            deferred.notify();
          },
          timeUpdate = function() {
            this.removeEventListener( 'timeupdate', timeUpdate );
            deferred.resolve( true );
          };

        if ( this.audioElem.paused ) {
          this.audioElem.addEventListener( 'playing', playing );
          this.audioElem.addEventListener( 'timeupdate', timeUpdate );
          this.audioElem.play();
        } else {
          this.audioElem.pause();
          deferred.resolve( false );
        }

        return deferred.promise;
      }
    }
  });

Once the promises generators are wired up, we can handle promises readily, via the then() method.
The method takes three callbacks as parameters :

  • the first one is called if the promise terminates in a resolve state = success
  • the second one in case of reject state = error
  • the third one in case of notify state = progress

With this in mind, go back to the Berlioz constructor and put the generators in the right places:

  • togglePlay() must trigger when the 'play_pause' event is published
  • initialize() is executed at the very end of the constructor. In case of success, the id3 metadata are published over the 'init_metadata' event.

We clearly see here the clarity and simplicity of Promise's control flow, which tremendously improve code comprehension and maintainability.

_B = Berlioz = function( host, containerElem ) {
  // bind audio element's promises generators on Berlioz instance
  for ( promiseGenerator in _B.aEP ) {
    _B.aEP[ promiseGenerator ] = _B.aEP[ promiseGenerator ].bind( this );
  }

  // toggle play/pause
  this.subscribe( 'play_pause', function() {
    _B.aEP.togglePlay()
    .then(function( play ) {
      if ( play ) {
        self.publish('play');
      } else {
        self.publish('pause');
      }
    }, function() {
    }, function() {
      // promise in progress
      self.publish('playing');
    });
  });

  // load audio element!
  _B.aEP.initialize('audio')
  .then(function( metadata ) {
    self.publish( 'init_metadata', metadata );
  }, function() {
  }, function() {
  });
};

I hope that throughout this post, I managed to demonstrate that Flexbox is a neat solution for designing UIs which rely on change in their orientation and whose elements have to be positionned and resized along various axis.
The Mediator is the proper choice in a system where communication channels have to be decoupled.
At last, Promises is the solution to get away from callback hell. This post deals with the tip of the iceberg, though.

This audio player is simple but flexible enough to quickly evolve (eg add a seek bar). This is my very first post on 47tibo and I look forward to have feedbacks (of any kind), so don't hesitate to leave comments, thanks!

Latest See all →

Parallax scrolling: why a fixed background?
new

This post explains the use of a fixed background-image in parallax scrolling. To do so I review the visual concepts behind parallax and illustrate it with the Skrollr library and a few lines of Scss.

Building an audio player with CSS Flexbox and JavaScript Promises
new

A step by step tutorial to learn how to build an HTML5 audio player from scratch, focused on:

  • the use of CSS Flexbox for a simple responsive layout,
  • an implementation of the Mediator pattern in JavaScript to develop a maintainable player,
  • the use of Promises to simplify callbacks management

About

47tibo.com is a blog focused on JavaScript, HTML5 and Ruby on Rails, offering tutorials and best practices, from intermediate to advanced.
Posts are structured around code snippets and out of the box examples, featuring various topics, from abstract to concrete ones (like performance or HTML5 features).
Source codes are under Creative Commons licence but original graphics are copyrighted.

For a presentation of all techniques at play on 47tibo.com, click here.

Connect

Liked what you've seen here?
If so you might be interested in the following: