News

Create complex responsive sites with intention.js

Write a responsive music player with intention.js and learn what it means to create custom axes and contexts

intentionjs

Intention.js is a JavaScript library from Dow Jones; its main job is to measure and respond to things. Its prime use is when building complex responsive websites that switch elements around at different widths, add and remove classes and switch out sources. Intention.js is similar to AngularJS in that you describe the page’s behaviour within the HTML structure through data-* attributes and the ‘intent’ keyword. Intention.js has four ‘keywords’ for placing elements: before, prepend, after, and append.

The key to understanding intention.js is its terminology. An axis is comprised of multiple contexts that contain a name and a value. So the width axis could have a context called ‘tablet’ and a value of 510. Intention uses this information to say ‘when listening to the width, apply a class of tablet when the width is 510px or greater’. We’re building a responsive, browser-based music player to learn how to use intention.js’ data attributes, create custom axes and write contexts so we can gain a firm understanding of how it works.

Script dependencies

Intention.js depends on jQuery and Underscore in order for it to work, so be sure to include these before intention.js. Intention also comes with an optional ‘bootstrap’ file called context.js, which gives four common measurements: width, orientation, touch capable, and whether it’s high DPI. We then include our own JavaScript file after it. All of these dependencies weigh in at a hefty 115.5kb pre-gzip.

001 <script src=”js/jquery.js”></script>
002 <script src=”js/underscore.js”></script>
003 <script src=”js/intention.js”></script>
004 <script src=”js/context.js”></script>
005 <script src=”js/app.js”></script>

Intent attribute

To signify that Intention should update an element, we have to use the data-intent attribute as a keyword. You can just put ‘intent’ but it’d be nice if we could write with standards. Once intention.js knows to do something to an element, we then tell it what specifically we want to update.

001 <body data-intent>
002 </body>


In orientation

In the case of the body tag, we want to add a class of portrait or landscape each time it changes. Context.js gives us some default behaviour, one of which is detecting orientation so we can simply write ‘in-orientation:’ to add the class. Don’t panic, we have a look at the JavaScript behind this in more detail in the following step.

001 <body data-intent data-in-orientation:>
002 </body>


Orientation axis

This JavaScript is the heart of intention.js, and it’s known as an axis. The ID value is how we call it with in-ID or in this case ‘in-orientation’. Contexts is an array of objects, the name is the class name that will be applied and the value attached to it will be used when measuring it.

001 var orientation_axis = intent.responsive({
002    ID:’orientation’,
003    contexts: [{name:’portrait’, rotation: 0},
004        {name:’landscape’, rotation:90}], 
005    measure: function() {
006        return Math.abs(window.orientation);
007    }, /* next step */
008 });

Matcher method

The measure method runs the test and the matcher method returns a true or false value depending on if a context’s value passes the test against the measurement (eg the orientation value). We’re doing a comparison between window.orientation and each of the contexts: portrait (0) and landscape (90).

001 matcher: function(measure, ctx){
002    return measure === ctx.rotation;
003 }

Respond to change

Following on from the orientation axis, if window.orientation is 90 then it’ll add the class of ‘landscape’ to all elements that have data-in-orientation on them, and when it’s 0 the class will switch ‘landscape’ out with ‘portrait’. This isn’t completely magic though, context.js still adds an event listener calling respond(), which re-evaluates the measure and then calls the matcher.

001 //call immediately
002 orientation_axis.respond();
003 //also call on orientation change
004 $(window).on(‘orientationchange’, orientation_axis.    respond);

Now playing markup

We’ve now got a class to dynamically change on orientation. That’s great and all, but you don’t need a 15kb library to do that for you. To show how versatile Intention is we’ll build a music player, starting with the markup for showing what’s currently playing. This is all standard HTML5 markup with some ‘hooks’ for JavaScript.

001 <section class=”now-playing cf”>
002    <div class=”left-column column”>
003        <img class=”artwork” src=”” alt=”Album         artwork”>
004    </div>
005    <div class=”right-column column”>
006        <p class=”song”></p>
007        <p class=”album”></p>
008        <p class=”artist”></p>
009    </div>
010   </section>

Player controls markup

We’ll add markup for the player controls too. These classes relate to icons in Font Awesome (fortawesome.github.io/Font-Awesome). The main control here is the Play button, which will become a Pause when clicked. ‘In-playing’ doesn’t exist yet – we’ll be writing a custom axis for it shortly.

001 <section class=”player-controls”>
002    <i class=”library-icon icon-reorder”></i>
003    <i class=”icon-step-backward previous control”></i>
004    <i class=”play icon-play control” data-intent data-in-    playing:></i>
005    <i class=”icon-step-forward next control”></i>
006    <div class=”volume-container”></div>
007 </section>
008 <audio></audio>

Custom axis

To make a new axis, we pass intent.responsive() as an object with an ID, an array of contexts: the name will be used for the class and the value can be anything to match against. In this case it’ll be true or false but it could be a number or anything else. Calling .respond() will evaluate it immediately.

001 var playing = intent.responsive({
002    ID: ‘playing’,
003    contexts: [{
004        name: ‘icon-pause’,
005        val: true
006    }, {
007        name: ‘icon-play’,
008        val: false
009    }],
010    //next two steps
011 }).respond();

