Angular Cli - Getting Started

Angular cli is a nice scaffolding tool to create quick production ready apps for Angularjs.

You can read official docs here on http://ngcli.github.io/

In this post we will start with installing Angular cli and will create a sample app to play songs from sound cloud using all goodies provided by Angular cli.

Installing

npm install -g angcli  


Getting app with SoundCloud

In order to make API requests to SoundCloud you need to create an app with them. Go to http://soundcloud.com/you/apps and register new application with them to obtain client_id.

Let's start by creating a new project named ngSound , which will play trending songs from sound cloud.

Note :- Idea behind this post to discover the good parts of Angular cli.

ng new ngSound  

We will use Javascript instead of CoffeeScript and angular-routes to manage routing.

If everything went fine , you will see a new project named ngSound created inside your workspace.

cd ngSound  

ngCli ships with a sample controller and template, let's delete them so that we can create our own templates and controllers.

rm -rf app/controllers/welcome.js  
rm -rf app/templates/welcome.html  

Also we need to install angular-media-player in order to play sounds. https://github.com/mrgamer/angular-media-player

bower install angular-media-player  

Now in order to make sure angular-media-player is part of vendor bundle , we need to add it to vendors array inside ngConfig.json

  "vendors": [
    {
      "angular-deferred-bootstrap": "angular-deferred-bootstrap"
    },
    {
      "angular": "angular"
    },
    {
      "angular-route": "angular-route"
    },
    {
      "angular-media-player":"angular-media-player"
    }
  ],

Finally we will start with creating a new controller for our home page.

ng generate controller home  

will create our new controller inside app/controllers and test for this controller inside tests/spec/controllers.

Finally we have everything in place , we will get our hands dirty by creating template of our home view.

Let's create app/templates/home.html file with following markup.

<div class="sc-app">  
  <div class="sc-container">

    <div class="sc-player wrap">
      <div class="table">
        <div class="table-cell">
          <button class="sc-button">
            <svg viewBox="0 0 32 32" style="max-height:100%" fill="currentColor"><path d="M0 0 H4 V14 L32 0 V32 L4 18 V32 H0 z"></path></svg>
          </button><!-- end sc-button -->
        </div>
        <div class="table-cell">
          <button class="sc-button">
            <svg viewBox="0 0 32 32" style="max-height:100%" fill="currentColor"><path d="M0 0 L32 16 L0 32 z"></path></svg>
            <svg viewBox="0 0 32 32" style="max-height:100%" fill="currentColor"><path d="M0 0 H12 V32 H0 z M20 0 H32 V32 H20 z"></path></svg>
          </button>
        </div>
        <div class="table-cell">
          <button class="sc-button">
            <svg viewBox="0 0 32 32" style="max-height:100%" fill="currentColor"><path d="M0 0 L28 14 V0 H32 V32 H28 V18 L0 32 z"></path></svg>
          </button>
        </div>
        <div class="table-cell full-width">
          <div class="progress-wrap">
            <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100"></div>
          </div><!-- end progress-wrap -->
        </div>
      </div><!-- end table -->
      <div class="sc-duration">
        <span>00:00</span> - <span>00:00</span>
      </div><!-- end sc-duration -->
    </div>
    <div class="sc-playlist wrap">
      <ul>
        <li>
          <a href="">
            <span class="index">0</span>
            Placeholder for song title
          </a>
        </li>
      </ul>
    </div><!-- end sc-playlist -->
  </div><!-- end sc-container -->
</div><!-- end sc-app -->  

Above markup is static and does not contain any angular specific code. It is a simple markup to create playlist and audio player controls.

Also copy below css to your app.css file, i am not spending time to explain css or html as it is pretty much straight forward.

body {  
  background: #203036;
  font-family: 'Open Sans', sans-serif;
  color: #f52;
}

.sc-container {
  margin: auto;
  width: 760px;
  position: relative;
}

.table {
  display: table;
  width: 100%;
}

.table-cell {
  display: table-cell;
  vertical-align: middle;
}
.table-cell.full-width {
  width: 100%;
}

.wrap {
  background: #1a272b;
}

