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

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

$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
    1. Strip x- and data- from the front
    2. 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

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

  1. Creation
    • Root scope is created by the application bootstrap via the $injector
    • During template linking, some directives create new child scopes
  2. Watches registration
    • During template linking, directives register watches on the scope
    • Watches are used to propagate model values to the DOM

Scope Lifecycle #3

  1. Model Mutation
    • Mutations are properly observed only in $apply
    • Angular APIs do this automatically (e.g. in controllers, services, etc.)
  2. 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
  3. 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!

 

Made with Slides.com