AngularJS
Content
Data Binding
Controllers
Services
Filters
Directives
Scopes
Data Binding
Data Binding
- Auto synchronization of data between model and view
- In Angular: model is the single-source-of-truth
- View as projection of model
- Data binding in classical template systems
- Merge template and model components into a view for one time
- Dev is responsible for syncing between view and model
- Data binding in Angular
- Angular template
- Uncompiled html with additional markup is compiled
- Produces a live view
- Any changes are reflected
- Model -> View, View -> Model
- Angular template
Data Binding #2
- View is a projection of the model
- Controller is completely separated from the view
- Makes testing easy
- Test controller in isolation without the view and related DOM/browser dependency
Controllers
Controllers
- A controller is a JavaScript constructor function that is used to augment the Angular scope
- Is attached to the DOM via the ng-controller directive
- Angular will instantiate a new Controller object with specified constructor function
- New child is available as injectable parameter to the controller's constructor function as $scope
Controllers #2
- Use controller to:
- setup initial state of the $scope object
- add behavior to the $scope object
- Do NOT use controllers to:
- Manipulate DOM - use directive
- only business logic
- no presentation logic
- makes testing easy
- Format input - use form controls
- Filter output - use filters
- Share code or state across controllers - use services
- Manage life-cycle of other components
- Manipulate DOM - use directive
$scope
- Attach properties to the $scope object
- All $scope properties will be available to the template in the DOM where the controller is registered
var myApp = angular.module('myApp',[]);
myApp.controller('GreetingController', ['$scope', function($scope) {
$scope.greeting = 'Hola!';
}]);
<div ng-controller="GreetingController">
{{ greeting }}
</div>
- $scope object is injected via inline injection annotation
Adding Behavior
- Attach methods to $scope object
var myApp = angular.module('myApp',[]);
myApp.controller('DoubleController', ['$scope', function($scope) {
$scope.double = function(value) { return value * 2; };
}]);
<div ng-controller="DoubleController">
Two times <input ng-model="num"> equals {{ double(num) }}
</div>
Controller Example #1
var myApp = angular.module('spicyApp1', []);
myApp.controller('SpicyController', ['$scope', function($scope) {
$scope.spice = 'very';
$scope.chiliSpicy = function() {
$scope.spice = 'chili';
};
$scope.jalapenoSpicy = function() {
$scope.spice = 'jalapeño';
};
}]);
<div ng-controller="SpicyController">
<button ng-click="chiliSpicy()">Chili</button>
<button ng-click="jalapenoSpicy()">Jalapeño</button>
<p>The food is {{spice}} spicy!</p>
</div>
Controller Example #2
var myApp = angular.module('spicyApp2', []);
myApp.controller('SpicyController', ['$scope', function($scope) {
$scope.customSpice = "wasabi";
$scope.spice = 'very';
$scope.spicy = function(spice) {
$scope.spice = spice;
};
}]);
<div ng-controller="SpicyController">
<input ng-model="customSpice">
<button ng-click="spicy('chili')">Chili</button>
<button ng-click="spicy(customSpice)">Custom spice</button>
<p>The food is {{spice}} spicy!</p>
</div>
Scope Inheritance
- Attach controllers to different levels of the DOM hierarchy
- ng-controller directive creates a new child scope each time
- Hierarchy of scopes, that inherit from each other
- E.g. For three nested ng-controller directives, 4 scopes are created
- root scope
- 3 nested scopes
Scope Inheritance #2
var myApp = angular.module('scopeInheritance', []);
myApp.controller('MainController', ['$scope', function($scope) {
$scope.timeOfDay = 'morning';
$scope.name = 'Nikki';
}]);
myApp.controller('ChildController', ['$scope', function($scope) {
$scope.name = 'Mattie';
}]);
myApp.controller('GrandChildController', ['$scope', function($scope) {
$scope.timeOfDay = 'evening';
$scope.name = 'Gingerbread Baby';
}]);
<div class="spicy">
<div ng-controller="MainController">
<p>Good {{timeOfDay}}, {{name}}!</p>
<div ng-controller="ChildController">
<p>Good {{timeOfDay}}, {{name}}!</p>
<div ng-controller="GrandChildController">
<p>Good {{timeOfDay}}, {{name}}!</p>
</div>
</div>
</div>
</div>
// css
div.spicy div {
padding: 10px;
border: solid 2px blue;
}
Testing Controllers
// app code
var myApp =
angular.module('myApp',[]);
myApp.controller('MyController',
function($scope) {
$scope.spices = [
{
"name":"pasilla",
"spiciness":"mild"
},
{
"name":"jalapeno",
"spiciness":"hot hot hot!"},
{
"name":"habanero",
"spiciness":"LAVA HOT!!"
}
];
$scope.spice = "habanero";
});
// jasmine test
describe('myController function', function() {
describe('myController', function() {
var $scope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('MyController', {$scope: $scope});
}));
it('should create "spices" model with 3 spices',
function() {
expect($scope.spices.length).toBe(3);
});
it('should set the default value of spice',
function() {
expect($scope.spice).toBe('habanero');
});
});
});
Testing Controllers #2
describe('state', function() {
var mainScope, childScope, grandChildScope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
mainScope = $rootScope.$new();
$controller('MainController', {$scope: mainScope});
childScope = mainScope.$new();
$controller('ChildController', {$scope: childScope});
grandChildScope = childScope.$new();
$controller('GrandChildController', {$scope: grandChildScope});
}));
it('should have over and selected', function() {
expect(mainScope.timeOfDay).toBe('morning');
expect(mainScope.name).toBe('Nikki');
expect(childScope.timeOfDay).toBe('morning');
expect(childScope.name).toBe('Mattie');
expect(grandChildScope.timeOfDay).toBe('evening');
expect(grandChildScope.name).toBe('Gingerbread Baby');
});
});
Services
Services
- Services are shareable objects wired via DI (dependency injection)
- Use services to organize and share code accross app
- Services are lazily instantiated
- only create instance if application component depends on it
- Services are singletons
- Each component dependent on a service gets a reference to the single instance generated by the service factory
- Angular offers many useful services (E.g. $http)
- Developers may register own services
Using Services
- Add service as dependency for the component
angular.module('myServiceModule', []).
controller('MyController',
['$scope','notify',
function ($scope, notify) {
$scope.callNotify = function(msg) {
notify(msg);
};
}]).
factory('notify', ['$window', function(win) {
var msgs = [];
return function(msg) {
msgs.push(msg);
if (msgs.length == 3) {
win.alert(msgs.join("\n"));
msgs = [];
}
};
}]);
<div id="simple" ng-controller="MyController">
<p>
Let's try this simple notify service,
injected into the controller...
</p>
<input ng-init="message='test'" ng-model="message">
<button ng-click="callNotify(message);">
NOTIFY
</button>
<p>
(you have to click 3 times to see an alert)
</p>
</div>
Creating Services
- Define own services by service's name and service factory function
- Service factory function generates the single object or function that represents the service to the rest of the app
- This object or function is injected into any component
var myModule = angular.module('myModule', []);
myModule.factory('serviceId', function() {
var shinyNewServiceInstance;
// factory function body that constructs shinyNewServiceInstance
return shinyNewServiceInstance;
});
Creating Services #2
angular.module('myModule', []).config(['$provide', function($provide) {
$provide.factory('serviceId', function() {
var shinyNewServiceInstance;
// factory function body that constructs shinyNewServiceInstance
return shinyNewServiceInstance;
});
}]);
Service Dependencies
var batchModule = angular.module('batchModule', []);
batchModule.factory('batchLog', ['$interval', '$log', function($interval, $log) {
var messageQueue = [];
function log() {
if (messageQueue.length) {
$log.log('batchLog messages: ', messageQueue);
messageQueue = [];
}
}
// start periodic checking
$interval(log, 50000);
return function(message) {
messageQueue.push(message);
}
}]);
batchModule.factory('routeTemplateMonitor', ['$route', 'batchLog', '$rootScope',
function($route, batchLog, $rootScope) {
$rootScope.$on('$routeChangeSuccess', function() {
batchLog($route.current ? $route.current.template : null);
});
}]);
Testing Services
var mock, notify;
beforeEach(function() {
mock = {alert: jasmine.createSpy()};
module(function($provide) {
$provide.value('$window', mock);
});
inject(function($injector) {
notify = $injector.get('notify');
});
});
it('should not alert first two notifications', function() {
notify('one');
notify('two');
expect(mock.alert).not.toHaveBeenCalled();
});
Testing Services #2
it('should alert all after third notification', function() {
notify('one');
notify('two');
notify('three');
expect(mock.alert).toHaveBeenCalledWith("one\ntwo\nthree");
});
it('should clear messages after alert', function() {
notify('one');
notify('two');
notify('third');
notify('more');
notify('two');
notify('third');
expect(mock.alert.callCount).toEqual(2);
expect(mock.alert.mostRecentCall.args).toEqual(["more\ntwo\nthird"]);
});
Filters
Introduction
{{ expression | filter }}
{{ expression | filter1 | filter2 | ... }}
{{ expression | filter:arg1:arg2:... }}
- Formats the value of an expression
- Filters may be used in templates
- Or in controllers, services and directives
Using Filters Elsewhere
- Inject dependency with <filterName>Filter
- E.g. ['numberFilter', function(numberFilter) {}]
- Injects the filter number
Using Filters Elsewhere #2
<div
ng-controller="FilterController as ctrl">
<div>
All entries:
<span
ng-repeat="entry in ctrl.array">
{{entry.name}}
</span>
</div>
<div>
Entries that contain an "a":
<span
ng-repeat="entry in ctrl.filteredArray">
{{entry.name}}
</span>
</div>
</div>
angular.module('FilterInControllerModule', []).
controller('FilterController', ['filterFilter',
function(filterFilter) {
this.array = [
{name: 'Tobias'},
{name: 'Jeff'},
{name: 'Brian'},
{name: 'Igor'},
{name: 'James'},
{name: 'Brad'}
];
this.filteredArray =
filterFilter(this.array, 'a');
}]);
Custom Filters
<div ng-controller="MyController">
<input ng-model="greeting" type="text"><br>
No filter: {{greeting}}<br>
Reverse: {{greeting|reverse}}<br>
Reverse + uppercase: {{greeting|reverse:true}}
<br>
</div>
angular.module('myReverseFilterApp', [])
.filter('reverse', function() {
return function(input, uppercase) {
input = input || '';
var out = "";
for (var i = 0; i < input.length; i++) {
out = input.charAt(i) + out;
}
// conditional based on optional argument
if (uppercase) {
out = out.toUpperCase();
}
return out;
};
})
.controller('MyController', ['$scope',
function($scope) {
$scope.greeting = 'hello';
}]);
Directives
Introduction
- Markers on a DOM element
- As attribute, element, CSS class or even comment
- Tells Angular compiler ($compile) to attach specified behavior to that DOM or to transfrom DOM element (and children)
- Angular provides a set of built-in directives (ngModel, ngRepeat, ...)
- Developers may also create own directives
- When Angular bootstraps the app the html compiler traverses DOM matching directives against DOM elements
- Compile html means
- Attach event listeners to the html to make it interactive
Directive Matching
- Input element matches the ngModel directive
- Also data-ng:model matches the ngModel directive
- Angular performs directive name normalization
- Html is case-insensitive
- Directives in html are usually written dash-delimited (ng-model)
- Real directive names are written in camelCase notation
<input ng-model="foo">
<input data-ng:model="foo">
Directive Matching #2
<div ng-controller="Controller">
Hello <input ng-model='name'> <hr/>
<span ng-bind="name"></span> <br/>
<span ng:bind="name"></span> <br/>
<span ng_bind="name"></span> <br/>
<span data-ng-bind="name"></span> <br/>
<span x-ng-bind="name"></span> <br/>
</div>
- Normalization process
- Strip x- and data- from the front
- Convert the (:, -, _) delimited name to camelCase
Directive Matching #3
<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>
- $compile can match directives based on element, attribute, class or comment
- Mostly element or attribute is used
- When creating components use element directives
- When decorating existing elements with new behavior use attribute directives
Creating Directives
- Directives are also registered on modules
- Use the module.directive API
- Takes a normalized directive name and a factory function
- Function should return an object with different options to tell $compile how the directive should behave when matched
- Factory function is invoked once when the compiler matches the directive for the first time
- Prefix own directives (e.g. ng-)
Template-expanding Directives
<div ng-controller="Controller">
<div my-customer></div>
</div>
- Template that is often used may be wrapped in a directive
- Only make changes in one place
- In-lined value is used for template option
- Use templateUrl option for more complicated templates
angular.module('docsSimpleDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}}
Address: {{customer.address}}'
};
});
Template-expanding Directives #2
<div ng-controller="Controller">
<div my-customer></div>
</div>
angular.module('docsTemplateUrlDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
templateUrl: 'my-customer.html'
};
});
Name: {{customer.name}}
Address: {{customer.address}}
Template-expanding Directives #3
<div ng-controller="Controller">
<div my-customer type="name">
</div>
<div my-customer type="address">
</div>
</div>
angular.module('docsTemplateUrlDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
templateUrl: function(elem, attr){
return 'customer-'+attr.type+'.html';
}
};
});
Name: {{customer.name}}
Address: {{customer.address}}
Restrict Option
'A' - only matches attribute name
'E' - only matches element name
'C' - only matches class name
'AEC' - matches either attribute or element or class name
- When creating directives, it is restricted to attributes and elements only by default
Restrict Option #2
<div ng-controller="Controller">
<my-customer></my-customer>
</div>
angular.module('docsRestrictDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
templateUrl: 'my-customer.html'
};
});
Name: {{customer.name}}
Address: {{customer.address}}
Isolate Scope
<div ng-controller="NaomiController">
<my-customer></my-customer>
</div>
<hr>
<div ng-controller="IgorController">
<my-customer></my-customer>
</div>
angular.module('docsScopeProblemExample', [])
.controller('NaomiController', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.controller('IgorController', ['$scope', function($scope) {
$scope.customer = {
name: 'Igor',
address: '123 Somewhere'
};
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
templateUrl: 'my-customer.html'
};
});
Name: {{customer.name}}
Address: {{customer.address}}
- Without isolate scope
Isolate Scope #2
<div ng-controller="Controller">
<my-customer info="naomi">
</my-customer>
<hr>
<my-customer info="igor">
</my-customer>
</div>
angular.module('docsIsolateScopeDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.naomi = { name: 'Naomi',
address: '1600 Amphitheatre' };
$scope.igor = { name: 'Igor',
address: '123 Somewhere' };
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
});
Name: {{customerInfo.name}}
Address: {{customerInfo.address}}
Isolate Scope #3
- Scope options contains a property for each isolate scope binding
- customerInfo corresponds to isolate scope's property
- "=info" tells $compile to bind to the info attribute
- When attribute name is the same we can use a simple "=" instead
scope: {
customerInfo: '=info'
}
// or
scope: {
customerInfo: '='
}
DOM Manipulation
angular.module('docsTimeDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
}])
.directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
timeoutId = $interval(function() {
updateTime(); // update DOM
}, 1000);
}
return {
link: link
};
}]);
Link Function
function link(scope, element, attrs, ...) {
// ...
}
- Directives that modify the DOM use the link function
- scope: Angular scope object
- element: jqLite wrapped element that matches the directive
- attrs: hash of key-value pairs of normalized attribute names and values
Wrapping Directives
<div ng-controller="Controller">
<my-dialog>
Check out
the contents, {{name}}!
</my-dialog>
</div>
angular.module('docsTransclusionDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: 'my-dialog.html'
};
});
<div class="alert"
ng-transclude>
</div>
Transclude Option
<div ng-controller="Controller">
<my-dialog>
Check out
the contents, {{name}}!
</my-dialog>
</div>
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html',
link: function (scope, element) {
scope.name = 'Jeff';
}
};
});
<div class="alert"
ng-transclude>
</div>
- Transclude changes the way scopes are nested
- Contents of a transcluded directive have access to the outer scope
More Examples
- https://code.angularjs.org/1.3.8/docs/guide/directive
Scopes
Introduction
- Scope is an object that refers to the app model execution context for expressions
- Arranged in hierarchical structure (like DOM)
- Scopes can watch expressions and propagate events
- Characteristics
- provide APIs ($watch) to observe model mutations
- provide APIs ($apply) to propagate any model changes into view, outside of the Angular context
- Scopes can be nested to limit access to the properties of application components
Introduction #2
- Nested scopes are either child scopes or isolate scopes
- Child scopes prototypically inherit properties from parent scopes
- Provide context for expression evaluation
- E.g. {{ username }} must be evaluated for a given scope
Scope as Data-Model
- Scope as glue between app controller and view
- During the template linking phase the directives set up $watch expressions on the scope
- $watch notifies directive on property changes, which allows directives to render the updated value to the DOM
- Code Example
- Rendering of {{ greeting }} involves
- Retrieve scope of associated DOM node where the epxression is defined in the template
- Evaluate expression against found scope
- Rendering of {{ greeting }} involves
Scope Hierarchies
- Each Angular app has a root scope
- And many child scopes
- Sometimes directives create a new scope
- Scope as a tree structure, mirroring the DOM
- When evaluating an expression, it first looks at the associated scope
- If no such property exists, it searches the parent scope, until the root scope is reached
- (Same as the prototypical inheritance in JavaScript)
Retrieving Scopes from the DOM
- Scopes are attached to the DOM as $scope data property
- Can be retrieved for debugging purposes
- ng-app marks the location where the root scope is attached
- Examine scope in the browser
- Right click on element
- Access currently selected element via $0
- Retrieve scope via angular.element($0).scope()
Scope Events Propagation
- Scopes can propagate events in similar fashion to DOM events
- Events can be broadcasted to scope children
- Events can be emitted to scope parents
angular.module('eventExample', [])
.controller('EventController',
['$scope', function($scope) {
$scope.count = 0;
$scope.$on('MyEvent', function() {
$scope.count++;
});
}]);
<div ng-controller="EventController">
Root scope <tt>MyEvent</tt> count: {{count}}
<ul>
<li ng-repeat="i in [1]"
ng-controller="EventController">
<button ng-click="$emit('MyEvent')">
$emit('MyEvent')
</button>
<button ng-click="$broadcast('MyEvent')">
$broadcast('MyEvent')
</button>
<br>
Middle scope <tt>MyEvent</tt> count: {{count}}
<ul>
<li ng-repeat="item in [1, 2]"
ng-controller="EventController">
Leaf scope <tt>MyEvent</tt> count: {{count}}
</li>
</ul>
</li>
</ul>
</div>
Scope Lifecycle
- Normal flow of a browser receiving an event is that it executes an associated JavaScript callback
- Once the callback completes, browser re-renders the DOM and returns to waiting or for next event
- When browser calls into JavaScript, the code is executed outside of the Angular execution context
- Model modifications are only processed in the Angular context
- Use $apply method to enter Angular context from outside
- E.g.: If a directive listens on DOM events, such as ng-click, it must evaluate the expression inside the $apply method
- After evaluating, the $apply method performs a $digest
- In this phase the scope examines all $watch expressions
Scope Lifecycle #2
- Creation
- Root scope is created by the application bootstrap via the $injector
- During template linking, some directives create new child scopes
- Watches registration
- During template linking, directives register watches on the scope
- Watches are used to propagate model values to the DOM
Scope Lifecycle #3
- Model Mutation
- Mutations are properly observed only in $apply
- Angular APIs do this automatically (e.g. in controllers, services, etc.)
- Mutation Observation
- At the end of $apply, Angular performs a $digest cycle on the root scope
- During $digest all $watch expressions or functions are checked for model mutation
- If change -> call listener
- Scope destruction
- When child scopes are no longer needed it is the responsbility of the child scope creator to destroy via scope.$destroy
Scope Watch Depths
Hello World Explanation
- During compilation phase
- ng-model and input directive set up a keydown listener on the <input> control
- The interpolation sets up a $watch to be notified of name changes
- During runtime phase
- Press 'x' causes browser to emit keydown event
- Input directive calls $apply("name = 'x';")
- Angular applies it to the model
References
https://code.angularjs.org/1.3.8/docs/guide/
Thank you for your attention!
AngularJS
By dinony
AngularJS
AngularJS
- 404