.sc-button {
  font-family: inherit;
  font-weight: 500;
  letter-spacing: 0.1rem;
  line-height: 2.5rem;
  text-decoration: none;
  padding: 0.25rem 1rem;
  display: inline-block;
  color: #f52;
  background-color: transparent;
  border: 0;
  border-radius: 3px;
  -webkit-transition-property: color, background-color, box-shadow, -webkit-transform;
  transition-property: color, background-color, box-shadow, transform;
  -webkit-transition-duration: 0.1s;
  transition-duration: 0.1s;
  -webkit-transition-timing-function: ease-out;
  transition-timing-function: ease-out;
  cursor: pointer;
  margin-left: 10px;
}
.sc-button:focus {
  outline: none;
}
.sc-button:hover {
  box-shadow: inset 0 0 0 3px #f52;
}
.sc-button svg {
  width: 1rem;
  height: 1rem;
  position: relative;
  top: 0.1875rem;
  top: 0.21875rem;
}
.sc-button:active {
  -webkit-transform: scale(0.925);
  -ms-transform: scale(0.925);
  transform: scale(0.925);
}
.sc-button:active svg {
  -webkit-transform: scale(0.925);
  -ms-transform: scale(0.925);
  transform: scale(0.925);
}

.progress-wrap {
  position: relative;
  top: 2px;
  width: 95%;
  cursor: pointer;
  height: 0.5rem;
  background-color: rgba(0, 0, 0, 0.8);
}
.progress-wrap .progress {
  display: block;
  height: 0.5rem;
  border: 0;
  width: 0px;
  background-color: #f52;
  cursor: pointer;
  transition: width 0.6s ease;
}

.sc-player {
  padding: 1rem 0;
}

.sc-playlist {
  margin-top: 20px;
}
.sc-playlist ul {
  margin: 0;
  padding: 10px 0px;
}
.sc-playlist ul li {
  list-style: none;
}
.sc-playlist ul li a {
  display: block;
  color: #f52;
  text-decoration: none;
  font-weight: 500;
  padding-top: 0.5rem;
  padding-left: 1.5rem;
  padding-right: 1rem;
  padding-bottom: 0.5rem;
}
.sc-playlist ul li a .index {
  margin-right: 1rem;
}
.sc-playlist ul li a:hover {
  background: #263941;
}
.sc-playlist ul li a.active {
  background: #263941;
}

.sc-duration {
  text-align: right;
  color: #737373;
  margin-right: 20px;
  top: -11px;
  position: relative;
  font-size: 13px;
}

Finally register this new route and controller to our app/routes.js file.

var routeMap;

routeMap = [  
  {
    url: '/',
    templateUrl: "home.html",
    controller: 'homeCtrl'
  }
];

module.exports = routeMap;  

Start Angular cli in built server to see how things are going to look.

ng serve  

This is how it may look , not pretty but good enough to get us started.

Angular cli DI philosphy

Angular cli let you extend boot method inside your app/app.js to inject 3rd party modules to your angular app. In our case we need to inject mediaPlayer to make use of angular-media-player.

Open app/app.js

First we will require angular-media-player after requiring boot file.

require("angular-media-player");  

and then extend boot method by passing an array of dependencies.

boot(["mediaPlayer"]);  

This is how your app/app.js will look like

var boot;

boot = require("../core/boot");  
require("angular-media-player");  
boot(["mediaPlayer"]);  
require("./templates");  
require("./controllers");

If everything goes fine till now , it means you are ready to make calls to SoundCloud in order to fetch trending tracks.

Using Constants

Angular cli makes it convenient to define constants and we define them inside app/constants.js file.

var CONSTANTS;

CONSTANTS = {  
  'base_url':'http://api.soundcloud.com/tracks',
  'filter': 'trending',
  'client_id': '<Your client id>'
};

module.exports = CONSTANTS;  

By defining constants in this we keep our configuration dry. now in order to access these constants we need to use a special syntax provided by Angular cli.

{@= LET.base_url @} outputs http://api.soundcloud.com/tracks

Isn't that convenient enough ?

Pay Attention

  • First we need to know the url , where we will request to fetch tracks from soundcloud and it will look something like this. http://api.soundcloud.com/tracks?filter=trending&limit=20&clientid=yourclient_id
  • Your playlist object should have key called src in order to make use of angular-media-player.
  • So we need a service to fetch tracks and convert to desired array which will be consumed by angular-media-player.

Creating Factory to make api call.

ng generate factory sounds  

We generate a new factory named sounds , which get available to us inside store/sounds.js

Delete sample data from factory and it should something like this now.

module.exports = /* @ngInject */ function($http,$q){  
  var factory = {};
  factory.fetchTracks = function(){
  }
  return factory;
}

We have injected $http and $q to make $http request and then returning a promise from this service.

Creating url

We will use constants to create final url.

var soundsUrl = "{@= LET.base_url @}?filter={@= LET.filter @}&client_id={@= LET.client_id @}&limit=20";

// becomes http://api.soundcloud.com/tracks?filter=trending&limit=20&client_id=<your client id>


Making http request

We can simply return the $http request from this function , which itself returns a promise, but remember we need to refactor our object to make it compatible with angular-media-player.

