Upload
fwdays
View
171
Download
1
Embed Size (px)
Citation preview
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; });