From ea7b680a158cb13aba66b2fb2589a922673c2bf4 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 14 Nov 2016 20:26:32 +0100 Subject: [PATCH 1/6] docs($compile): add double compilation known issue Related #15278 --- src/ng/compile.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/ng/compile.js b/src/ng/compile.js index afd0a803b819..95090264e2b8 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -943,6 +943,103 @@ * * For information on how the compiler works, see the * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + * Double compilation occurs when an already compiled part of the DOM gets compiled again. This is + * not an intended use case and can lead to misbehaving directives, performance issues, and memory + * leaks. + * A common scenario where this happens is a directive that calls `$compile` in a directive link + * function on the directive element: + * + * ``` + angular.module('app').directive('addInput', function($compile) { + return { + link: function(scope, element, attrs) { + var newEl = angular.element(''); + attrs.$set('addInput', null) // To stop infinite compile loop + element.append(newEl); + $compile(element)(scope); // Double compilation + } + } + }) + ``` + * At first glance, it looks like removing the original `addInput` attribute is all there is needed + * to make this example work. + * However, if the directive element or its children have other directives attached, they will be compiled and + * linked again, because the compiler doesn't keep track of which directives have been assigned to which + * elements. + * + * This can cause unpredictable behavior, e.g. `ngModel` $formatters and $parsers will be + * attached again to the ngModelController. It can also degrade performance, as + * watchers for text interpolation are added twice to the scope. + * + * Double compilation should therefore avoided. In the above example, the better way is to only + * compile the new element: + * ``` + angular.module('app').directive('addInput', function($compile) { + return { + link: function(scope, element, attrs) { + var newEl = angular.element(''); + $compile(newEl)(scope); // Only compile the new element + element.append(newEl); + } + } + }) + ``` + * + * Another scenario is adding a directive programmatically to a compiled element and then executing + * compile again. + * ```html + * + * ``` + * + ``` + angular.module('app').directive('addOptions', function($compile) { + return { + link: function(scope, element, attrs) { + attrs.$set('addInput', null) // To stop infinite compile loop + attrs.$set('ngModelOptions', '{debounce: 1000}'); + $compile(element)(scope); // Double compilation + } + } + }); + ``` + * + * In that case, it is necessary to intercept the *initial* compilation of the element: + * + * 1. give your directive the `terminal` property and a higher priority than directives + * that should not be compiled twice. In the example, the compiler will only compile directives + * which have a priority of 100 or higher. + * 2. inside this directive's compile function, remove the original directive attribute from the element, + * and add any other directive attributes. Removing the attribute is necessary, because otherwise the + * compilation would result in an infinite loop. + * 3. compile the element but restrict the maximum priority, so that any already compiled directives + * are not compiled twice. + * 4. in the link function, link the compiled element with the element's scope + * + * ``` + angular.module('app').directive('addOptions', function($compile) { + return { + priority: 100, // ngModel has priority 1 + terminal: true, + template: '', + compile: function(templateElement, templateAttributes) { + templateAttributes.$set('addOptions', null); + templateAttributes.$set('ngModelOptions', '{debounce: 1000}'); + + var compiled = $compile(templateElement, null, 100); + + return function linkFn(scope) { + compiled(scope) // Link compiled element to scope + } + } + } + }); + ``` + * */ var $compileMinErr = minErr('$compile'); From ae6a1263ee51211da6b2ab5712508f405b1964f7 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 15 Nov 2016 18:23:31 +0100 Subject: [PATCH 2/6] fixup: better examples --- src/ng/compile.js | 49 +++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 95090264e2b8..eff83c7ea988 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -952,39 +952,49 @@ * not an intended use case and can lead to misbehaving directives, performance issues, and memory * leaks. * A common scenario where this happens is a directive that calls `$compile` in a directive link - * function on the directive element: + * function on the directive element. In the following example, a directive adds a mouseover behavior + * to a button with `ngClick` on it: * * ``` - angular.module('app').directive('addInput', function($compile) { + angular.module('app').directive('addMouseover', function($compile) { return { link: function(scope, element, attrs) { - var newEl = angular.element(''); - attrs.$set('addInput', null) // To stop infinite compile loop + var newEl = angular.element(' My Hint'); + element.on('mouseenter mouseout', function() { + scope.$apply('showHint = !showHint'); + }); + + attrs.$set('addMouseover', null); // To stop infinite compile loop element.append(newEl); $compile(element)(scope); // Double compilation } } }) ``` - * At first glance, it looks like removing the original `addInput` attribute is all there is needed + * At first glance, it looks like removing the original `addMouseover` attribute is all there is needed * to make this example work. * However, if the directive element or its children have other directives attached, they will be compiled and * linked again, because the compiler doesn't keep track of which directives have been assigned to which * elements. * - * This can cause unpredictable behavior, e.g. `ngModel` $formatters and $parsers will be - * attached again to the ngModelController. It can also degrade performance, as - * watchers for text interpolation are added twice to the scope. + * This can cause unpredictable behavior, e.g. `ngClick` or other event handlers will be attached + * again. It can also degrade performance, as watchers for text interpolation are added twice to the scope. + * + * Double compilation should therefore be avoided. In the above example, only the new element should + * be compiled: * - * Double compilation should therefore avoided. In the above example, the better way is to only - * compile the new element: * ``` - angular.module('app').directive('addInput', function($compile) { + angular.module('app').directive('addMouseover', function($compile) { return { link: function(scope, element, attrs) { - var newEl = angular.element(''); - $compile(newEl)(scope); // Only compile the new element + var newEl = angular.element(' My Hint'); + element.on('mouseenter mouseout', function() { + scope.$apply('showHint = !showHint'); + }); + + attrs.$set('addMouseover', null); element.append(newEl); + $compile(newEl)(scope); // Only compile the new element } } }) @@ -1000,7 +1010,7 @@ angular.module('app').directive('addOptions', function($compile) { return { link: function(scope, element, attrs) { - attrs.$set('addInput', null) // To stop infinite compile loop + attrs.$set('addOptions', null) // To stop infinite compile loop attrs.$set('ngModelOptions', '{debounce: 1000}'); $compile(element)(scope); // Double compilation } @@ -1010,15 +1020,15 @@ * * In that case, it is necessary to intercept the *initial* compilation of the element: * - * 1. give your directive the `terminal` property and a higher priority than directives + * 1. Give your directive the `terminal` property and a higher priority than directives * that should not be compiled twice. In the example, the compiler will only compile directives * which have a priority of 100 or higher. - * 2. inside this directive's compile function, remove the original directive attribute from the element, + * 2. Inside this directive's compile function, remove the original directive attribute from the element, * and add any other directive attributes. Removing the attribute is necessary, because otherwise the * compilation would result in an infinite loop. - * 3. compile the element but restrict the maximum priority, so that any already compiled directives + * 3. Compile the element but restrict the maximum priority, so that any already compiled directives * are not compiled twice. - * 4. in the link function, link the compiled element with the element's scope + * 4. In the link function, link the compiled element with the element's scope * * ``` angular.module('app').directive('addOptions', function($compile) { @@ -1027,9 +1037,10 @@ terminal: true, template: '', compile: function(templateElement, templateAttributes) { - templateAttributes.$set('addOptions', null); templateAttributes.$set('ngModelOptions', '{debounce: 1000}'); + // The third argument is the max priority. Only directives with priority < 100 will be compiled, + // therefore we don't need to remove the attribute var compiled = $compile(templateElement, null, 100); return function linkFn(scope) { From 275b4e3c39f508cac50fe61a07b2589a783489a7 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 15 Nov 2016 18:34:48 +0100 Subject: [PATCH 3/6] move to the compiler guide --- docs/content/guide/compiler.ngdoc | 107 ++++++++++++++++++++++++++++++ src/ng/compile.js | 106 ++--------------------------- 2 files changed, 111 insertions(+), 102 deletions(-) diff --git a/docs/content/guide/compiler.ngdoc b/docs/content/guide/compiler.ngdoc index 1dd5bda50107..baf5fc65f450 100644 --- a/docs/content/guide/compiler.ngdoc +++ b/docs/content/guide/compiler.ngdoc @@ -380,3 +380,110 @@ restrict: 'E', replace: true ``` +### Double Compilation, and how to avoid it + +Double compilation occurs when an already compiled part of the DOM gets compiled again. This is an +undesired effect and can lead to misbehaving directives, performance issues, and memory +leaks. +A common scenario where this happens is a directive that calls `$compile` in a directive link +function on the directive element. In the following example, a directive adds a mouseover behavior +to a button with `ngClick` on it: + +``` +angular.module('app').directive('addMouseover', function($compile) { + return { + link: function(scope, element, attrs) { + var newEl = angular.element(' My Hint'); + element.on('mouseenter mouseout', function() { + scope.$apply('showHint = !showHint'); + }); + + attrs.$set('addMouseover', null); // To stop infinite compile loop + element.append(newEl); + $compile(element)(scope); // Double compilation + } + } +}) +``` + +At first glance, it looks like removing the original `addMouseover` attribute is all there is needed +to make this example work. +However, if the directive element or its children have other directives attached, they will be compiled and +linked again, because the compiler doesn't keep track of which directives have been assigned to which +elements. + +This can cause unpredictable behavior, e.g. `ngClick` or other event handlers will be attached +again. It can also degrade performance, as watchers for text interpolation are added twice to the scope. + +Double compilation should therefore be avoided. In the above example, only the new element should +be compiled: + +``` +angular.module('app').directive('addMouseover', function($compile) { + return { + link: function(scope, element, attrs) { + var newEl = angular.element(' My Hint'); + element.on('mouseenter mouseout', function() { + scope.$apply('showHint = !showHint'); + }); + + attrs.$set('addMouseover', null); + element.append(newEl); + $compile(newEl)(scope); // Only compile the new element + } + } +}) +``` + +Another scenario is adding a directive programmatically to a compiled element and then executing +compile again. + +```html + +``` + +``` +angular.module('app').directive('addOptions', function($compile) { + return { + link: function(scope, element, attrs) { + attrs.$set('addOptions', null) // To stop infinite compile loop + attrs.$set('ngModelOptions', '{debounce: 1000}'); + $compile(element)(scope); // Double compilation + } + } +}); +``` + +In that case, it is necessary to intercept the *initial* compilation of the element: + + 1. Give your directive the `terminal` property and a higher priority than directives + that should not be compiled twice. In the example, the compiler will only compile directives + which have a priority of 100 or higher. + 2. Inside this directive's compile function, remove the original directive attribute from the element, + and add any other directive attributes. Removing the attribute is necessary, because otherwise the + compilation would result in an infinite loop. + 3. Compile the element but restrict the maximum priority, so that any already compiled directives + are not compiled twice. + 4. In the link function, link the compiled element with the element's scope + +``` +angular.module('app').directive('addOptions', function($compile) { + return { + priority: 100, // ngModel has priority 1 + terminal: true, + template: '', + compile: function(templateElement, templateAttributes) { + templateAttributes.$set('ngModelOptions', '{debounce: 1000}'); + + // The third argument is the max priority. Only directives with priority < 100 will be compiled, + // therefore we don't need to remove the attribute + var compiled = $compile(templateElement, null, 100); + + return function linkFn(scope) { + compiled(scope) // Link compiled element to scope + } + } + } +}); +``` + diff --git a/src/ng/compile.js b/src/ng/compile.js index eff83c7ea988..febb37553a26 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -948,108 +948,10 @@ * * ### Double Compilation * - * Double compilation occurs when an already compiled part of the DOM gets compiled again. This is - * not an intended use case and can lead to misbehaving directives, performance issues, and memory - * leaks. - * A common scenario where this happens is a directive that calls `$compile` in a directive link - * function on the directive element. In the following example, a directive adds a mouseover behavior - * to a button with `ngClick` on it: - * - * ``` - angular.module('app').directive('addMouseover', function($compile) { - return { - link: function(scope, element, attrs) { - var newEl = angular.element(' My Hint'); - element.on('mouseenter mouseout', function() { - scope.$apply('showHint = !showHint'); - }); - - attrs.$set('addMouseover', null); // To stop infinite compile loop - element.append(newEl); - $compile(element)(scope); // Double compilation - } - } - }) - ``` - * At first glance, it looks like removing the original `addMouseover` attribute is all there is needed - * to make this example work. - * However, if the directive element or its children have other directives attached, they will be compiled and - * linked again, because the compiler doesn't keep track of which directives have been assigned to which - * elements. - * - * This can cause unpredictable behavior, e.g. `ngClick` or other event handlers will be attached - * again. It can also degrade performance, as watchers for text interpolation are added twice to the scope. - * - * Double compilation should therefore be avoided. In the above example, only the new element should - * be compiled: - * - * ``` - angular.module('app').directive('addMouseover', function($compile) { - return { - link: function(scope, element, attrs) { - var newEl = angular.element(' My Hint'); - element.on('mouseenter mouseout', function() { - scope.$apply('showHint = !showHint'); - }); - - attrs.$set('addMouseover', null); - element.append(newEl); - $compile(newEl)(scope); // Only compile the new element - } - } - }) - ``` - * - * Another scenario is adding a directive programmatically to a compiled element and then executing - * compile again. - * ```html - * - * ``` - * - ``` - angular.module('app').directive('addOptions', function($compile) { - return { - link: function(scope, element, attrs) { - attrs.$set('addOptions', null) // To stop infinite compile loop - attrs.$set('ngModelOptions', '{debounce: 1000}'); - $compile(element)(scope); // Double compilation - } - } - }); - ``` - * - * In that case, it is necessary to intercept the *initial* compilation of the element: - * - * 1. Give your directive the `terminal` property and a higher priority than directives - * that should not be compiled twice. In the example, the compiler will only compile directives - * which have a priority of 100 or higher. - * 2. Inside this directive's compile function, remove the original directive attribute from the element, - * and add any other directive attributes. Removing the attribute is necessary, because otherwise the - * compilation would result in an infinite loop. - * 3. Compile the element but restrict the maximum priority, so that any already compiled directives - * are not compiled twice. - * 4. In the link function, link the compiled element with the element's scope - * - * ``` - angular.module('app').directive('addOptions', function($compile) { - return { - priority: 100, // ngModel has priority 1 - terminal: true, - template: '', - compile: function(templateElement, templateAttributes) { - templateAttributes.$set('ngModelOptions', '{debounce: 1000}'); - - // The third argument is the max priority. Only directives with priority < 100 will be compiled, - // therefore we don't need to remove the attribute - var compiled = $compile(templateElement, null, 100); - - return function linkFn(scope) { - compiled(scope) // Link compiled element to scope - } - } - } - }); - ``` + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. * */ From 53bef9809f9b86137f3f3a5da95ddf3317b9a6bf Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 17 Nov 2016 19:28:05 +0100 Subject: [PATCH 4/6] fixup --- docs/content/guide/compiler.ngdoc | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/content/guide/compiler.ngdoc b/docs/content/guide/compiler.ngdoc index baf5fc65f450..5e0d250cb158 100644 --- a/docs/content/guide/compiler.ngdoc +++ b/docs/content/guide/compiler.ngdoc @@ -386,7 +386,7 @@ Double compilation occurs when an already compiled part of the DOM gets compiled undesired effect and can lead to misbehaving directives, performance issues, and memory leaks. A common scenario where this happens is a directive that calls `$compile` in a directive link -function on the directive element. In the following example, a directive adds a mouseover behavior +function on the directive element. In the following **faulty example**, a directive adds a mouseover behavior to a button with `ngClick` on it: ``` @@ -423,11 +423,10 @@ angular.module('app').directive('addMouseover', function($compile) { return { link: function(scope, element, attrs) { var newEl = angular.element(' My Hint'); - element.on('mouseenter mouseout', function() { + element.on('mouseenter mouseleave', function() { scope.$apply('showHint = !showHint'); }); - attrs.$set('addMouseover', null); element.append(newEl); $compile(newEl)(scope); // Only compile the new element } @@ -436,7 +435,7 @@ angular.module('app').directive('addMouseover', function($compile) { ``` Another scenario is adding a directive programmatically to a compiled element and then executing -compile again. +compile again. See the following **faulty example**: ```html @@ -459,11 +458,9 @@ In that case, it is necessary to intercept the *initial* compilation of the elem 1. Give your directive the `terminal` property and a higher priority than directives that should not be compiled twice. In the example, the compiler will only compile directives which have a priority of 100 or higher. - 2. Inside this directive's compile function, remove the original directive attribute from the element, - and add any other directive attributes. Removing the attribute is necessary, because otherwise the - compilation would result in an infinite loop. - 3. Compile the element but restrict the maximum priority, so that any already compiled directives - are not compiled twice. + 2. Inside this directive's compile function, add any other directive attributes to the template. + 3. Compile the element, but restrict the maximum priority, so that any already compiled directives + (including the `addOptions` directive) are not compiled again. 4. In the link function, link the compiled element with the element's scope ``` @@ -471,7 +468,6 @@ angular.module('app').directive('addOptions', function($compile) { return { priority: 100, // ngModel has priority 1 terminal: true, - template: '', compile: function(templateElement, templateAttributes) { templateAttributes.$set('ngModelOptions', '{debounce: 1000}'); From 6b1159191eedcef2ce4c704c3774d93982b02c5d Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 22 Nov 2016 23:23:47 +0100 Subject: [PATCH 5/6] Add period --- docs/content/guide/compiler.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/guide/compiler.ngdoc b/docs/content/guide/compiler.ngdoc index 5e0d250cb158..cc12623ef880 100644 --- a/docs/content/guide/compiler.ngdoc +++ b/docs/content/guide/compiler.ngdoc @@ -461,7 +461,7 @@ In that case, it is necessary to intercept the *initial* compilation of the elem 2. Inside this directive's compile function, add any other directive attributes to the template. 3. Compile the element, but restrict the maximum priority, so that any already compiled directives (including the `addOptions` directive) are not compiled again. - 4. In the link function, link the compiled element with the element's scope + 4. In the link function, link the compiled element with the element's scope. ``` angular.module('app').directive('addOptions', function($compile) { From 9114144d758550431c9904c02cd15d27a56265c6 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 23 Nov 2016 13:05:15 +0100 Subject: [PATCH 6/6] remove second mouseout --- docs/content/guide/compiler.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/guide/compiler.ngdoc b/docs/content/guide/compiler.ngdoc index cc12623ef880..28a4eb0ff400 100644 --- a/docs/content/guide/compiler.ngdoc +++ b/docs/content/guide/compiler.ngdoc @@ -394,7 +394,7 @@ angular.module('app').directive('addMouseover', function($compile) { return { link: function(scope, element, attrs) { var newEl = angular.element(' My Hint'); - element.on('mouseenter mouseout', function() { + element.on('mouseenter mouseleave', function() { scope.$apply('showHint = !showHint'); });