HOW TO BUILD YOUR COLLECTION OF REUSABLE Components for AngularJS

By Andrea Stagi, deveLover @ Nephila

With great works comes big libraries

 

(Stan Lee)

I can get... satisfaction!

(Rolling Stones)

LEarning

Something made by me

You fritter and waste the hours in an offhand way

(Pink Floyd)

A lot of time required To SET up, develop and maintain

A lot of time saved time reusing code

Sweet library O'Mine!

(Guns 'n' Roses)

You've got the power!

No need to fork, modify and understand code made by others

Changes can be discussed among the team members

ng-nephila

https://github.com/nephila/ng-nephila

Let's start coding the simplest element EVER: a filter!

angular.module('ngNephila.filters.range', [])
.filter('nphRange', function(){
  return function(start, end, step) {
    var res = [];
    // DO STUFF
    return res;
  };
});
angular.module('ngNephila.filters.range', [])
.filter('nphRange', function(){
  return function(start, end, step) {
    var res = [];
    var leftToRight = true;
    if (step < 1) {
      throw new Error('Step parameter must be >= 1');
    }
    step = ( step === undefined || step === null ) ? 1 : step;
    if (end === undefined || end === null) {
      end = start;
      start = 0;
    }
    if (start > end) {
      var aux = end;
      end = start;
      start = aux;
      leftToRight = false;
    }
    for (var i = start; i < end; i+=step) {
      if (leftToRight) {
        res.push(i);
      } else {
        res.unshift(i);
      }
    }
    return res;
  };
});

nph what? 

Custom namespace is important!

ng-* (Angular)
UI-* (Angular UI)
HV-* (HireVUE)

Using Karma for testing

Write a simple test for FILTERs

describe('Filter: range', function () {

  beforeEach(module('ngNephila.filters.range'));

  var range;

  beforeEach(inject(function($filter) {
    range = $filter('nphRange');
  }));

  it('should return consider the step when present', function () {
    expect(range(6, 12, 2)).toEqual([6, 8, 10]);
    expect(range(12, 6, 2)).toEqual([10, 8, 6]);
  });

  it('has a range filter', function () {
    expect(range).not.toBeNull();
  });

  it('should raise an exeption if step < 1', function () {
    expect(function() {
      range(6, 12, -1);
    }).toThrow();
  });
});

Setup GULP FILE for testing

gulp.task('test-src', function () {
  return gulp.src(testFiles)
    .pipe(karma({
      configFile: 'karma.conf.js',
      action: 'run',
      browsers: ['Chrome']
    }));
});
var testFiles = [
  'bower_components/moment/min/moment-with-locales.min.js',
  'bower_components/jquery/dist/jquery.min.js',
  'bower_components/angular/angular.min.js',
  'bower_components/angular-mocks/angular-mocks.js',
  'src/**/*.js',
  'template/**/*.html.js'
];

Coverage

gulp.task('travis-src', function () {
  return gulp.src(testFiles)
    .pipe(karma({
      configFile: 'karma.conf.js',
      action: 'run',
      reporters: ['dots', 'coverage', 'coveralls'],
      browsers: ['Firefox'],
      coverageReporter: {
        type: 'lcov',
        dir: 'coverage/',
        subdir: '.',
      }
    }));
});

Services, Factories, Providers

Service

angular.module('ngNephila.services.pathJoin', [
  'ngNephila.filters.path'
])
.service('nphPathJoin', function(pathFilter) {
  // IMPLEMENT IT USING 'this'
});

Factory

angular.module('ngNephila.services.pathJoin', [
  'ngNephila.filters.path'
])
.factory('nphPathJoin', function(pathFilter) {
  return function() {
    // IMPLEMENTATION
  };
});

PROVIDER

angular.module('ngNephila.services.pagination', [])
.provider('nphPagination', function paginationProvider() {

  var itemsPerPage = 0;

  this.setItemsPerPage = function (extItemsPerPage) {
    itemsPerPage = extItemsPerPage;
  };

  function PaginatorFactory() {
    this.getPaginator = function () {
      return new Paginator();
    };
  }

  function Paginator() {
    // Paginator IMPLEMENTATION
  }

  this.$get = function() {
    return new PaginatorFactory();
  };
});

App cONFIG

var app = angular.module('demo', ['ngNephila']);

app.config(function(nphPaginationProvider) {
  nphPaginationProvider.setItemsPerPage(5);
});

Minification

angular.module('ngNephila.services.debounce', [])
.factory('nphDebounce', ['$timeout','$q', function($timeout, $q) {
  return function debounce(func, wait, immediate) {
    // IMPLEMENTATION
  };
}]);

HOW TO MANAGE External Dependencies

Do not create bundle with dependencies

Smaller library

No bad surprises

Wait a moment! NOW I need some code organization

.
├── components
│   ├── module.js
│   └── test
├── filters
│   ├── module.js
│   ├── range.js
│   └── test
│       └── range.spec.js
├── module.js
└── services
    ├── module.js
    └── test
angular.module('ngNephila.filters', [
  'ngNephila.filters.range',
  'ngNephila.filters.titlecase',
  'ngNephila.filters.stripHtml',
  'ngNephila.filters.strip',
  'ngNephila.filters.path'
]);
angular.module('ngNephila', [
  'ngNephila.filters',
  'ngNephila.services',
  'ngNephila.components'
]);
angular.module('ngNephila.filters', [
  'ngNephila.filters.range',
  'ngNephila.filters.titlecase',
  'ngNephila.filters.stripHtml',
  'ngNephila.filters.strip',
  'ngNephila.filters.path'
]);

Filter module.js

