Top Banner
Inside The AngularJS Directive Compiler Tero Parviainen @teropa
59

"Inside The AngularJS Directive Compiler" by Tero Parviainen

Jul 29, 2015

Download

Technology

fwdays
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Inside The AngularJS Directive Compiler

Tero Parviainen @teropa

Page 2: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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

Page 3: "Inside The AngularJS Directive Compiler" by Tero Parviainen

<body ng-app="myApp"> <main-navigation> </main-navigation> <login-form orientation="vertical"> </login-form> <news-feed max-items="10"> </news-feed></body>

Page 4: "Inside The AngularJS Directive Compiler" by Tero Parviainen

{ 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) { } } } }

Page 5: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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

Page 6: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Learning by Imitation

Page 7: "Inside The AngularJS Directive Compiler" by Tero Parviainen

1.  Compilation 2.  Linking 3.  Inheritance 4.  Isolation

Page 8: "Inside The AngularJS Directive Compiler" by Tero Parviainen

1.  Compilation 2.  Linking 3.  Inheritance 4.  Isolation

Page 9: "Inside The AngularJS Directive Compiler" by Tero Parviainen

The Directive Compiler

$compile Directives

+ DOM

Compiled DOM

Page 10: "Inside The AngularJS Directive Compiler" by Tero Parviainen

$compileProvider.directive('myClass', function() { return { compile: function(element) { element.addClass('decorated'); } }; });

Directive Registration

Page 11: "Inside The AngularJS Directive Compiler" by Tero Parviainen

<div id="root"> <div my-class></div> </div>

Directive Usage

Page 12: "Inside The AngularJS Directive Compiler" by Tero Parviainen

var root = document.querySelector('#root'); var $root = angular.element(root); $compile($root);

Compilation

Page 13: "Inside The AngularJS Directive Compiler" by Tero Parviainen

The Compile Provider

function $CompileProvider() { this.directive = function(name, factory) { }; }

Page 14: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Directive Registration

function $CompileProvider() { var directives = {}; this.directive = function(name, factory) { directives[name] = directives[name] || []; directives[name].push(factory()); }; }

Page 15: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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) { }; }; }

Page 16: "Inside The AngularJS Directive Compiler" by Tero Parviainen

The compileNode helper function

this.$get = function() { function compileNode(element) { } return function $compile(element) { compileNode(element); }; };

Page 17: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Collecting Directives

this.$get = function() { function collectDirectives(element) { } function compileNode(element) { var directives = collectDirectives(element); } return function $compile(element) { compileNode(element); }; };

Page 18: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }; };

Page 19: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Element Directives

function collectElementDirectives(element) { var elName = element[0].nodeName; var directiveName = _.camelCase(elName); return directives[directiveName] || []; }

Page 20: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; }

Page 21: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Class Directives

function collectClassDirectives(element) { var result = []; _.each(element[0].classList, function(cName) { var dirName = _.camelCase(cName); result = result.concat(directives[dirName] || []); }); return result; }

Page 22: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Applying The Directives

function compileNode(element) { var directives = collectDirectives(element); directives.forEach(function(directive) { directive.compile(element); }); }

Page 23: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Recursing to Child Nodes

function compileNode(element) { var directives = collectDirectives(element); directives.forEach(function(directive) { directive.compile(element); }); element.children().forEach(compileNode); }

Page 24: "Inside The AngularJS Directive Compiler" by Tero Parviainen

1.  Compilation 2.  Linking 3.  Inheritance 4.  Isolation

Page 25: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Scopes

• Application data + behavior • Change detection • Events

Page 26: "Inside The AngularJS Directive Compiler" by Tero Parviainen

The Directive Compiler And Linker

$compile Directives

+ DOM

Compiled DOM +

Linker

Linker Linked DOM

Compiled DOM +

Scope

Page 27: "Inside The AngularJS Directive Compiler" by Tero Parviainen

$compileProvider.directive('myClass', function() { return { compile: function(element) { return function link(scope, element) { element.addClass(scope.theClass); }; } }; });

Directive with a Link Function

Page 28: "Inside The AngularJS Directive Compiler" by Tero Parviainen

var root = document.querySelector('#root'); var $root = angular.element(root); var linkFunction = $compile($root); $rootScope.theClass = 'decorated'; linkFunction($rootScope);

Linking

Page 29: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); };

Page 30: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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) { }; }

Page 31: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); }; }

Page 32: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); }; }

Page 33: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); }; }

Page 34: "Inside The AngularJS Directive Compiler" by Tero Parviainen

1.  Compilation 2.  Linking 3.  Inheritance 4.  Isolation

Page 35: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Scope Hierarchy

$rootScope

$scope $scope

$scope

Page 36: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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

Page 37: "Inside The AngularJS Directive Compiler" by Tero Parviainen

$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

Page 38: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; } }); // ... }

Page 39: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); };

Page 40: "Inside The AngularJS Directive Compiler" by Tero Parviainen

1.  Compilation 2.  Linking 3.  Inheritance 4.  Isolation

Page 41: "Inside The AngularJS Directive Compiler" by Tero Parviainen

$rootScope

$scope $scope

$scope

Isolate Scopes

Page 42: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Isolate Bindings

$rootScope

$scope $scope

$scope

expression

attribute

Page 43: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); }; } }; });

Page 44: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; } } }); // ... }

Page 45: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); };

Page 46: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; } } }); // ... }

Page 47: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); };

Page 48: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); }); }; } }; });

Page 49: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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) { } ); } // ... };

Page 50: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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); } ); }

Page 51: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; }); } ); }

Page 52: "Inside The AngularJS Directive Compiler" by Tero Parviainen

Two-Way Data Binding

$rootScope

$scope $scope

$scope

expression

attribute

Two directions

Page 53: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; }); } ); }

Page 54: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; } }); } ); }

Page 55: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; }); } );

Page 56: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; });

Page 57: "Inside The AngularJS Directive Compiler" by Tero Parviainen

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; });

Page 58: "Inside The AngularJS Directive Compiler" by Tero Parviainen

1.  Compilation 2.  Linking 3.  Inheritance 4.  Isolation

Page 59: "Inside The AngularJS Directive Compiler" by Tero Parviainen

teropa.info

-75% off list price with code

”FRAMEWORKSDAYS”