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
ng-swissknife
By Andrea Stagi
ng-swissknife
How to build your collection of reusable components for AngularJS
- 2,993