angular.module('ngNephila', [
  'ngNephila.filters',
  'ngNephila.services',
  'ngNephila.components'
]);

ROOT module.js

Directives

Reusable HTML component

<nph-paginator start="1" compress="2" number-of-items="numberOfItems"
on-page-change="pageChange(page)" next-label="Next" prev-label="Prev."
compress-label="...."></nph-paginator>

Restrict 'E'

angular.module('ngNephila.components.paginator', [
  'ngNephila.services.pagination',
  'ngNephila.filters.range',
  'ngNephila.tpls.paginator.paginator'
])
.directive('nphPaginator', [
  '$filter', 'nphPagination', function($filter, nphPagination) {
    return {
      restrict: 'E',
      // TO BE CONTINUED ...
    }
  }
]);

Reusable HTML behaviour

<input nph-focus-me="true"></input>

Restrict 'A'

angular.module('ngNephila.components.focusMe',[])
.directive('nphFocusMe', ['$timeout', function($timeout) {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      scope.$watch(attrs.nphFocusMe, function(value) {
        if(value === true) {
          $timeout(function() {
            element[0].focus();
          });
        }
      });
    }
  };
}]);

Directives with templates

How Angular DOWNLOADS TEMPLATES 

$http.get(templateUrl, {cache: $templateCache})

What I USE: PREFILLED DEFAULT TEMPLATE AND TEMPLATEURL ATTRIBUTE 

angular.module('ngNephila.components.paginator', [
    //......
])
.directive('nphPaginator', [
  '$filter', 'nphPagination', function($filter, nphPagination) {
    return {
      //......
      templateUrl: function(elem,attrs) {
        return attrs.templateUrl || 'template/paginator/paginator.html';
      },
      //......
    }
]);

paginator.html template example

<ul>
  <li>
    <a href="" ng-click="paginator.prev()">{{prevLabel || "<<"}}</a>
  </li>
  <li ng-hide="canHide(i)" 
    ng-class="{active: i == paginator.getCurrentPage()}" 
    ng-repeat="i in pages">
    <a href="" ng-hide="isFirstCanHide(i)" 
       ng-click="paginator.goToPage(i)">{{i}}</a>
    <span ng-show="isFirstCanHide(i)">{{compressLabel || "..."}}</span>
  </li>
  <li>
    <a href="" ng-click="paginator.next()">{{nextLabel || ">>"}}</a>
  </li>
</ul>

html2js gulp task

gulp.task('html2js', function () {
  return gulp.src('template/**/*.html')
    .pipe(html2js({
      moduleName: function (file) {
        var path = file.path.split('/'),
            folder = path[path.length - 2],
            fileName = path[path.length - 1].split('.')[0];
        var name = 'ngNephila.tpls.' + folder;
        return name + '.' + fileName;
      },
      prefix: "template/"
    }))
    .pipe(rename({
      extname: ".html.js"
    }))
    .pipe(gulp.dest('template'))
});

Final Result!

(function(module) {
try {
  module = angular.module('ngNephila.tpls.paginator.paginator');
} catch (e) {
  module = angular.module('ngNephila.tpls.paginator.paginator', []);
}
module.run(['$templateCache', function($templateCache) {
  $templateCache.put('template/paginator/paginator.html',
    '<ul>\n' +
    '  <li>\n' +
    '    <a href="" ng-click="paginator.prev()">{{prevLabel || "<<"}}</a>\n' +
    '  </li>\n' +
    '  <li ng-hide="canHide(i)" ng-class="{active: i == paginator.getCurrentPage()}" ng-repeat="i in pages">\n' +
    '    <a href="" ng-hide="isFirstCanHide(i)" ng-click="paginator.goToPage(i)">{{i}}</a>\n' +
    '    <span ng-show="isFirstCanHide(i)">{{compressLabel || "..."}}</span>\n' +
    '  </li>\n' +
    '  <li>\n' +
    '    <a href="" ng-click="paginator.next()">{{nextLabel || ">>"}}</a>\n' +
    '  </li>\n' +
    '</ul>');
}]);
})();

})();

transclude: true

WTF?

angular.module('ngNephila.components.tabsaccordion', [])
.directive('nphTabsaccordion', function() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,

// ......
<nph-tabsaccordion>
  <nph-tabheaders>
    <nph-tabheader selected="true" ref="tab1">
      Tab 1
    </nph-tabheader>
    <nph-tabheader ref="tab2">
      Tab 2
    </nph-tabheader>
  </nph-tabheaders>
  <nph-tabcontents>
    <nph-tabcontent ref="tab1">
      Content 1
    </nph-tabcontent>
    <nph-tabcontent ref="tab2">
      Content 2
    </nph-tabcontent>
  </nph-tabcontents>
</nph-tabsaccordion>

The Result is Plain html, why?

Introducing REPLACE: true

angular.module('ngNephila.components.tabsaccordion', [])
.directive('nphTabsaccordion', function() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    replace: true,

// ......

Sub elements needs to access the parent's scope: introducing REQUIRE

.directive('nphTabheader', function() {
  return {
    scope: {
      ref: '@',
      selected: '='
    },
    require: '^nphTabsaccordion',
    restrict: 'E',
    transclude: true,
    replace: true,
    link: function(scope, element, attrs, tabsaccordion) {
      // .....
    },
    // ....
  };
})

TO BE CONTINUEd...

Make it modular! 

Improvements all night long!

ES6 porting (next DjangoBeer sorry!)

Better demo page (...)

That's all folks, thank you!

(Te Ende)

Twitter: @4stagi

Github: github.com/astagi

Email: a.stagi@nephila.it

Made with Slides.com