59
Inside The AngularJS Directive Compiler Tero Parviainen @teropa

"Inside The AngularJS Directive Compiler" by Tero Parviainen

  • Upload
    fwdays

  • View
    171

  • Download
    1

Embed Size (px)

Citation preview

Inside The AngularJS Directive Compiler

Tero Parviainen @teropa

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

Learning by Imitation

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

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

The Directive Compiler

$compile Directives

+ DOM

Compiled DOM

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

Directive Registration

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

Directive Usage

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

Compilation

The Compile Provider

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

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

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

Scopes

• Application data + behavior • Change detection • Events

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

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

Scope Hierarchy

$rootScope

$scope $scope

$scope

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

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

$rootScope

$scope $scope

$scope

Isolate Scopes

Isolate Bindings

$rootScope

$scope $scope

$scope

expression

attribute

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

Two-Way Data Binding

$rootScope

$scope $scope

$scope

expression

attribute

Two directions

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

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

teropa.info

-75% off list price with code

”FRAMEWORKSDAYS”