Directives
“Angular attempts to minimize the impedance mismatch between document centric HTML and what an application needs by creating new HTML constructs. Angular teaches the browser new syntax through a construct we call directives.” https://docs.angularjs.org/guide/introduction
<body ng-app="myApp"> <main-navigation> </main-navigation> <login-form orientation="vertical"> </login-form> <news-feed max-items="10"> </news-feed></body>
{ priority: 0, terminal: false, template: '<div></div>', templateNamespace: 'html', replace: false, multiElement: false, transclude: false, restrict: 'A', scope: false, controller: 'MyCtrl', controllerAs: 'myCtrl', bindToController: true, require: '^parentCtrl', compile: function(el) { return { pre: function(scope, el, attrs) { }, post: function(scope, el, attrs) { } } } }
src/ng/compile.js • 1307 lines of code • 84 functions
• Compilation & linking • Attribute management • Controllers • Isolate bindings • Templates & transclusion
https://github.com/es-analysis/plato
$compileProvider.directive('myClass', function() { return { compile: function(element) { element.addClass('decorated'); } }; });
Directive Registration
var root = document.querySelector('#root'); var $root = angular.element(root); $compile($root);
Compilation
Directive Registration
function $CompileProvider() { var directives = {}; this.directive = function(name, factory) { directives[name] = directives[name] || []; directives[name].push(factory()); }; }
Constructing $compile
function $CompileProvider() { var directives = {}; this.directive = function(name, factory) { directives[name] = directives[name] || []; directives[name].push(factory()); }; this.$get = function() { return function $compile(element) { }; }; }
The compileNode helper function
this.$get = function() { function compileNode(element) { } return function $compile(element) { compileNode(element); }; };
Collecting Directives
this.$get = function() { function collectDirectives(element) { } function compileNode(element) { var directives = collectDirectives(element); } return function $compile(element) { compileNode(element); }; };
Three Collection Strategies
this.$get = function() { function collectDirectives(element) { return collectElementDirectives(element) .concat(collectAttrDirectives(element)) .concat(collectClassDirectives(element)); } function compileNode(element) { var directives = collectDirectives(element); } return function $compile(element) { compileNode(element); }; };
Element Directives
function collectElementDirectives(element) { var elName = element[0].nodeName; var directiveName = _.camelCase(elName); return directives[directiveName] || []; }
Attribute Directives
function collectAttrDirectives(element) { var result = []; _.each(element[0].attributes, function(attr) { var dirName = _.camelCase(attr.name); result = result.concat(directives[dirName] || []); }); return result; }
Class Directives
function collectClassDirectives(element) { var result = []; _.each(element[0].classList, function(cName) { var dirName = _.camelCase(cName); result = result.concat(directives[dirName] || []); }); return result; }
Applying The Directives
function compileNode(element) { var directives = collectDirectives(element); directives.forEach(function(directive) { directive.compile(element); }); }
Recursing to Child Nodes
function compileNode(element) { var directives = collectDirectives(element); directives.forEach(function(directive) { directive.compile(element); }); element.children().forEach(compileNode); }
The Directive Compiler And Linker
$compile Directives
+ DOM
Compiled DOM +
Linker
Linker Linked DOM
Compiled DOM +
Scope
$compileProvider.directive('myClass', function() { return { compile: function(element) { return function link(scope, element) { element.addClass(scope.theClass); }; } }; });
Directive with a Link Function
var root = document.querySelector('#root'); var $root = angular.element(root); var linkFunction = $compile($root); $rootScope.theClass = 'decorated'; linkFunction($rootScope);
Linking
The Node Link Function
function compileNode(element) { var directives = collectDirectives(element); directives.forEach(function(directive) { directive.compile(element); }); element.children().forEach(compileNode); return function nodeLinkFn(scope) { }; } return function $compile(element) { return compileNode(element); };
Collect Link Functions
function compileNode(element) { var directives = collectDirectives(element), linkFns = []; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFns.push(linkFn); }); element.children().forEach(compileNode); return function nodeLinkFn(scope) { }; }
Apply Link Functions
function compileNode(element) { var directives = collectDirectives(element), linkFns = []; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFns.push(linkFn); }); element.children().forEach(compileNode); return function nodeLinkFn(scope) { linkFns.forEach(function(linkFn) { linkFn(scope, element); }); }; }
Collect Child Link Functions
function compileNode(element) { var directives = collectDirectives(element), linkFns = []; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFns.push(linkFn); }); var childLinkFns = element.children().map(compileNode); return function nodeLinkFn(scope) { linkFns.forEach(function(linkFn) { linkFn(scope, element); }); }; }
Apply Child Link Functions
function compileNode(element) { var directives = collectDirectives(element), linkFns = []; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFns.push(linkFn); }); var childLinkFns = element.children().map(compileNode); return function nodeLinkFn(scope) { childLinkFns.forEach(function(childLinkFn) { childLinkFn(scope); }); linkFns.forEach(function(linkFn) { linkFn(scope, element); }); }; }
Scope Hierarchy vs. DOM Hierarchy
<article ng-app="myApp"> <section ng-controller="..."></section> <section ng-controller="..."> <div ng-controller="..."> </div> </section> </article> $rootScope
$scope $scope
$scope
article
section section
div
$compileProvider.directive('myClass', function() { return { scope: true, compile: function(element) { return function link(scope, element) { scope.counter = 0; element.on('click', function() { scope.counter++; }); }; } }; });
Directive Requests a Scope
Remember The “New Scope Directive”
function compileNode(element) { var directives = collectDirectives(element), linkFns = [], newScopeDir; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFns.push(linkFn); if (directive.scope) { if (newScopeDir) { throw 'No more than 1 new scope plz!'; } newScopeDir = directive; } }); // ... }
Make a new scope during linking
return function nodeLinkFn(scope) { if (newScopeDir) { scope = scope.$new(); } childLinkFns.forEach(function(childLinkFn) { childLinkFn(scope); }); linkFns.forEach(function(linkFn) { linkFn(scope, element); }); };
Directive with Isolate Scope & Bindings <div click-logger="'Hello!'"></div> $compileProvider.directive('clickLogger', function() { return { scope: { message: '=clickLogger' }, compile: function(element) { return function link(scope, element) { scope.counter = 0; element.on('click', function() { console.log(scope.message); }); }; } }; });
Remember The “Iso Scope Directive”
function compileNode(element) { var directives = collectDirectives(element), linkFns = [], newScopeDir, newIsoScopeDir; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFns.push(linkFn); if (directive.scope) { if (newScopeDir || newIsoScopeDir) { throw 'No more than 1 new scope plz!'; } if (_.isObject(directive.scope)) { newIsoScopeDir = directive; } else { newScopeDir = directive; } } }); // ... }
Create Isolate Scope During Linking
return function nodeLinkFn(scope) { var isoScope; if (newScopeDir) { scope = scope.$new(); } if (newIsoScopeDir) { isoScope = scope.$new(true); } childLinkFns.forEach(function(childLinkFn) { childLinkFn(scope); }); linkFns.forEach(function(linkFn) { linkFn(scope, element); }); };
Remember Link Function Directives
function compileNode(element) { var directives = collectDirectives(element), linkFns = [], newScopeDir, newIsoScopeDir; directives.forEach(function(directive) { var linkFn = directive.compile(element); linkFn.directive = directive; linkFns.push(linkFn); if (directive.scope) { if (newScopeDir || newIsoScopeDir) { throw 'No more than 1 new scope plz!'; } if (_.isObject(directive.scope)) { newIsoScopeDir = directive; } else { newScopeDir = directive; } } }); // ... }
Apply Isolate Scope
return function nodeLinkFn(scope) { var isoScope; if (newScopeDir) { scope = scope.$new(); } if (newIsoScopeDir) { isoScope = scope.$new(true); } childLinkFns.forEach(function(childLinkFn) { childLinkFn(scope); }); linkFns.forEach(function(linkFn) { var isIso = (linkFn.directive === newIsoScopeDir); linkFn(isIso ? isoScope : scope, element); }); };
Isolate Bindings <div click-logger="'Hello!'"></div> $compileProvider.directive('clickLogger', function() { return { scope: { message: '=clickLogger' }, compile: function(element) { return function link(scope, element) { scope.counter = 0; element.on('click', function() { console.log(scope.message); }); }; } }; });
Loop Over Isolate Bindings
return function nodeLinkFn(scope) { var isoScope; if (newScopeDir) { scope = scope.$new(); } if (newIsoScopeDir) { isoScope = scope.$new(true); _.forOwn( newIsoScopeDir.scope, function(spec, scopeName) { } ); } // ... };
Get Attribute Expression
if (newIsoScopeDir) { isoScope = scope.$new(true); _.forOwn( newIsoScopeDir.scope, function(spec, scopeName) { var attrName = spec.match(/^=(.*)/)[1]; var denormalized = _.kebabCase(attrName); var expr = element.attr(denormalized); } ); }
Watch & Bind The Expression
if (newIsoScopeDir) { isoScope = scope.$new(true); _.forOwn( newIsoScopeDir.scope, function(spec, scopeName) { var attrName = spec.match(/^=(.*)/)[1]; var denormalized = _.kebabCase(attrName); var expr = element.attr(denormalized); scope.$watch(expr, function(newValue) { isoScope[scopeName] = newValue; }); } ); }
Parse Expression String to Function
if (newIsoScopeDir) { isoScope = scope.$new(true); _.forOwn( newIsoScopeDir.scope, function(spec, scopeName) { var attrName = spec.match(/^=(.*)/)[1]; var denormalized = _.kebabCase(attrName); var expr = element.attr(denormalized); var exprFn = $parse(expr); scope.$watch(exprFn, function(newValue) { isoScope[scopeName] = newValue; }); } ); }
Refactor The Watcher
if (newIsoScopeDir) { isoScope = scope.$new(true); _.forOwn( newIsoScopeDir.scope, function(spec, scopeName) { var attrName = spec.match(/^=(.*)/)[1]; var denormalized = _.kebabCase(attrName); var expr = element.attr(denormalized); var exprFn = $parse(expr); scope.$watch(function() { var newParentValue = exprFn(scope); var childValue = isoScope[scopeName]; if (newParentValue !== childValue) { isoScope[scopeName] = newParentValue; } }); } ); }
Track The Parent Value
_.forOwn( newIsoScopeDir.scope, function(spec, scopeName) { var attrName = spec.match(/^=(.*)/)[1]; var denormalized = _.kebabCase(attrName); var expr = element.attr(denormalized); var exprFn = $parse(expr); var parentValue; scope.$watch(function() { var newParentValue = exprFn(scope); var childValue = isoScope[scopeName]; if (newParentValue !== childValue) { isoScope[scopeName] = newParentValue; } parentValue = newParentValue; }); } );
Check for Parent vs. Child Change
var parentValue; scope.$watch(function() { var newParentValue = exprFn(scope); var childValue = isoScope[scopeName]; if (newParentValue !== childValue) { if (newParentValue !== parentValue) { isoScope[scopeName] = newParentValue; } else { } } parentValue = newParentValue; });
Propagate Change Up
var parentValue; scope.$watch(function() { var newParentValue = exprFn(scope); var childValue = isoScope[scopeName]; if (newParentValue !== childValue) { if (newParentValue !== parentValue) { isoScope[scopeName] = newParentValue; } else { exprFn.assign(scope, childValue); newParentValue = childValue; } } parentValue = newParentValue; });