Measure method

Within our custom axis requires a measure function, some sort of test that our contexts will be measured against. We’re going to write a simple music player that will have a playing method, so that’ll be our measure for if it’s playing or not. If you grasp this concept, then intention.js will be a snap for you.

001 measure: function () {
002    return webPlayer.playing();
003 },    

Matcher method continued

The matcher is run each time respond() is called. Its job is to check if a context should be applied by returning true. Here we’re saying ‘return true if the context  passed is the same as the playing state or false if it doesn’t’. So if what the measure function returns is true, then the icon-play context is in use.

001 matcher: function (measure, context) {
002    return context.val === measure;
003 }

Public interface

Now that we’ve dealt with intention.js a fair bit, next we’ll build up our actual web player through the imaginatively titled ‘webPlayer’. It’ll be very simple so we only have Play, Pause and Volume controls. The demo on the resource disc included with this issue also has seeking and a countdown if you’d like to see how those work.

001 var webPlayer = (function () {
002    return {
003        playing: function () {
004            return playing;
005        },
006        play: play,
007        pause: pause,
008        volume: volume,
009        getVolume: getVolume
010    };
011 })();

Play function

The Play function sets the variable ‘playing’ to true and starts playing the <audio> element. We can access our playing axis out of scope by using intent.axes, which contains all of the axes that have been added. Using this, we can force the playing axis to re-evaluate itself and switch to the Pause class.

001 var play = function () {
002    playing = true;
003    setSong(currentSong);        
004    player.play();
005    intent.axes.playing.respond();
006    return playing;
007 };

Pause function

We do the opposite with our Pause method; set playing to false, invoke Pause on the player (the <audio> element) and force the playing axis to respond. This will ‘measure’ the playing variable so as it’s now false it’ll sw
itch back to the Play icon. Pretty simple stuff.

001 var pause = function () {
002    playing = false;
003    player.pause();
004    intent.axes.playing.respond();
005    return playing;
006 };

Volume axis

So we’ve got the Play and Pause buttons working. Now we’ll apply the axis pattern again, except this time we’ll deal with more than a simple Boolean. To step it up a gear we’ll measure against three values to show a dynamic volume icon. The order here is important as it goes from highest to lowest.

001 var volume = intent.responsive({
002    ID: ‘volume’,
003    contexts: [{
004        name: ‘icon-volume-up’,
005        val: 0.5
006    }, {
007        name: ‘icon-volume-down’,
008        val: 0.01
009    }, {
010        name: ‘icon-volume-off’,
011        val: 0
012    }],
013 }).respond();

Volume matcher/measure

Matcher will iterate through each context and see if its value (.val) is greater than or equal to the measure (the current volume, as returned through webPlayer). It will stop iterating when one of the matchers returns true (which is why the order is important). The measure function will get the current value of the volume bar for you.

001 matcher: function (measure, context) {
002    return measure >= context.val;
003 },
004 measure: function () {
005    return webPlayer.getVolume();
006 }

Intent events

You can also listen for events within Intention. An intent is fired whenever a context returns true, context in an axis returns true, or when a specific axis passes any context. Below is an example of each – note the colon to differentiate between always returning true and passing any context.

001 intent.on('volume', volume.respond);
002 intent.on('volume:icon-volume-up', volume.respond);
003 intent.on('playing:', playing.respond);

Set volume

volumeBar is a range input (<input type=”range”>) and we can read its current value with .value. Once this is set, we get the volume axis to re-evaluate with respond(). This’ll cause the matcher to iterate through the contexts and update the class name depending on which context returns true first.

001 var volume = function () {
002    player.volume = volumeBar.value;
003    intent.axes.volume.respond();
004    return player.volume;
005 };

Get the volume

The getVolume method will be a simple wrapper for returning the value of the volume from the player. Unfortunately we can’t use the volume() method because this would cause an infinite loop of setting, responding, returning a value and then responding again. This is the method that the measure function of the volume context will use.

001 var getVolume = function () {
002    return player.volume;
003 };

Volume markup

The HTML for the range input is mostly standard but the attributes from data-intent onwards dictate how this snippet of HTML should behave when within a certain context (the mobile and tablet contexts). When we’re in a mobile context, append the volume to volume container, which will make it vertical and in tablet and up put it outside (after) the volume container.

001 <input class=”volume” name=”volume” type=”range”     min=”0”     max=”1” step=”0.001” data-intent data-in-mobile-    append=”.    volume-container” data-in-tablet-after=”.volume-container”>

Before and prepend

As well as -after and -append there are sibling commands -before and -prepend. Although these four may sound limiting, they cover the majority of bases when manipulating the DOM. The main bonus of manipulating it this way is that Intention manages it for you so you don’t have to manually store the element in different states.

001 <div class=”time” in-mobile-before=”.now-playing” in-    tablet-prepend=”.player-controls”>
002    <span class=”time-now”></span><span class=”total-    time”></span>
003 </div>

Intent on simplicity

In this tutorial we’ve started to explore just how adaptable intention.js can be by responding to events in a music player as well as responding to different widths, simply by adding HTML attributes to describe how it should act. If you’re looking for a simple solution, especially on retro responsive builds, then this might be the tool for you.

×