AngularJS Internals

– A look behind all the magic


by Carl Vuorinen / @cvuorinen

Carl Vuorinen

@cvuorinen
cvuorinen
cvuorinen.net

HTML enhanced for web apps!


Hello {{name}}!


Todo:

    <li ng-repeat="todo in list" ng-click="list.splice($index, 1)"> {{todo}} </li>

Scope

$scope == POJO


Plain Old Javascript Object

$watch & $digest

 red border 
== "pseudo" code

$scope.$watch = function(watchFunc, listenerFunc) {
    this.$$watchers.push({
        watchFunc: watchFunc,
        listenerFunc: listenerFunc
    });
};
                    

$scope.$digest = function() {
    var dirty = true;
    var scope = this;
    
    while (dirty) {
        dirty = false; // reset from last iteration
        
        scope.$$watchers.map(function (watcher) {
            // execute watch function to get new value
            newValue = watcher.watchFunc(scope);
            
            // check if value has changed from last cycle
            if (newValue !== watcher.lastValue) {
                dirty = true;
                
                // execute listener and store new last value
                watcher.listenerFunc(newValue, watcher.lastValue, scope);
                watcher.lastValue = newValue;
            }
        });
    }
};
                    

Scope Inheritance

Javascript prototypical inheritance


function Vehicle() {}
Vehicle.prototype.speed = 4;
Vehicle.prototype.go = function() {
  return 'Vr'
       + 'o'.repeat(this.speed)
       + 'm!';
};

function Truck() {}
Truck.prototype = Vehicle.prototype;

var scania = new Truck();
scania.go();  // Vroooom!
                            

function Car(speed) {
    this.speed = speed;
}
Car.prototype = Object.create(
    Vehicle.prototype
);
Car.prototype.constructor = Car;

var honda = new Car(6);
honda.go();  // Vroooooom!

var tesla = new Car(12);
tesla.go();  // Vroooooooooooom!
                            

Scope Hierarchy


$scope.$new = function() {
    var ChildScope = function() {};
    ChildScope.prototype = this;

    return new ChildScope();
};
                    



<img ng-if="showMore" src="img/mo-money.jpg">
Whenever you use ngModel, there’s got to be a dot in there somewhere. If you don’t have a dot, you’re doing it wrong. Miško Hevery (creator of AngularJS)

Controller As Syntax


angular.module('app')
.controller("SomeController", function() {
    this.value = "Hello!";
});
                    

{{ some.value }}

Controller As Syntax


angular.module('app')
.controller("SomeController", function() {
    var vm = this;
    vm.value = "Hello!";
    vm.sayHello = function() {
        alert(vm.value);
    }
});
                    

Expressions


Holy smokes, Batman!

New JS frameworks since last week: {{ lastWeekCount }} +{{ (lastWeekCount / jsFrameworks.length * 100) | number:1 }}%

Time to

$parse

is kinda like


$parse = function(expr) {
    return function(scope) {
        with (scope) {
            return eval(expr);
        }
    }
}
                    

... except it's really not

DIY Spreadsheet using $parse

JS Bin on jsbin.com

{{ interpolation }}

  1. find {{ and }} characters in DOM text nodes
  2. $parse found expressions
  3. create a $watch with the parsed expression
  4. listener updates DOM node text


+ same thing for attributes

Directives


angular.module('app')
.directive('awesomeButton', function() {
  return {
    scope: {
      click: '&',
      icon: '@'
    },
    transclude: true,
    template: '<button class="button" '+
              '        ng-click="click()">'+
              '<i class="fa fa-{{icon}}"></i>'+
              '<span ng-transclude></span>'+
              '</button>'
  };
});
                    

<awesome-button icon="bullhorn" click="my.alert()"> Click here! </awesome-button>

Compiling & Linking




1. compile
2. compile
3. compile

4. compile
5. link
6. link
7. link
8. link

Transclusion


Some Content


angular.module('app')
.directive('myDirective', function() {
  return {
    transclude: true,
    template: '
' + '

Title

' + '<div ng-transclude></div>' + '
' }; });
take content
insert here

angular.module('ng')
.directive('ngInit', function($parse) {
  return {
    link: function (scope, element, attrs) {
      $parse(attrs.ngInit)(scope);
    }
  };
});
                    

angular.module('ng')
.directive('ngClick', function($parse) {
  return {
    link: function (scope, element, attrs) {
      var expression = $parse(attrs.ngClick);
      element.on('click', function() {
        scope.$apply(function () {
          expression(scope);
        });
      });
    }
  };
});
                    

angular.module('ng')
.directive('ngRepeat', function($parse, $transclude) {
  return {
    transclude: 'element',
    link: function (scope, element, attrs) {
      var match = attrs.ngRepeat.match(/^\s*([\s\S]+?)  ...  \s*$/);
      
      $scope.$watch($parse(match.target), function (collection) {
        // locate existing items, mark items not present for removal
        
        // remove leftover items
        
        // update existing items' scopes
        
        // create new elements with $transclude
        // and keep a reference to their scopes
      });
    }
  };
});
                    

angular.module('ng')
.directive('ngModel', function() {
  return {
    link: function (scope, element, attrs) {
      var currentValue;
      element.on('keyup', function() {
        if ($(element).val() != currentValue) {
          scope.$apply(function () {
            scope[attrs.ngModel] = currentValue = $(element).val();
          });
        }
      });
      
      $scope.$watch(attrs.ngModel, function (newValue) {
        currentValue = newValue;
        $(element).val(newValue);
      });
    }
  };
});
                    

Performance

The amount of data on a Scope does not affect performance.

The number of $watches and the frequency of $digest cycles
can slow things down.

30 rows x 50 cols = 1500 cells

Don't call functions that do
resource extensive or
time consuming computations
from watched expressions!





Answer: {{ deepThought.compute(question) }}
                    



<input ng-model="question"
       ng-change="answer = deepThought.compute(question)">
Answer: {{ answer }}
                    



<input ng-model="question"
       ng-change="answer = deepThought.compute(question)"
       ng-model-options="{ debounce: 1000 }">
Answer: {{ answer }}
                    

OK, let's look at the
original example again


Todo:

    <li ng-repeat="todo in list" ng-click="list.splice($index, 1)"> {{todo}} </li>
expression
$watch
$watch
expression

expression
+ $watch
 expression
by Tero Parviainen

Know Your AngularJS Inside Out

http://teropa.info/build-your-own-angular

Questions?

THE END