How to not program on AngularJS
limurezzz@gmail.com
Andrei Yemialyanchik
MVC directory structure
templates/
_login.html
_feed.html
app/
app.js
controllers/
LoginController.js
FeedController.js
directives/
FeedEntryDirective.js
services/
LoginService.js
FeedService.js
filters/
CapatalizeFilter.js
app/
app.js
Feed/
_feed.html
FeedController.js
FeedEntryDirective.js
FeedService.js
Login/
_login.html
LoginController.js
LoginService.js
Shared/
CapatalizeFilter.js
MVC? MVVM? MVW?
Having said, I'd rather see developers build kick-ass apps that are well-designed and follow separation of concerns, than see them waste time arguing about MV* nonsense. And for this reason, I hereby declare AngularJS to be MVW framework - Model-View-Whatever. Where Whatever stands for "whatever works for you".
Lead of the AngularJS
Global dependencies
var underscore = angular.module('underscore', []);
underscore.factory('_', function() {
return window._; //Underscore must already be loaded on the page
});
var app = angular.module('app', ['underscore']);
app.controller('MainCtrl', ['$scope', '_', function($scope, _) {
init = function() {
_.keys($scope);
}
init();
}]);
Like a boss
Scoping $scope's
<div ng-controller="navCtrl">
<span>{{user}}</span>
<div ng-controller="loginCtrl">
<span>{{user}}</span>
<input ng-model="user"></input>
</div>
</div>
<div ng-controller="navCtrl">
<span>{{user.name}}</span>
<div ng-controller="loginCtrl">
<span>{{user.name}}</span>
<input ng-model="user.name"></input>
</div>
</div>
app.controller('navCtrl', function($scope) {
$scope.user = {name: ''};
});
Use modules
var app = angular.module('app',[]);
app.service('MyService', function(){
//service code
});
app.controller('MyCtrl', function($scope, MyService){
//controller code
});
Small application
var services = angular.module('services',[]);
services.service('MyService', function(){
//service code
});
var controllers = angular.module('controllers',['services']);
controllers.controller('MyCtrl', function($scope, MyService){
//controller code
});
var app = angular.module('app',['controllers', 'services']);
var sharedServicesModule = angular.module('sharedServices',[]);
sharedServices.service('NetworkService', function($http){});
var loginModule = angular.module('login',['sharedServices']);
loginModule.service('loginService', function(NetworkService){});
loginModule.controller('loginCtrl', function($scope, loginService){});
var app = angular.module('app', ['sharedServices', 'login']);
Group similar types of objects
Group features
Angular way
ANGULAR WAY
We all started with jQuery
Come on, tell me about JQuery
$(document).ready(function () {
$("#allow_max_members").bind('click', function() {
var shown;
shown = $(this).prop('checked');
$("#max_members").toggle(shown);
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" href="style.css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div>
<input type="checkbox" id="allow_max_members" checked='true'/>
<input id="max_members" type="text" />
</div>
</body>
</html>
script.js
Stop using jQuery
In order to really understand how to build an AngularJS application, stop using jQuery. jQuery keeps the developer thinking of existing HTML standards, but as the docs say "AngularJS lets you extend HTML vocabulary for your application."
AngularJS is a framework!
$scope - viewModel
Binding
$scope.$watch('qty * cost', function(newValue, oldValue) {
//update the DOM with newValue
});
For example, these are valid expressions in Angular:
-
1+2
-
a+b
-
user.name
-
items[index]
Scope hierarchical structure
$scope
$scope.$digest()
$scope.$apply()
//Pseudo-Code of $apply()
function $apply(expr) {
try {
return $eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
$root.$digest();
}
}
Goes through list of watchers and calls listeners if it's needed starting with the current scope recursively to all descendants.
$digest
appModule.directive("check", function () {
return function (scope, elem, attrs) {
elem.bind('click', function () {
if (elem.prop("checked")) {
scope.$apply(attrs["myCheck"]);
}
});
};
});
Example: when you need to call $apply manually
Error: $digest already in progress
if(!$scope.$$phase) {
//$digest or $apply
}
anti-pattern
$timeout(function() {
$scope.message = "Timeout called!";
})// no need to pass 2nd param if it's zero!
Right way
setTimeout(function () {
$scope.$apply(function () {
$scope.message = "Timeout called!";
});
}, 0);
=
$timeout(function() {
//code here
}, 100, false); //doesn't call $apply();
Don’t, please don’t use DOM selectors inside controller!
appModule.controller('myCtrl', function($scope) {
// bad programmer =(
if ($('.header').length > 0) {
$('.pointer').css({
top: - $('.header').height()
});
}
})
Controller should not depend on markup!
$ - is jQuery
angular.element('.header')
Error: [jqLite:nosel] Looking up elements via selectors is not supported by jqLite!
Controllers should never do DOM manipulation or hold DOM selectors. Likewise business logic should live in services, not controllers.
native element object
// include BEFORE AngularJS
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.js"></script>
// now AngularJS uses jQuery instead of jqLite
<script src="https://code.angularjs.org/1.2.25/angular.js"></script>
Why not?
- You can not test such controllers
- Markup is not reliable
- Separation of concerns
- Can't reuse
Why? why?
Because...
interaction problems:
-
communication between scopes
-
communication between DOM elements
-
communication between "components" (whatever it means)
But using "Angular way" you can face...
wait for it
-
Events ($broadcast, $emit)
-
Common scope (be careful with $rootScope)
-
Directives communication ('require')
-
Shared resources (injectable objects: service, factory, value)
Solutions:
If you need DOM element - use directive
angular.module('appModule')
.directive("metaTitle", function ($title) {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
scope.$watch("$current", function(current) {
if (current) {
$title.documentHeight = element.height();
}
});
}
}
})
.value('$title', {documentHeight: 50});
Solution examples
Task:
You have fixed header with pointers. Click on pointers should scroll to the desired paragraph on the page.
<body ng-controller='MainCtrl'>
<div class='header'>
<ul>
<li ng-repeat="item in pointers" ng-click="goToParagraph(item)">Go to {{item}}
<span ng-hide="$last"> | </span>
</li>
<li class='change-height'
ng-click='$emit('changeHeight')'>Change height</li>
</ul>
</div>
...
<ul>
<li ng-repeat="item in pointers">
<a class='pointer' name="{{item}}"></a>
<span>{{item}}</span>
...
</li>
</ul>
</body>
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope, $location, $anchorScroll) {
$scope.pointers = [1, 2, 3, 4, 5, 6];
$scope.goToParagraph = function(hash) {
$location.hash(hash);
$anchorScroll();
}
});
.header {
position: fixed;
background-color: green;
width: 100%;
color: #fff;
}
.pointer{
position: relative;
display: block;
top: -50px;
}
.change-height {
float: right;
}
app.directive('header', function($rootScope) {
return {
link: function(scope, element, attrs) {
scope.$on('changeHeight', function() {
var height = Math.floor(Math.random() * 100) + 50;
element.css({
height: height + 'px'
});
$rootScope.$broadcast('newHeight', height);
}
},
restrict: 'C'
});
app.directive('pointer', function() {
return {
link: function(scope, element, attrs) {
scope.$on('newHeight', function(e, value) {
if (value) {
element.css({'top': -value + 'px'});
}
});
},
restrict: 'C'
}
})
Event communication
Shared resource
app.directive('header', function($header) {
return {
link: function(scope, element, attrs) {
scope.$on('changeHeight', function() {
var height = Math.floor(Math.random() * 100) + 50;
element.css({
height: height + 'px'
});
$header.height = height;
});
},
restrict: 'C'
}
}
).value('$header', {height: 50});
app.directive('pointer', function($header) {
return {
link: function(scope, element, attrs) {
scope.$header = $header;
scope.$watch('$header.height', function(newVal) {
if (newVal) {
element.css({'top': -newVal + 'px'});
}
})
},
restrict: 'C'
}
})
app.directive('pointer', function($header) {
return {
link: function(scope, element, attrs) {
scope.$watch(function() {
return $header.height;
}, function(newVal) {
if (newVal) {
element.css({'top': -newVal + 'px'});
}
})
},
restrict: 'C'
}
})
Common scope
Common scope
app.directive('header', function() {
return {
link: function(scope, element, attrs) {
scope.$on('changeHeight', function() {
var height = Math.floor(Math.random() * 100) + 50;
element.css({
height: height + 'px'
});
scope.newHeight = height;
}
},
restrict: 'C'
});
app.directive('pointer', function() {
return {
link: function(scope, element, attrs) {
scope.$watch('newHeight', function(newVal) {
if (newVal) {
element.css({'top': -newVal + 'px'});
}
});
},
restrict: 'C'
}
})
app.directive('header', function() {
return {
link: function(scope, element, attrs) {
scope.$on('changeHeight', function() {
var height = Math.floor(Math.random() * 100) + 50;
element.css({
height: height + 'px'
});
scope.internalHeight = height;
}
},
restrict: 'C',
scope: {
internalHeight: '=height' // what is the difference between '@', '=', '&'?
}
});
Isolated scope
app.directive('pointer', function() {
return {
link: function(scope, element, attrs) {
scope.$watch('offset', function(newVal) {
if (newVal) {
element.css({'top': -newVal + 'px'});
}
});
},
restrict: 'C',
scope: {
offset: '='
}
}
})
<body ng-controller='MainCtrl'>
<div class='header' height='pageScope.newHeight'>
<ul>
<li ng-repeat="item in pointers" ng-click="goToParagraph(item)">Go to {{item}}
<span ng-hide="$last"> | </span>
</li>
<li class='change-height'
ng-click='$emit('changeHeight')'>Change height</li>
</ul>
</div>
...
<ul>
<li ng-repeat="item in pointers">
<a class='pointer' offset='pageScope.newHeight' name="{{item}}"></a>
<span>{{item}}</span>
...
</li>
</ul>
</body>
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope, $location, $anchorScroll) {
$scope.pointers = [1, 2, 3, 4, 5, 6];
$scope.goToParagraph = function(hash) {
$location.hash(hash);
$anchorScroll();
}
$scope.pageScope = {newHeight: 50};
});
Change in markup
Directive communication:
how to use 'require'?
<body ng-controller='MainCtrl'>
<div class='header'>
<ul>
<li ng-repeat="item in pointers" ng-click="goToParagraph(item)">Go to {{item}}
<span ng-hide="$last"> | </span>
</li>
<li class='change-height' height='pageScope.newHeight'>Change height</li>
</ul>
</div>
...
<ul>
<li ng-repeat="item in pointers">
<a class='pointer' offset='pageScope.newHeight' name="{{item}}"></a>
<span>{{item}}</span>
...
</li>
</ul>
</body>
app.directive('changeHeight', function() {
return {
link: function(scope, element, attrs) {
element.bind('click', function() {
var height = Math.floor(Math.random() * 100) + 50;
element.css({ height: height + 'px' }); // <-- this is not '.header'. It doesn't work!!!
scope.internalHeight = height;
scope.$apply();
});
},
restrict: 'C',
scope: {
internalHeight: '=height'
}
});
app.directive('changeHeight', function() {
return {
link: function(scope, element, attrs, ctrl) {
element.bind('click', function() {
var height = Math.floor(Math.random() * 100) + 50;
// we need to define getElement() method in header
ctrl.getElement().css({ height: height + 'px' });
scope.internalHeight = height;
scope.$apply();
});
},
restrict: 'C',
scope: {
internalHeight: '=height'
},
require: '^header' // or '?^header' - doesn't throw error if header is not found
});
app.directive('header', function() {
return {
controller: function($element) {
this.getElement = function() {
return $element;
}
}
});
ANOTHER ANGULAR WAY
app.value('$header', {})
.directive('pointerOffsetCreator', function($header) {
return {
restrict: "A",
scope: {
className: '@'
},
link: function(scope, elem, attrs) {
if (elem.prop("tagName") == 'STYLE') {
scope.$watch(function() {
return $header.height;
}, function(newValue) {
function createClassString() {
return '.' + attrs.className + "{" +
"top: -" + newValue + "px" + "!important" +
"}";
}
if (newValue) {
elem.empty();
elem.html(createClassString());
}
});
}
}
}
})
<head>
<style pointer-offset-creator class-name='pointer'></style>
</head>
Questions?
I see you want to ask something...
- Batarang: https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?utm_source=chrome-ntp-icon
- Egghead: https://egghead.io/technologies/angularjs?order=desc&page=1
- Pull to refresh for iScroll5 on AngularJS: https://github.com/Limurezzz/pull-to-refresh-iscroll5
Useful links:
BIG QR=)
Do you want more?
angular.module('app')
.directive('alert', function () {
return {
restrict: 'EA',
controller: ['$scope', '$attrs', function ($scope, $attrs) {
$scope.closeable = 'close' in $attrs;
}],
template:
"<div class='alert alert-dismissable' ng-class='\"alert-\" + (type || \"warning\")'>\n" +
" <button ng-show='closeable' type='button' class='close' ng-click='close()'>×</button>\n" +
" <div ng-transclude></div>\n" +
"</div>\n",
transclude: true,
replace: true,
scope: {
type: '=',
close: '&'
}
};
})
<html ng-app>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="style.css" />
<script src="https://code.angularjs.org/1.3.0-rc.2/angular.js"></script>
<script src="app.js"></script>
</head>
<body>
<alert type="danger">My message</alert>
</body>
</html>
// compiled markup
<div class="alert alert-dismissable ng-isolate-scope alert-danger"
ng-class="'alert-' + (type || 'warning')" type="'danger'">
<button ng-show="closeable" type="button" class="close ng-hide" ng-click="close()">×</button>
<div ng-transclude=""><span class="ng-scope">My message</span></div>
</div>
Alert directive
angular.module('myApp')
.service("$notification", ["$timeout", function ($timeout) {
var alerts = [];
var TIMEOUT_MS = 5000;
this.getAlerts = function() {
return alerts;
}
this.success = function(msg) {
addAlert(msg, 'success');
}
this.error = function(msg) {
addAlert(msg, 'danger');
}
this.warning = function(msg) {
addAlert(msg, 'warning');
}
var addAlert = function (msg, type) {
if (msg) {
var item = {msg: msg, type: type};
alerts.push(item);
$timeout(function() {
var index = alerts.indexOf(item);
if (index > -1) {
alerts.splice(index, 1);
}
}, TIMEOUT_MS);
}
};
}])
Notification service
angular.module('appModule')
.run(function ($templateCache) {
$templateCache.put("whateverYouWant",
'<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{alert.msg}}</alert>\n');
})
.directive("globalNotification", ['$notification', function ($notification) {
return {
restrict: 'AC',
templateUrl: 'whateverYouWant',
link: function (scope, element, attrs) {
scope.alerts = $notification.getAlerts();
scope.closeAlert = function (index) {
scope.alerts.splice(index, 1);
};
}
}
}])
Notification directive
// put this div somewhere on your page
<div class="global-notification"></div>
How to not program on AngularJS
By Andrei Yemialyanchik
How to not program on AngularJS
Before you start programming with AngluarJS please consider typical mistakes and anti-patterns. This presentation will give you a guide to follow best practices and methods. Don't hesitate to use or fork this presentations for you needs. Just keep reference to origin. May the force be with you.
- 25,175