REAL WORLD AngularJS
Yaniv Efraim
About Myself
I am a senior Front End developer @Supersonic
Developing for the web for more than 10 years
Love Javascript
Live slides: http://tinyurl.com/angularil-realworld
Agenda
AngularJS pro tips
Live Code & Demo
Q&A
Pro Tips
Part 1
Tip 1: Application structure best practices
app/
app.module.js
app.config.js
controllers/
attendees.js
session-detail.js
...
directives/
calendar.directive.js
calendar.directive.html
...
services/
dataservice.js
localstorage.js
...
views/
attendees.html
session-detail.html
sessions.html
...
Folder-by-type (Avoid)
Folder-by-Feature
app/
----- components/ //reusable components
---------- sidebar/
--------------- sidebar.module.js
--------------- sidebarDirective.js
--------------- sidebarView.html
--------------- _sidebar.scss
--------------- sidebar_test.js
---------- article/
--------------- article.module.js
--------------- articleDirective.js
--------------- articleView.html
--------------- ...
----- pages/ // acts as pages or partials of our site
---------- home/
--------------- home.module.js
--------------- homeController.js
--------------- homeService.js
--------------- homeView.html
--------------- _home.scss
--------------- home_test.js
----- app.module.js
----- app.routes.js
index.html
Use many small, self contained modules
Each component should have its own module:
---------- home/
--------------- home.module.js
--------------- home.controller.js
--------------- home.service.js
--------------- home.view.html
--------------- home.css
--------------- home_test.js
angular.module('home',[]);//home.module.js
angular.module('home').controller('HomeCtrl', HomeCtrl);//home.controller.js
angular.module('home').factory('homeService', homeService);//home.service.js
Use many small, self contained modules
Root module should be a container referencing all depending modules:
angular.module('app',[
//Angular Modules
'ngRoute',
//Custom modules
'sidebar',
'article',
'home',
'blog',
//3rd Party Modules
'ui.bootstrap'
]);
Advantages
- Dramatically easier to find what you are looking for
- Modules are reusable
- Easier for several developers to work on same project
- Easier to remove components
- Application can grow easily
Further reading:
Tip 2: Each controller should mind his own business!
$scope inheritance == Semi-Global Data
-
Hard to track where data is coming from (data is shared between controllers)
-
Scope's value changes can affect places you did not intend to affect.
-
Harder to refactor
-
The "dot rule" (example in few slides)
- Controllers are coupled
Solutions
-
Prefer using ControllerAs syntax (Angular 1.2+).
This will create new "isolated" context for your
controller (which is not affected by parent controllers)
- Use "Components Pattern"
Using ControllerAs syntax, we are creating an instance:
<div ng-controller="MainCtrl as main">
{{ main.title }}
</div>
angular.module('myApp', [])
.controller("MainCtrl", MainCtrl);
function MainCtrl() {
this.title = "My Title";
this.sendMessage = function() { };
}
Solution 1: ControllerAs
ControllerAs
Or even better:
<div ng-controller="MainCtrl as main">
{{ main.title }}
</div>
angular.module('myApp', [])
.controller("MainCtrl", MainCtrl);
function MainCtrl() {
var vm = this;// vm stands for 'viewmodel'
vm.title = "My Title";
vm.sendMessage = function() { };
}
How ControllerAs is implemented behind the scenes
ControllerAs - Nested controllers (the "dot rule")
(Problem)
(Solution)
Demo
Other benefits to this approach
-
Removes the use of $scope when no need for special operations (like $scope.$broadcast). This is a good preparation for AngularJS V2.
-
Helps avoid the temptation of using $scope methods inside a controller when it may otherwise be better to avoid them or move them to a factory
-
Syntax is closer to that of a "vanilla" JavaScript constructor
Solution 2: Components Pattern
Avoid using controllers. Use directives instead:
Further reading:
Tip 3: Use Angular Services as your "Model"
Why business logic / app state inside controllers is bad?
-
Controllers instantiated for each view and dies when the view unloads
-
Controllers are not reusable - they are coupled with the view
- Controllers are not meant to be injected
Controller's job is to talk to the "Model"
(don't use it as a model)
angular.module('Store', [])
.controller('OrderCtrl', function(Products) {
this.products = Products.query();
this.items = [];
this.addToOrder = function(item) {
this.items.push(item);
};
this.removeFromOrder = function(item) {
this.items.splice(this.items.indexOf(item), 1);//-->Business logic inside controller
};
this.totalPrice = function() {
return this.items.reduce(function(memo, item) {
return memo + (item.qty * item.price);//-->Business logic inside controller
}, 0);
};
});
Delegate business logic to a Service
angular.module('Store')
.factory('Order', function() {
var add = function(item) {
this.items.push(item);
};
var remove = function(item) {
if (this.items.indexOf(item) > -1) {
this.items.splice(this.items.indexOf(item), 1);
}
};
var total = function() {
return this.items.reduce(function(memo, item) {
return memo + (item.qty * item.price);
}, 0);
};
return {
items: [],
addToOrder: add,
removeFromOrder: remove,
totalPrice: total
};
});
Now the controller is doing its job: talking to the Model
angular.module('Store', [])
.controller('OrderCtrl', function(Products, Order) {
this.products = Products.query();
this.items = Order.items;
this.addToOrder = function(item) {
Order.addToOrder(item);
};
this.removeFromOrder = function(item) {
Order.removeFromOrder(item);
};
this.totalPrice = function() {
return Order.total();
};
});
Further reading:
Tip 4: Avoid Globals
var app = angular
.module('app')
.controller('HomeController', HomeController);
function HomeController(){
var vm = this;
vm.name = 'my name';
};
(How many globals do you see here?)
Use Immediately Invoked Function Expression (IIFE)
(function(){
var app = angular
.module('app')
.controller('HomeController', HomeController);
function HomeController(){
var vm = this;
vm.name = 'my name';
};
})();
TIP 5: Use one time databinding when possible
For AngularJS > 1.3 use "one-time binding"
Use "bindonce.js" for AngularJS <= 1.2 (https://github.com/Pasvaz/bindonce)
AngularJS 1.3+ "one time binding"
One-time expressions will stop recalculating once
they are stable, which happens after the first digest…
<p>Hello {{name}}!</p>
Becomes:
<p>Hello {{::name}}!</p>
Demo
Anguler 1.3+ :
Anguler <= 1.2 :
Tip 6: Automate minification annotation using ng-Annotate (Grunt/Gulp)
ng-annotate
app.controller('MyController',
['serviceA', 'serviceB', function(serviceA, serviceB){
...
}])
Will become:
app.controller('MyController', myController);
function myController(serviceA, serviceB){
...
}
ng-annotate will automatically add:
myController.$inject = ["serviceA", "serviceB"];
The usual controller decleration:
ng-annotate
For complex scenarios explicitly use the @ngInject
app.controller('MyController', myController);
function myController(){
...
}
/* @ngInject */
myController.prototype.methodA = function(serviceA, serviceB){
...
}
ng-annotate will recognise the need for injection and add:
myController.prototype.methodA.$inject = ["serviceA", "serviceB"];
Tip 7: Precache html templates using html2js (Grunt/Gulp)
html2js
-
html2js parses all template files in your application and creates a javascript file that populates the $templateCache
-
The output is an Angular module that can be injected as a dependency into the main app module
- The output javascript file can be concatenated into the main js file + minified + GZiped
html2js usage (example)
html2js output file example (auto-generated by html2js task):
angular.module("compiled-templates", []).run(["$templateCache", function($templateCache) {
$templateCache.put("all-templates",
"<script type=text/ng-template id=my-directive-template.html><div ng-controller=\"SomeCtrl\" class=\"some-class\" ng-click=\"performeClick()\">\n" +
" <div class=\"div-class\">\n" +
" ...
" </div>\n" +
" </div></script>);
});
Templates module should be added to main module:
angular.module('myApp', [ 'compiled-templates', /* other dependencies */ ]);
html2js usage (example)
Compile generated templates in your main controller:
var templatesHTML = $templateCache.get('all-templates');
$compile(templatesHTML)($rootScope);
Reference your templates as usual:
myApp.directive("myDirective", [function () {
return{
scope:{},
restrict: 'E',
templateUrl: 'my-directive-template.html',
link: function(scope, element, attrs){
},
controller: ['$scope', function($scope){
}]
};
}]);
Further reading:
Tip 8: Use Route's Resolve
app.config(function($routeProvider) {
var getName = function(NameService) {
return NameService.getName();
};
$routeProvider
.when('/profile', {
templateUrl: '/profile.html',
controller: 'ProfileController',
/* only navigate when we've resolved these promises */
resolve: {
name: getName,
profile: getProfile,
anythingElse: getAnythingElse
}
})
});
Tip 9: Cache service calls
var dataPromise = null;
function getData() {
if (dataPromise == null)
dataPromise = $http.get("data.json").then(function (res) {
return res.data;
});
return dataPromise;
}
Tip 10: Decouple your Controllers/Directives
-
Use DI in order to access modules
-
Use pub/sub communication
- Data and state sharing should be via services
Bonus Tip! Prepare for Angular 2.0 in today's apps
-
Use Component-Based Architecture (a.k.a. use directives, not ng-controllers)
-
Reduce dependency on $scope
-
Be modular
-
Use AngularUI's ui-router
-
Don't use angular.element
Further reading: