rizens

From Angular ES5 Directive To Angular ES2015 Component (ES6)

By Oren Farhi on Jan 1, 2016

Recently, I started refactoring Echoes Player, my open source project, from angular ES5 to AngularJS with ES2015 (former ES6). I’m following several concepts and i’de like to share the process of converting an AngularJS directive  written with es5 to an AngularJS component using ES2015.

Why Using ES2015 With Angular 1

I think that one of the most important reasons to start using ES2015 with AngularJS is in preparing current AngularJS code to migration, when the time comes, to Angular (+2) code.

I’ve written before about 5 steps to prepare your AngularJS code to Angular (+2), and 3 more steps to follow as well.

ES2015 is the new and current standard for writing javascript. It has taken a very long time to close the spec, however, now that the spec is closed, more and more libraries, frameworks, blog posts and tutorials are using it.

ES2015 features a very nice collection of new syntax and new methods for achieving several operations in less code (sometimes), more readable code and new types for handling collections (like Map and Set).

Moreover, Angular (+2) is written with ES2015 and Typescript. This will assist in code migration to Angular (+2).

Lets Start!

1. Angular 1 With ES5

I’ll use one of the modules i’ve written in Echoes Player with ES2015, and will convert it to a directive/component with ES2015.

Echoes Player is a media player that is based on youtube api (it’s open source as well). In its layout, it consists a sidebar, a top search bar and a content area. The sidebar include the “now playing” playlist that lists the tracks that currently queued to play.

Since I added this feature fast, I didn’t create a module for it - I used angular’s directives (ng-repeat) and added few more properties to the controller of this scope.

“Now Playlist” Html Template

taken from the index.html file:

<ul id="user-playlists" class="nav nav-list xux-maker xnicer-ux user-playlists"
	ng-class="{
		'transition-in': vm.playlists.length,
		'slide-down': vm.showPlaylistSaver
	}"
	sv-root sv-part="vm.playlists"
	sv-on-sort="vm.updateIndex($item, $indexTo)"
	>
	<li class="user-playlist"
		ng-class="{ 'active': vm.nowPlaying.index === $index}"
		ng-repeat="video in vm.playlists | filter:vm.playlistSearch"
		sv-element>
		<a class="" title="{{:: video.snippet.title }}"
			ng-click="vm.playVideo(video, $index)">
			{{ $index + 1 }})
			<img class="video-thumb" draggable="false" ng-src="{{:: video.snippet.thumbnails.default.url }}" sv-handle title="Drag to sort">
			<span class="video-title">{{:: video.snippet.title }}</span>
			<span class="badge badge-info">{{:: video.time }}</span>
			<span class="label label-danger ux-maker" title="Remove From Playlist"
				ng-click="vm.remove($event, video, $index)"><i class="las la-remove"></i></span>
		</a>
	</li>
</ul>

Aside from using angular’s built-in directives, this tracks in this playlist are draggable (i’m using the angular-sortable-view module). The tracks can be removed from this list as well.

“Now Playlist” Controller

The controller for this template is defined above the “ul” element. This is the ”UserPlaylistsCtrl” that can be found in the user-playlists.ctrl.js:

;(function() {
  "use strict"

  angular.module("echoes").controller("UserPlaylistsCtrl", UserPlaylistsCtrl)

  /* @ngInject */
  function UserPlaylistsCtrl($http, YoutubePlayerSettings, UserPlaylists) {
    var vm = this
    vm.title = "UserPlaylistsCtrl"
    // used by "Now Playlist"
    vm.playlists = YoutubePlayerSettings.nowPlaylist
    vm.playVideo = playVideo
    vm.nowPlaying = YoutubePlayerSettings.nowPlaying
    // used by "Now Playlist"
    vm.playlistSearch = ""
    // used by "Now Playlist"
    vm.remove = remove
    vm.clearPlaylist = YoutubePlayerSettings.clear
    vm.togglePlaylistSaver = togglePlaylistSaver
    vm.showPlaylistSaver = false
    vm.onPlaylistSave = onPlaylistSave
    // used by "Now Playlist"
    vm.updateIndex = updateIndex

    function playVideo(video, index) {
      vm.nowPlaying.index = index
      YoutubePlayerSettings.playVideoId(video)
    }

    function remove($event, video, index) {
      $event.stopPropagation()
      YoutubePlayerSettings.remove(video, index)
    }

    function togglePlaylistSaver() {
      vm.showPlaylistSaver = !vm.showPlaylistSaver
    }

    function onPlaylistSave() {
      togglePlaylistSaver()
      UserPlaylists.list()
    }

    function updateIndex($item, $indexTo) {
      if ($item.id === vm.nowPlaying.media.id) {
        vm.nowPlaying.index = $indexTo
      }
    }
  }
})()