$http.get(soundsUrl)
.success(function(success){
    var playlist_object = [];
    angular.forEach(success,function(values,keys){
        var track = {src:values.stream_url+'?client_id={@= LET.client_id @}',title:values.title};
        playlist_object.push(track);
    });
    deferred.resolve(playlist_object);
}).error(function(err){
    deferred.reject(err);
});

Your soundsFactory should look something like this at the end

/**
  @author
  @description
  @name soundsFactory
*/
module.exports = /* @ngInject */ function($http,$q){  
  var factory = {};
  factory.fetchTracks = function(){
    var soundsUrl = "{@= LET.base_url @}?filter={@= LET.filter @}&client_id={@= LET.client_id @}&limit=20";
    deferred = $q.defer();
    $http.get(soundsUrl).success(function(success){
      var playlist_object = [];
      angular.forEach(success,function(values,keys){
        var track = {src:values.stream_url+'?client_id={@= LET.client_id @}',title:values.title};
        playlist_object.push(track);
      });
      deferred.resolve(playlist_object);
    }).error(function(err){
      deferred.reject(err);
    });
    return deferred.promise;
  }
  return factory;
}

Next we need to inject to this service to our controller , so that we can use it.

Uncomment below line inside app/app.js

require("./store");  

inject soundsFactory inside app/controllers/home.js and we define tracks to $scope variable.

module.exports = /* @ngInject */ function($scope,soundsFactory){  
  soundsFactory.fetchTracks().then(function(success){
    $scope.tracks = success;
  }).catch(function(error){
    console.log(error);
  });
}

ng-repeat time

Let's loop through all the tracks and display a nice playlist of songs.

Open app/templates/home.html

Replace list item with Placeholder message with below ng-repeat loop.

<ul>  
    <li ng-repeat="track in tracks">
        <a href="">
            <span class="index">{{$index}}</span>
            {{::track.title}}
        </a>
    </li>
</ul>  

Which will look like the below image

So we have our tracks now . WOW !

Let's makes use of angular-media-player to add functions to our controls and play songs.

Dropping audio element

We need audio element on the page to play music.

<audio media-player="mediaPlayer" data-playlist="tracks">  
</audio>  

Adding play pause method to our playlist anchor tags, so that whenever we click on an element it should play that track.

<a href="" ng-click="mediaPlayer.playPause($index)">  
    <span class="index">{{$index}}</span>
    {{::track.title}}
</a>  

If you click on any of the songs it will start playing, but audio player is of no use so far.

Deciding which icon to show

Adding ng-if to play and pause icons , so that only one shows at a time.

<button class="sc-button">  
    <svg ng-if="!mediaPlayer.playing" viewBox="0 0 32 32" style="max-height:100%" fill="currentColor" class="plangular-icon plangular-icon-play">
        <path d="M0 0 L32 16 L0 32 z"></path>
    </svg>
    <svg ng-if="mediaPlayer.playing" viewBox="0 0 32 32" style="max-height:100%" fill="currentColor" class="plangular-icon plangular-icon-pause">
        <path d="M0 0 H12 V32 H0 z M20 0 H32 V32 H20 z"></path>
       </svg>
</button>


Adding control to play , next and previous buttons.

<button class="sc-button" ng-click="mediaPlayer.prev()">  
</button>  
<button class="sc-button" ng-click="mediaPlayer.playPause()">  
</button>  
<button class="sc-button" ng-click="mediaPlayer.next()">  
</button>  


Making seekbar functional

<div class="progress-wrap">  
  <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" ng-style="{ width: mediaPlayer.currentTime*100/mediaPlayer.duration + '%' }"></div>
</div><!-- end progress-wrap -->  


Show duration

<div class="sc-duration">  
    <span>{{mediaPlayer.formatTime}}</span>
    - <span>{{mediaPlayer.formatDuration}}</span>
</div><!-- end sc-duration -->


Showing active track on playlist

<a href="" ng-class="{'active':mediaPlayer.currentTrack == $index+1}" ng-click="mediaPlayer.playPause($index)">  
    <span class="index">{{$index}}</span>
    {{::track.title}}
</a>  


Attach click events to seekbar

We will attach click event to seekbar , so that we can move forward and backward while playing track.

<div class="progress-wrap" ng-click="mediaPlayer.seek(mediaPlayer.duration * seekPercentage($event))">  
....
</div>  


Adding seekPercentage method to controller.

app/controllers/home.js

$scope.seekPercentage = function ($event) {
    var percentage = ($event.offsetX / $event.target.offsetWidth);
    if (percentage <= 1) {
      return percentage;
    } else {
      return 0;
    }
  };

And boom