This controller serves other purposes beside the now playlist feature. The relevant properties and functions that the now playlist uses, are marked with a comment above it.

2. Decisions Taken Before Converting ES5 to Es2015

The inspiration for refactoring the code comes from angular-class boilerplate of using angular with es2015. In the process of refactoring this code and converting it to ES2015, I had to decide how to isolate the several features in this area. Creating the ”Now Playlist” component is one of these decisions.

Defining A New Component For “Now Playlist”

First, I wanted to redefine the html template to be used as a web-component (or rather an html tag). After much thought, I came up with this component:

<now-playlist
  videos="nowPlaying.playlist"
  filter="nowPlaying.playlistSearch"
  on-select="nowPlaying.playVideo(video)"
  on-remove="nowPlaying.removeVideo($event, video, $index)"
  on-sort="nowPlaying.updateIndex($item, $indexTo)"
></now-playlist>

I decided to expose the relevant attributes in order to keep the logics in one “smart” component (the “now-playing” component) and keeping this component as stateless as possible.

Creating Files For “Now Playlist” Component

The next step was to generate the appropriate boilerplate of files for this component. For generating these files, I used “gulp-dogen” - an npm module I wrote for this repetitive task of generating directories and files for a certain purpose.

“gulp-dogen” takes the name of the component from a cli command, then it adds this name in all of the files where you specify it, and created a new directory with the new files in it.

Finally, I came up with these files:

index.js
now - playlist.component.js
now - playlist.ctrl.js
now - playlist.less
now - playlist.tpl.html

I’m using the ”index.js” notation, similar to node.js require syntax, so I can simply import the now-playlist component as such:

import NowPlaylist from "./now-playlist"
// instead of
import NowPlaylist from "./now-playlist/index.js"

index.js - Module & Directive Defintion

This file defines the angular module and its accompanied services and directives. This is also the place for importing any dependant modules - like the angular-sortable-view module. Finally, It exports the now-playlist module, so it can be consumed by other modules.

import angular from "angular"
import AngularSortableView from "angular-sortable-view/src/angular-sortable-view.js"
import nowPlaylist from "./now-playlist.component.js"

export default angular
  .module("now-playlist", ["angular-sortable-view"])
  .directive("nowPlaylist", nowPlaylist)

now-playlist.component.js - The Directive Definition

This file includes the directive/component definition for this module. Currently, I’m using angular v.1.4.8, where the new “component” syntax for creating an element directive isn’t included.

This file imports the controller and template of this directive from an external file. I defined this directive with all of the properties that will be included within the “component” syntax -

  • bindToController: true - binds external attributes to “this” context in the controller
  • restrict: ‘E’ - since it’s a “component” - it’s an element tag
  • replace: true - since there is no support for real web components yet
  • controllerAs: ‘nowPlaylist’ - this follows Angular (+2) convention as well - exposing this as the camel-case version of this element tag.

There will be less code in this file once the new “component” function is available. Also, it will need to export an object (json) rather than a function.

import NowPlaylistCtrl from "./now-playlist.ctrl.js"
import template from "./now-playlist.tpl.html"

/* @ngInject */
export default function nowPlaylist() {
  // Usage:
  //  <now-playlist></now-playlist>
  // Creates:
  //
  var directive = {
    template,
    controller: NowPlaylistCtrl,
    controllerAs: "nowPlaylist",
    scope: {
      videos: "=",
      filter: "=",
      nowPlaying: "=",
      onSelect: "&",
      onRemove: "&",
      onSort: "&",
    },
    bindToController: true,
    replace: true,
    restrict: "E",
  }
  return directive
}

now-playlist.ctrl.js - The Component’s Controller

This file includes the logics and view model for this component’s view. I used ES2015 “class” defintion, since controllers in AngularJS are created with the “new” keyword. Notice that “this” context, is overloaded with more properties that are defined as part of the scope. Apart from the “constructor” function, I created

/* @ngInject */
export default class NowPlaylistCtrl {
  /* @ngInject */
  constructor() {
    // injected with this.videos, this.onRemove, this.onSelect, this.filter, this.nowPlaying
    this.showPlaylistSaver = false
  }

  removeVideo($event, video, $index) {
    this.onRemove && this.onRemove({ $event, video, $index })
  }

  selectVideo(video, $index) {
    this.onSelect && this.onSelect({ video, $index })
  }

  sortVideo($item, $indexTo) {
    this.onSort && this.onSort({ $item, $indexTo })
  }
}

now-playlist.tpl.html - The Component’s html

This file contains the html template that was in the index.html. Few things have changed:

  • The code now references to the ‘controllerAs’ alias “nowPlaylist”
  • The “ul” is wrapped with a “section” element
  • The “css” classes now reflects the correct meaning - “now-playlist”, “now-playlist-track”
<section class="now-playlist" ng-class="{
			'transition-in': nowPlaylist.videos.length,
			'slide-down': nowPlaylist.showPlaylistSaver
		}">
	<ul class="nav nav-list xux-maker xnicer-ux"

		sv-root sv-part="nowPlaylist.videos"
		sv-on-sort="nowPlaylist.sortVideo($item, $indexTo)"
		>
		<li class="now-playlist-track"
			ng-class="{ 'active': nowPlaylist.nowPlaying.index === $index}"
			ng-repeat="video in nowPlaylist.videos | filter:nowPlaylist.filter"
			sv-element>
			<a class="" title="{{:: video.snippet.title }}"
				ng-click="nowPlaylist.selectVideo(video, $index)">
				{{ $index + 1 }})
				<img class="video-thumb" draggable="false" ng-src="{{:: video.snippet.thumbnails.default.url }}" sv-handle title="Drag to sort">
				<span class="video-title">{{:: video.snippet.title }}</span>
				<span class="badge badge-info">{{:: video.time }}</span>
				<span class="label label-danger ux-maker remove-track" title="Remove From Playlist"
					ng-click="nowPlaylist.removeVideo($event, video, $index)"><i class="las la-remove"></i></span>
			</a>
		</li>
	</ul>
</section>

Eventually, I also moved the relevant css/less rules to the “now-playlist.less” file.

Usage of “Now Playlist” In A Broader Context

Finally, the area that contains the now-playlist and 2 other components, has been also refactored in the same way-

  • a small toolbar for filtering the playlist, clearing the tracks and save the playlist
  • a form component for typing a name to save the playlist to the current signed-in youtube’s user

This is the smart component ”now-playing” html template code (I still have work to do - this ng-if expression should be changed):

<div class="sidebar-pane">
  <now-playlist-filter
    playlist="nowPlaying.playlist"
    on-save="nowPlaying.togglePlaylistSaver(show)"
    on-clear="nowPlaying.clearPlaylist()"
    on-change="nowPlaying.onFilterChange(filter)"
  ></now-playlist-filter>
  <section
    class="playlist-saver-container clearfix"
    ng-if="nowPlaying.showPlaylistSaver && nowPlaying.playlist.length > 0"
  >
    <playlist-saver
      class="col-md-12"
      tracks="nowPlaying.playlist"
      on-cancel="nowPlaying.togglePlaylistSaver()"
      on-save="nowPlaying.onPlaylistSave()"
    ></playlist-saver>
  </section>
  <now-playlist
    videos="nowPlaying.playlist"
    filter="nowPlaying.playlistSearch"
    on-select="nowPlaying.playVideo(video)"
    on-remove="nowPlaying.removeVideo($event, video, $index)"
    on-sort="nowPlaying.updateIndex($item, $indexTo)"
  ></now-playlist>
</div>

Final Thoughts

The process of converting the code gave me the opportunity to restructure the app, rethink in a component base architecture and eliminate some of the code. Now, the code is more modularized, organized with components, defined with ES2015 and is ready for Angular (+2) - with some minimal changes I believe.

Moreover, this process should be taken step by step. It can be an iterative process and be applied to each component - one at a time.

The source code for Echoes Player with ES2015 is still a work in progress. The commits are documented in #84 as well.

However, this is not all. I plan on writing more articles on this process - as it contains much more preparation and adjustments.

I started gathering these concepts in an “Angular ES2015 Style Guide” - You are welcome to collaborate on this style-guide - suggest and add your thoughts.

Hi there, I'm Oren Farhi

I'm an Experienced Software Engineer, Front End Tech Lead, focusing on Front End & Software Architecture and creating well formed applications.

Profile Avatar
© 2024, Built by Oren Farhi