From 6f96a7615cd71b88a43192bef7417359518c7d1e Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 23 Aug 2014 07:37:34 +0100 Subject: [PATCH 01/15] feat($compile/ngBind): allow disabling binding info The compiler and ngBind directives add binding information (`ng-binding` CSS class and `$binding` data property) to elements when they are bound to the scope. This is only to aid testing and debugging for tools such as Protractor and Batarang. In production this is unnecessary and add a performance penalty. This can be now disabled by calling `$compileProvider.debugInfoEnabled(false)` in a module `config` block: ``` someModule.config(['$compileProvider', function($compileProvider) { $compileProvider.debugInfoEnabled(false); }]); ``` In the bench/apps/largetable-bp benchmark this change, with debug info disabled, improved by ~140ms, that is 10%. Measuring the "create" phase, 25 loops, mean time ~1340ms -> ~1200ms. We were storing the whole `interpolationFn` in the `$binding` data on elements but this function was bringing a lot of closure variables with it and so was consuming unwanted amounts of memory. Now we are only storing the parsed interpolation expressions from the binding (i.e. the values of `interpolationFn.expressions`). BREAKING CHANGE: The value of `$binding` data property on an element is always an array now and the expressions do not include the curly braces `{{ ... }}`. --- src/ng/compile.js | 47 ++++++++++++++++++++++++++------- src/ng/directive/ngBind.js | 47 +++++++++++++++------------------ src/ngScenario/Scenario.js | 3 --- test/ng/compileSpec.js | 30 +++++++++++++++++---- test/ng/directive/ngBindSpec.js | 7 ----- 5 files changed, 84 insertions(+), 50 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index ac0eab395690..0de0532be36c 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -668,6 +668,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + /** + * @ngdoc method + * @name $compileProvider#debugInfoEnabled + * + * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the + * current debugInfoEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable various debug runtime information in the compiler such as adding + * binding information and a reference to the current scope on to DOM elements. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `$binding` data property containing an array of the binding expressions + * + * The default value is true. + */ + var debugInfoEnabled = true; + this.debugInfoEnabled = function(enabled) { + if(isDefined(enabled)) { + debugInfoEnabled = enabled; + return this; + } + return debugInfoEnabled; + }; + this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', @@ -867,6 +895,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo(element, binding) { + element + .addClass('ng-binding') + .data('$binding', + (element.data('$binding') || []).concat(binding.expressions || [binding]) + ); + } : noop; return compile; @@ -1950,17 +1985,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directives.push({ priority: 0, compile: function textInterpolateCompileFn(templateNode) { - // when transcluding a template that has bindings in the root - // then we don't have a parent and should do this in the linkFn - var parent = templateNode.parent(), hasCompileParent = parent.length; - if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding'); - return function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - parent.data('$binding', bindings); - if (!hasCompileParent) safeAddClass(parent, 'ng-binding'); + var parent = node.parent(); + compile.$$addBindingInfo(parent, interpolateFn); scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { node[0].nodeValue = value; }); diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index d4dd81fc9121..6be2dfd05fd2 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -51,23 +51,22 @@ */ -var ngBindDirective = ngDirective({ - compile: function ngBindCompile(templateElement) { - templateElement.addClass('ng-binding'); - - return function ngBindLink(scope, element, attr) { - element.data('$binding', attr.ngBind); - element = element[0]; - - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.textContent = (value == undefined ? '' : value); - }); - }; - } -}); +var ngBindDirective = ['$compile', function($compile) { + return { + restrict: 'AC', + compile: function(templateElement) { + return function (scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBind); + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + // We are purposefully using == here rather than === because we want to + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); + }; + } + }; +}]; /** @@ -121,11 +120,10 @@ var ngBindDirective = ngDirective({ */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { +var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { return function(scope, element, attr) { - // TODO: move this to scenario runner var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); + $compile.$$addBindingInfo(element, interpolateFn); attr.$observe('ngBindTemplate', function(value) { element.text(value); }); @@ -178,14 +176,13 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { */ -var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { +var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { return { restrict: 'A', - compile: function ngBindCompile(tElement, tAttrs) { - tElement.addClass('ng-binding'); + compile: function (tElement, tAttrs) { - return function ngBindLink(scope, element, attr) { - element.data('$binding', attr.ngBindHtml); + return function (scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBindHtml); var ngBindHtmlGetter = $parse(attr.ngBindHtml); var ngBindHtmlWatch = $parse(attr.ngBindHtml, function getStringValue(value) { return (value || '').toString(); diff --git a/src/ngScenario/Scenario.js b/src/ngScenario/Scenario.js index 6cf69d84ce79..7ee8030d404c 100644 --- a/src/ngScenario/Scenario.js +++ b/src/ngScenario/Scenario.js @@ -304,9 +304,6 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { var element = windowJquery(this), bindings; if (bindings = element.data('$binding')) { - if (!angular.isArray(bindings)) { - bindings = [bindings]; - } for(var expressions = [], binding, j=0, jj=bindings.length; j{{1+2}}')($rootScope); - expect(element.hasClass('ng-binding')).toBe(true); - expect(element.data('$binding')[0].exp).toEqual('{{1+2}}'); - })); + expect(element.hasClass('ng-binding')).toBe(false); + expect(element.data('$binding')).toBeUndefined(); + }); + }); + it('should occur if `debugInfoEnabled` is true', function() { + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }); + + inject(function($compile, $rootScope) { + element = $compile('
{{1+2}}
')($rootScope); + expect(element.hasClass('ng-binding')).toBe(true); + expect(element.data('$binding')).toEqual(['1+2']); + }); + }); + }); + it('should observe interpolated attrs', inject(function($rootScope, $compile) { $compile('
')($rootScope); diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index 704932b09085..2a1f00637044 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -122,13 +122,6 @@ describe('ngBind*', function() { describe('ngBindHtml', function() { - it('should add ng-binding class to the element in compile phase', inject(function($compile) { - var element = jqLite('
'); - $compile(element); - expect(element.hasClass('ng-binding')).toBe(true); - })); - - describe('SCE disabled', function() { beforeEach(function() { module(function($sceProvider) { $sceProvider.enabled(false); }); From d7c08e1f15ffd1e6ef76487f7af6adec7cc671da Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 23 Aug 2014 07:37:34 +0100 Subject: [PATCH 02/15] feat($compile): allow disabling scope info The compiler adds scope information (`ng-scope` CSS class and `$scope` data property) to elements when the are bound to the scope. This is mostly to aid debugging tools such as Batarang. In production this should be unnecesary and adds a performance penalty. In the bench/apps/largetable-bp this change caused an improvement of ~100ms (7%). This can be now disabled by calling `$compileProvider.debugInfoEnabled(false)` in a module `config` block: ``` someModule.config(['$compileProvider', function($compileProvider) { $compileProvider.debugInfoEnabled(false); }]); ``` In the bench/apps/largetable-bp benchmark this change, with debug info disabled, improved by ~120ms, that is ~10%. Measuring the "create" phase, 25 loops, mean time ~1200ms -> ~1080ms. --- src/ng/compile.js | 55 +++++++++++++++++++----------------------- test/ng/compileSpec.js | 49 ++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 0de0532be36c..391d19fd1fb9 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -886,6 +886,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + + function safeAddClass($element, className) { + try { + $element.addClass(className); + } catch(e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') @@ -896,11 +907,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { NG_ATTR_BINDING = /^ngAttr[A-Z]/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo(element, binding) { - element - .addClass('ng-binding') - .data('$binding', - (element.data('$binding') || []).concat(binding.expressions || [binding]) - ); + safeAddClass(element, 'ng-binding'); + element.data('$binding', (element.data('$binding') || []).concat(binding.expressions || [binding])); + } : noop; + + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo(element, scope, isolated, noTemplate) { + safeAddClass(jqLite(element), isolated ? 'ng-isolate-scope' : 'ng-scope'); + var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; + element.data ? element.data(dataName, scope) : jqLite.data(element, dataName, scope); } : noop; return compile; @@ -924,9 +938,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - var namespace = null; + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ + var namespace = null; assertArg(scope, 'scope'); if (!namespace) { namespace = detectNamespaceForChildElements(futureParentElement); @@ -949,7 +963,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - $linkNode.data('$scope', scope); + compile.$$addScopeInfo($linkNode, scope); if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); @@ -967,15 +981,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. - } - } - /** * Compile function matches each node in nodeList against the directives. Once all directives * for a particular node are collected their compile functions are executed. The compile @@ -1008,10 +1013,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { null, [], [], previousCompileContext) : null; - if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(attrs.$$element, 'ng-scope'); - } - childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !(childNodes = nodeList[i].childNodes) || !childNodes.length) @@ -1062,7 +1063,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - jqLite.data(node, '$scope', childScope); + compile.$$addScopeInfo(node, childScope); } else { childScope = scope; } @@ -1563,14 +1564,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { isolateScope = scope.$new(true); - if (templateDirective && (templateDirective === newIsolateScopeDirective || - templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $element.data('$isolateScope', isolateScope); - } else { - $element.data('$isolateScopeNoTemplate', isolateScope); - } - - + compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective))); safeAddClass($element, 'ng-isolate-scope'); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index d7b438837ca9..41808b2351a5 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4250,12 +4250,17 @@ describe('$compile', function() { - it('should not leak if two "element" transclusions are on the same element', function () { + it('should not leak if two "element" transclusions are on the same element (with debug info)', function () { if (jQuery) { // jQuery 2.x doesn't expose the cache storage. return; } + + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }); + inject(function($compile, $rootScope) { expect(jqLiteCacheSize()).toEqual(0); @@ -4277,12 +4282,48 @@ describe('$compile', function() { }); - it('should not leak if two "element" transclusions are on the same element', function () { + it('should not leak if two "element" transclusions are on the same element (without debug info)', function () { if (jQuery) { // jQuery 2.x doesn't expose the cache storage. return; } + + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(false); + }); + + inject(function($compile, $rootScope) { + expect(jqLiteCacheSize()).toEqual(0); + + element = $compile('
{{x}}
')($rootScope); + expect(jqLiteCacheSize()).toEqual(0); + + $rootScope.$apply('xs = [0,1]'); + expect(jqLiteCacheSize()).toEqual(0); + + $rootScope.$apply('xs = [0]'); + expect(jqLiteCacheSize()).toEqual(0); + + $rootScope.$apply('xs = []'); + expect(jqLiteCacheSize()).toEqual(0); + + element.remove(); + expect(jqLiteCacheSize()).toEqual(0); + }); + }); + + + it('should not leak if two "element" transclusions are on the same element (with debug info)', function () { + if (jQuery) { + // jQuery 2.x doesn't expose the cache storage. + return; + } + + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }); + inject(function($compile, $rootScope) { expect(jqLiteCacheSize()).toEqual(0); element = $compile('
{{x}}
')($rootScope); @@ -5220,7 +5261,9 @@ describe('$compile', function() { })); }); inject(function($compile) { - element = $compile('
')($rootScope); + // We need to wrap the transclude directive's element in a parent element so that the + // cloned element gets deallocated/cleaned up correctly + element = $compile('
')($rootScope); expect(capturedTranscludeCtrl).toBeTruthy(); }); }); From 8b5dbc6d3e98e954a9ef8f0d58d412274904d82a Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 17:55:04 -0700 Subject: [PATCH 03/15] test(input): dealoc elements --- test/ng/directive/inputSpec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 98bc3537e754..ec09d76e07d8 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -799,6 +799,8 @@ describe('ngModel', function() { expect(element).toBeInvalid(); expect(element).toHaveClass('ng-invalid-required'); + + dealoc(element); })); From 108d8492dedc097c7e80181808f6801a938848c5 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 17:55:46 -0700 Subject: [PATCH 04/15] test(ngClass): dealoc elements --- test/ng/directive/ngClassSpec.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index a00708f18d5d..e6d1c134f8bd 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -3,6 +3,9 @@ describe('ngClass', function() { var element; + beforeEach(module(function($compileProvider) { + $compileProvider.debugInfoEnabled(false); + })); afterEach(function() { dealoc(element); @@ -39,9 +42,9 @@ describe('ngClass', function() { })); - it('should support adding multiple classes conditionally via a map of class names to boolean' + + it('should support adding multiple classes conditionally via a map of class names to boolean ' + 'expressions', inject(function($rootScope, $compile) { - var element = $compile( + element = $compile( '
' + '
')($rootScope); @@ -63,7 +66,7 @@ describe('ngClass', function() { it('should remove classes when the referenced object is the same but its property is changed', inject(function($rootScope, $compile) { - var element = $compile('
')($rootScope); + element = $compile('
')($rootScope); $rootScope.classes = { A: true, B: true }; $rootScope.$digest(); expect(element.hasClass('A')).toBeTruthy(); @@ -124,7 +127,7 @@ describe('ngClass', function() { $rootScope.$digest(); $rootScope.dynCls = 'foo'; $rootScope.$digest(); - expect(element[0].className).toBe('ui-panel ui-selected ng-scope foo'); + expect(element[0].className).toBe('ui-panel ui-selected foo'); })); @@ -132,7 +135,7 @@ describe('ngClass', function() { element = $compile('
')($rootScope); $rootScope.dynCls = 'panel'; $rootScope.$digest(); - expect(element[0].className).toBe('panel bar ng-scope'); + expect(element[0].className).toBe('panel bar'); })); @@ -142,7 +145,7 @@ describe('ngClass', function() { $rootScope.$digest(); $rootScope.dynCls = 'window'; $rootScope.$digest(); - expect(element[0].className).toBe('bar ng-scope window'); + expect(element[0].className).toBe('bar window'); })); @@ -153,7 +156,7 @@ describe('ngClass', function() { element.addClass('foo'); $rootScope.dynCls = ''; $rootScope.$digest(); - expect(element[0].className).toBe('ng-scope'); + expect(element[0].className).toBe(''); })); @@ -161,7 +164,7 @@ describe('ngClass', function() { element = $compile('
')($rootScope); $rootScope.dynCls = [undefined, null]; $rootScope.$digest(); - expect(element[0].className).toBe('ng-scope'); + expect(element[0].className).toBe(''); })); @@ -364,10 +367,14 @@ describe('ngClass', function() { describe('ngClass animations', function() { var body, element, $rootElement; + afterEach(function() { + dealoc(element); + }); + it("should avoid calling addClass accidentally when removeClass is going on", function() { module('ngAnimateMock'); inject(function($compile, $rootScope, $animate, $timeout) { - var element = angular.element('
'); + element = angular.element('
'); var body = jqLite(document.body); body.append(element); $compile(element)($rootScope); @@ -435,7 +442,7 @@ describe('ngClass animations', function() { digestQueue.shift()(); $rootScope.val = 'crazy'; - var element = angular.element('
'); + element = angular.element('
'); jqLite($document[0].body).append($rootElement); $compile(element)($rootScope); @@ -482,7 +489,7 @@ describe('ngClass animations', function() { $rootScope.two = true; $rootScope.three = true; - var element = angular.element('
'); + element = angular.element('
'); $compile(element)($rootScope); $rootScope.$digest(); From cd2fe7ed71ea7c686af044a80fdc528a7c6c05b4 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 23 Aug 2014 07:37:34 +0100 Subject: [PATCH 05/15] feat: add angular.reloadWithDebugInfo() --- src/.jshintrc | 1 + src/Angular.js | 40 ++++++++++++++++++++++++++++++++++++++++ src/AngularPublic.js | 3 ++- test/AngularSpec.js | 20 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/.jshintrc b/src/.jshintrc index 87dd0638cf94..6b7190b12ff8 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -88,6 +88,7 @@ "getBlockNodes": false, "createMap": false, "VALIDITY_STATE_PROPERTY": false, + "reloadWithDebugInfo": false, "skipDestroyOnNextJQueryCleanData": true, diff --git a/src/Angular.js b/src/Angular.js index 189e2e45378b..000e49412543 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1402,6 +1402,14 @@ function bootstrap(element, modules, config) { modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); + + if (config.debugInfoEnabled) { + // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. + modules.push(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }]); + } + modules.unshift('ng'); var injector = createInjector(modules, config.strictDi); injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', @@ -1415,8 +1423,14 @@ function bootstrap(element, modules, config) { return injector; }; + var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { + config.debugInfoEnabled = true; + window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); + } + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } @@ -1430,6 +1444,32 @@ function bootstrap(element, modules, config) { }; } +/** + * @ngdoc function + * @name angular.reloadWithDebugInfo + * @module ng + * @description + * Use this function to reload the current application with debug information turned on. + * + * To improve performance adding various debugging information can be disabled. + * See {@link $compileProvider#debugInfoEnabled}. + * + * This overrides any setting of `$compileProvider.debugInfoEnabled()` that you defined in your + * modules. If you wish to debug an application via this information then you should open up a debug + * console in the browser then call this method directly in this console: + * + * ```js + * angular.reloadWithDebugInfo(); + * ``` + * + * The page should reload and the debug information should now be available. + * + */ +function reloadWithDebugInfo(doReload) { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + if ( doReload !== false ) window.location.reload(); +} + var SNAKE_CASE_REGEXP = /[A-Z]/g; function snake_case(name, separator) { separator = separator || '_'; diff --git a/src/AngularPublic.js b/src/AngularPublic.js index e859a0da7577..a0a61b727b39 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -136,7 +136,8 @@ function publishExternalAPI(angular){ 'uppercase': uppercase, 'callbacks': {counter: 0}, '$$minErr': minErr, - '$$csp': csp + '$$csp': csp, + 'reloadWithDebugInfo': reloadWithDebugInfo }); angularModule = setupModuleLoader(window); diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 42b2bbc48d8b..b03590dbda5e 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1099,6 +1099,26 @@ describe('angular', function() { }); + describe("reloadWithDebugInfo", function() { + + it("should reload the current page with debugInfo turned on", function() { + + element = jqLite('
{{1+2}}
'); + angular.bootstrap(element); + expect(element.hasClass('ng-scope')).toBe(false); + dealoc(element); + + // We pass the false to prevent the page actually reloading + angular.reloadWithDebugInfo(false); + + element = jqLite('
{{1+2}}
'); + angular.bootstrap(element); + expect(element.hasClass('ng-scope')).toBe(true); + dealoc(element); + }); + }); + + describe('startingElementHtml', function(){ it('should show starting element tag only', function(){ expect(startingTag('
text
')). From 56ebc98699ff539588c57d2f0e026ef22abecd20 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 23 Aug 2014 20:45:44 +0100 Subject: [PATCH 06/15] chore(clean-shrinkwrap): chokidar is fixed since 0.8.2 --- scripts/clean-shrinkwrap.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/clean-shrinkwrap.js b/scripts/clean-shrinkwrap.js index 66326f4f2fca..c46fe295da6b 100755 --- a/scripts/clean-shrinkwrap.js +++ b/scripts/clean-shrinkwrap.js @@ -23,9 +23,6 @@ function cleanModule(module, name) { if (name === 'chokidar') { if (module.version === '0.8.1') { delete module.dependencies; - } else if ( module.version !== '0.8.2') { - throw new Error("Unfamiliar chokidar version (v" + module.version + - ") , please check status of https://github.com/paulmillr/chokidar/pull/106"); } } From 8956d3400f163047309f1e8f7234a16b36d9536b Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 14:39:22 -0700 Subject: [PATCH 07/15] chore(deps): update protractor to 1.1.1 --- npm-shrinkwrap.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d057b17689ef..cc3cf7043cd4 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -3230,7 +3230,7 @@ } }, "protractor": { - "version": "1.0.0", + "version": "1.1.1", "dependencies": { "request": { "version": "2.36.0", @@ -3254,7 +3254,7 @@ "version": "0.12.1", "dependencies": { "punycode": { - "version": "1.3.0" + "version": "1.3.1" } } }, @@ -3361,6 +3361,9 @@ } } }, + "q": { + "version": "1.0.0" + }, "lodash": { "version": "2.4.1" }, From c43bbdf6ea7dad051f489a76cfa3d694be9bc297 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 25 Aug 2014 08:28:51 +0100 Subject: [PATCH 08/15] test(e2e): fix by.binding() locators After upgrading, Protractor requires exact string that is used in the binding. --- docs/content/guide/$location.ngdoc | 72 +++++++++++++++--------------- docs/content/guide/module.ngdoc | 4 +- src/ng/directive/input.js | 8 ++-- src/ng/directive/select.js | 8 ++-- src/ng/filter/filters.js | 2 +- src/ng/filter/limitTo.js | 4 +- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/docs/content/guide/$location.ngdoc b/docs/content/guide/$location.ngdoc index 81a7d85ad299..f7a1557a90f8 100644 --- a/docs/content/guide/$location.ngdoc +++ b/docs/content/guide/$location.ngdoc @@ -469,12 +469,12 @@ In these examples we use `` it("should show fake browser info on load", function(){ expect(addressBar.getAttribute('value')).toBe(url); - expect(element(by.binding('$location.protocol')).getText()).toBe('http'); - expect(element(by.binding('$location.host')).getText()).toBe('www.example.com'); - expect(element(by.binding('$location.port')).getText()).toBe('80'); - expect(element(by.binding('$location.path')).getText()).toBe('/path'); - expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}'); - expect(element(by.binding('$location.hash')).getText()).toBe('h'); + expect(element(by.binding('$location.protocol()')).getText()).toBe('http'); + expect(element(by.binding('$location.host()')).getText()).toBe('www.example.com'); + expect(element(by.binding('$location.port()')).getText()).toBe('80'); + expect(element(by.binding('$location.path()')).getText()).toBe('/path'); + expect(element(by.binding('$location.search()')).getText()).toBe('{"a":"b"}'); + expect(element(by.binding('$location.hash()')).getText()).toBe('h'); }); @@ -485,24 +485,24 @@ In these examples we use `` expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/first?a=b"); - expect(element(by.binding('$location.protocol')).getText()).toBe('http'); - expect(element(by.binding('$location.host')).getText()).toBe('www.example.com'); - expect(element(by.binding('$location.port')).getText()).toBe('80'); - expect(element(by.binding('$location.path')).getText()).toBe('/first'); - expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}'); - expect(element(by.binding('$location.hash')).getText()).toBe(''); + expect(element(by.binding('$location.protocol()')).getText()).toBe('http'); + expect(element(by.binding('$location.host()')).getText()).toBe('www.example.com'); + expect(element(by.binding('$location.port()')).getText()).toBe('80'); + expect(element(by.binding('$location.path()')).getText()).toBe('/first'); + expect(element(by.binding('$location.search()')).getText()).toBe('{"a":"b"}'); + expect(element(by.binding('$location.hash()')).getText()).toBe(''); navigation.get(1).click(); expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/sec/ond?flag#hash"); - expect(element(by.binding('$location.protocol')).getText()).toBe('http'); - expect(element(by.binding('$location.host')).getText()).toBe('www.example.com'); - expect(element(by.binding('$location.port')).getText()).toBe('80'); - expect(element(by.binding('$location.path')).getText()).toBe('/sec/ond'); - expect(element(by.binding('$location.search')).getText()).toBe('{"flag":true}'); - expect(element(by.binding('$location.hash')).getText()).toBe('hash'); + expect(element(by.binding('$location.protocol()')).getText()).toBe('http'); + expect(element(by.binding('$location.host()')).getText()).toBe('www.example.com'); + expect(element(by.binding('$location.port()')).getText()).toBe('80'); + expect(element(by.binding('$location.path()')).getText()).toBe('/sec/ond'); + expect(element(by.binding('$location.search()')).getText()).toBe('{"flag":true}'); + expect(element(by.binding('$location.hash()')).getText()).toBe('hash'); }); @@ -621,12 +621,12 @@ In these examples we use `` it("should show fake browser info on load", function(){ expect(addressBar.getAttribute('value')).toBe(url); - expect(element(by.binding('$location.protocol')).getText()).toBe('http'); - expect(element(by.binding('$location.host')).getText()).toBe('www.example.com'); - expect(element(by.binding('$location.port')).getText()).toBe('80'); - expect(element(by.binding('$location.path')).getText()).toBe('/path'); - expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}'); - expect(element(by.binding('$location.hash')).getText()).toBe('h'); + expect(element(by.binding('$location.protocol()')).getText()).toBe('http'); + expect(element(by.binding('$location.host()')).getText()).toBe('www.example.com'); + expect(element(by.binding('$location.port()')).getText()).toBe('80'); + expect(element(by.binding('$location.path()')).getText()).toBe('/path'); + expect(element(by.binding('$location.search()')).getText()).toBe('{"a":"b"}'); + expect(element(by.binding('$location.hash()')).getText()).toBe('h'); }); @@ -637,24 +637,24 @@ In these examples we use `` expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/index.html#!/first?a=b"); - expect(element(by.binding('$location.protocol')).getText()).toBe('http'); - expect(element(by.binding('$location.host')).getText()).toBe('www.example.com'); - expect(element(by.binding('$location.port')).getText()).toBe('80'); - expect(element(by.binding('$location.path')).getText()).toBe('/first'); - expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}'); - expect(element(by.binding('$location.hash')).getText()).toBe(''); + expect(element(by.binding('$location.protocol()')).getText()).toBe('http'); + expect(element(by.binding('$location.host()')).getText()).toBe('www.example.com'); + expect(element(by.binding('$location.port()')).getText()).toBe('80'); + expect(element(by.binding('$location.path()')).getText()).toBe('/first'); + expect(element(by.binding('$location.search()')).getText()).toBe('{"a":"b"}'); + expect(element(by.binding('$location.hash()')).getText()).toBe(''); navigation.get(1).click(); expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/index.html#!/sec/ond?flag#hash"); - expect(element(by.binding('$location.protocol')).getText()).toBe('http'); - expect(element(by.binding('$location.host')).getText()).toBe('www.example.com'); - expect(element(by.binding('$location.port')).getText()).toBe('80'); - expect(element(by.binding('$location.path')).getText()).toBe('/sec/ond'); - expect(element(by.binding('$location.search')).getText()).toBe('{"flag":true}'); - expect(element(by.binding('$location.hash')).getText()).toBe('hash'); + expect(element(by.binding('$location.protocol()')).getText()).toBe('http'); + expect(element(by.binding('$location.host()')).getText()).toBe('www.example.com'); + expect(element(by.binding('$location.port()')).getText()).toBe('80'); + expect(element(by.binding('$location.path()')).getText()).toBe('/sec/ond'); + expect(element(by.binding('$location.search()')).getText()).toBe('{"flag":true}'); + expect(element(by.binding('$location.hash()')).getText()).toBe('hash'); }); diff --git a/docs/content/guide/module.ngdoc b/docs/content/guide/module.ngdoc index 5315da44ba91..1ef968347a05 100644 --- a/docs/content/guide/module.ngdoc +++ b/docs/content/guide/module.ngdoc @@ -50,7 +50,7 @@ I'm in a hurry. How do I get a Hello World module working? it('should add Hello to the name', function() { - expect(element(by.binding("{{ 'World' | greet }}")).getText()).toEqual('Hello, World!'); + expect(element(by.binding(" 'World' | greet ")).getText()).toEqual('Hello, World!'); }); @@ -128,7 +128,7 @@ The above is a suggestion. Tailor it to your needs. it('should add Hello to the name', function() { - expect(element(by.binding("{{ greeting }}")).getText()).toEqual('Bonjour World!'); + expect(element(by.binding(" greeting ")).getText()).toEqual('Bonjour World!'); }); diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 911ec30f0cd7..068079fb1e51 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -814,7 +814,7 @@ var inputType = { it('should change state', function() { - var color = element(by.binding('color')); + var color = element(by.binding('color | json')); expect(color.getText()).toContain('blue'); @@ -1313,7 +1313,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt - var user = element(by.binding('{{user}}')); + var user = element(by.binding('user')); var userNameValid = element(by.binding('myForm.userName.$valid')); var lastNameValid = element(by.binding('myForm.lastName.$valid')); var lastNameError = element(by.binding('myForm.lastName.$error')); @@ -2542,7 +2542,7 @@ var minlengthDirective = function() { * * * var listInput = element(by.model('names')); - * var names = element(by.binding('{{names}}')); + * var names = element(by.binding('names')); * var valid = element(by.binding('myForm.namesInput.$valid')); * var error = element(by.css('span.error')); * @@ -2572,7 +2572,7 @@ var minlengthDirective = function() { * * it("should split the text by newlines", function() { * var listInput = element(by.model('list')); - * var output = element(by.binding('{{ list | json }}')); + * var output = element(by.binding(' list | json ')); * listInput.sendKeys('abc\ndef\nghi'); * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); * }); diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 8bb93c56ceec..285791e64727 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -115,7 +115,7 @@ var ngOptionsMinErr = minErr('ngOptions'); Select bogus.

- Currently selected: {{ {selected_color:myColor} }} + Currently selected: {{ {selected_color:myColor} }}
@@ -123,13 +123,13 @@ var ngOptionsMinErr = minErr('ngOptions');
it('should check ng-options', function() { - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); + expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('red'); element.all(by.model('myColor')).first().click(); element.all(by.css('select[ng-model="myColor"] option')).first().click(); - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); + expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('black'); element(by.css('.nullable select[ng-model="myColor"]')).click(); element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); + expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('null'); }); diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js index 65ea0e1ab0f4..0b69d7b4ba07 100644 --- a/src/ng/filter/filters.js +++ b/src/ng/filter/filters.js @@ -490,7 +490,7 @@ function dateFilter($locale) {
it('should jsonify filtered objects', function() { - expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.binding(" {'name':'value'} | json ")).getText()).toMatch(/\{\n "name": ?"value"\n}/); }); diff --git a/src/ng/filter/limitTo.js b/src/ng/filter/limitTo.js index 52abd260b9a2..6f01aa9c1f86 100644 --- a/src/ng/filter/limitTo.js +++ b/src/ng/filter/limitTo.js @@ -40,8 +40,8 @@ var numLimitInput = element(by.model('numLimit')); var letterLimitInput = element(by.model('letterLimit')); - var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); - var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + var limitedNumbers = element(by.binding(' numbers | limitTo:numLimit ')); + var limitedLetters = element(by.binding(' letters | limitTo:letterLimit ')); it('should limit the number array to first three items', function() { expect(numLimitInput.getAttribute('value')).toBe('3'); From ba2c5ed0c05dad900e4e16875c059c03862d776c Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 10:59:39 -0700 Subject: [PATCH 09/15] refactor($compile): $$addBindingInfo accepts single expression or an array Instead of knowing about `.expressions` property, it just accepts a single expression or an array of expressions. --- src/ng/compile.js | 12 ++++++++++-- src/ng/directive/ngBind.js | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 391d19fd1fb9..59c400b6e954 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -907,8 +907,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { NG_ATTR_BINDING = /^ngAttr[A-Z]/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo(element, binding) { + var bindings = element.data('$binding') || []; + + if (isArray(binding)) { + bindings = bindings.concat(binding); + } else { + bindings.push(binding); + } + safeAddClass(element, 'ng-binding'); - element.data('$binding', (element.data('$binding') || []).concat(binding.expressions || [binding])); + element.data('$binding', bindings); } : noop; compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo(element, scope, isolated, noTemplate) { @@ -1982,7 +1990,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile: function textInterpolateCompileFn(templateNode) { return function textInterpolateLinkFn(scope, node) { var parent = node.parent(); - compile.$$addBindingInfo(parent, interpolateFn); + compile.$$addBindingInfo(parent, interpolateFn.expressions); scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { node[0].nodeValue = value; }); diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index 6be2dfd05fd2..7d6f8d30a11f 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -123,7 +123,7 @@ var ngBindDirective = ['$compile', function($compile) { var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { return function(scope, element, attr) { var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - $compile.$$addBindingInfo(element, interpolateFn); + $compile.$$addBindingInfo(element, interpolateFn.expressions); attr.$observe('ngBindTemplate', function(value) { element.text(value); }); From c2418a18620486866ccb03fa14c29d49eb1dbc75 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 12:15:50 -0700 Subject: [PATCH 10/15] refactor: remove doReload arg used only for testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We run unit tests in “strict” mode and thus can’t monkey-patch `window.location` nor `window.location.reload`. In order to avoid full page reload, we could pass location as argument, or another level of indirection, something like this: ```js var ourGlobalFunkyLocation = window.location; function reloadWithDebugInfo() { window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; ourGlobalFunkyLocation.reload(); } // in the test ourGlobalFunkyLocation = { reload: function() {} }; reloadWithDebugInfo(); ourGlobalFunkyLocation = window.location; ``` I don’t think any of these make sense, just so that we can test setting `window.name`. If the `reloadWithDebugInfo` function was more complicated, I would do it. I don’t think it’s worthy to confuse production code with extra logic which purpose was only to make testing possible. --- src/Angular.js | 4 ++-- test/AngularSpec.js | 20 -------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 000e49412543..635314e77775 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1465,9 +1465,9 @@ function bootstrap(element, modules, config) { * The page should reload and the debug information should now be available. * */ -function reloadWithDebugInfo(doReload) { +function reloadWithDebugInfo() { window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; - if ( doReload !== false ) window.location.reload(); + window.location.reload(); } var SNAKE_CASE_REGEXP = /[A-Z]/g; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index b03590dbda5e..42b2bbc48d8b 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1099,26 +1099,6 @@ describe('angular', function() { }); - describe("reloadWithDebugInfo", function() { - - it("should reload the current page with debugInfo turned on", function() { - - element = jqLite('
{{1+2}}
'); - angular.bootstrap(element); - expect(element.hasClass('ng-scope')).toBe(false); - dealoc(element); - - // We pass the false to prevent the page actually reloading - angular.reloadWithDebugInfo(false); - - element = jqLite('
{{1+2}}
'); - angular.bootstrap(element); - expect(element.hasClass('ng-scope')).toBe(true); - dealoc(element); - }); - }); - - describe('startingElementHtml', function(){ it('should show starting element tag only', function(){ expect(startingTag('
text
')). From 48a5ae3f54c965b9914e2020ebd5c3ebfd216773 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 17:21:05 -0700 Subject: [PATCH 11/15] refactor($compile): $$addScopeInfo always expects jq wrapper `$$addScopeInfo` used to accept either DOM Node or jqLite/jQuery wrapper. This commit simplifies the method to always require jqLite/jQuery wrapper and thus remove the `element.data` condition which was wrong. If `element` was a raw comment element, the `data` property was a string (the value of the comment) and an exception was thrown. --- src/ng/compile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 59c400b6e954..0e6e80cc2954 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -920,9 +920,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } : noop; compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo(element, scope, isolated, noTemplate) { - safeAddClass(jqLite(element), isolated ? 'ng-isolate-scope' : 'ng-scope'); + safeAddClass(element, isolated ? 'ng-isolate-scope' : 'ng-scope'); var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; - element.data ? element.data(dataName, scope) : jqLite.data(element, dataName, scope); + element.data(dataName, scope); } : noop; return compile; @@ -1071,7 +1071,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - compile.$$addScopeInfo(node, childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } From 699f33a3bb7ef71733dd9d1aee2a33b3a1d4f7e4 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 26 Aug 2014 17:22:26 -0700 Subject: [PATCH 12/15] refactor($compile): rename element -> $element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To follow our convention (at least in this file): if it’s a jqLite/jQuery wrapper than the variable name starts with `$`. --- src/ng/compile.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 0e6e80cc2954..f7e817481a0a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -906,8 +906,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; - compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo(element, binding) { - var bindings = element.data('$binding') || []; + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { + var bindings = $element.data('$binding') || []; if (isArray(binding)) { bindings = bindings.concat(binding); @@ -915,14 +915,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { bindings.push(binding); } - safeAddClass(element, 'ng-binding'); - element.data('$binding', bindings); + safeAddClass($element, 'ng-binding'); + $element.data('$binding', bindings); } : noop; - compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo(element, scope, isolated, noTemplate) { - safeAddClass(element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; - element.data(dataName, scope); + $element.data(dataName, scope); } : noop; return compile; From 0ae0602a009070ce92dbfd3c19b13d7e923a5c33 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Wed, 27 Aug 2014 14:06:27 -0700 Subject: [PATCH 13/15] perf($compile): add debug classes in compile phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a93f03d and d37f103 we changed the compiler and ngBind to add debugging CSS classes (i.e. ng-scope, ng-binding) in linking function. This simplified the code and made sense under the original assumptions that the debug info will be disabled by default. That is however not the case - debug info is enabled by default. When debug info is enabled, this change improves the largetable-bp benchmark by ~580ms, that is 30% faster. Measuring the “create” phase, 25 loops, meantime ~1920ms -> ~1340ms. This change does not affect performance when debug info is disabled. --- src/ng/compile.js | 27 +++++++++++++++++++++++---- src/ng/directive/ngBind.js | 21 ++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index f7e817481a0a..5b6daa1f5848 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -915,16 +915,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { bindings.push(binding); } - safeAddClass($element, 'ng-binding'); $element.data('$binding', bindings); } : noop; + compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { + safeAddClass($element, 'ng-binding'); + } : noop; + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { - safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; $element.data(dataName, scope); } : noop; + compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + } : noop; + return compile; //================================ @@ -947,6 +953,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); + compile.$$addScopeClass($compileNodes); + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ var namespace = null; assertArg(scope, 'scope'); @@ -1021,6 +1029,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { null, [], [], previousCompileContext) : null; + if (nodeLinkFn && nodeLinkFn.scope) { + compile.$$addScopeClass(attrs.$$element); + } + childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !(childNodes = nodeList[i].childNodes) || !childNodes.length) @@ -1574,8 +1586,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || templateDirective === newIsolateScopeDirective.$$originalDirective))); - - safeAddClass($element, 'ng-isolate-scope'); + compile.$$addScopeClass($element, true); forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { var match = definition.match(LOCAL_REGEXP) || [], @@ -1988,8 +1999,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directives.push({ priority: 0, compile: function textInterpolateCompileFn(templateNode) { + var templateNodeParent = templateNode.parent(), + hasCompileParent = !!templateNodeParent.length; + + // When transcluding a template that has bindings in the root + // we don't have a parent and thus need to add the class during linking fn. + if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); + return function textInterpolateLinkFn(scope, node) { var parent = node.parent(); + if (!hasCompileParent) compile.$$addBindingClass(parent); compile.$$addBindingInfo(parent, interpolateFn.expressions); scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { node[0].nodeValue = value; diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index 7d6f8d30a11f..e8eadf323199 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -55,6 +55,7 @@ var ngBindDirective = ['$compile', function($compile) { return { restrict: 'AC', compile: function(templateElement) { + $compile.$$addBindingClass(templateElement); return function (scope, element, attr) { $compile.$$addBindingInfo(element, attr.ngBind); scope.$watch(attr.ngBind, function ngBindWatchAction(value) { @@ -121,13 +122,18 @@ var ngBindDirective = ['$compile', function($compile) { */ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { - return function(scope, element, attr) { - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - $compile.$$addBindingInfo(element, interpolateFn.expressions); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); - }; + return { + compile: function(templateElement) { + $compile.$$addBindingClass(templateElement); + return function(scope, element, attr) { + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + $compile.$$addBindingInfo(element, interpolateFn.expressions); + attr.$observe('ngBindTemplate', function(value) { + element.text(value); + }); + }; + } + } }]; @@ -180,6 +186,7 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, return { restrict: 'A', compile: function (tElement, tAttrs) { + $compile.$$addBindingClass(tElement); return function (scope, element, attr) { $compile.$$addBindingInfo(element, attr.ngBindHtml); From 7efe998a84740ced2183c0b9e702c5cb5bfd7b63 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Wed, 27 Aug 2014 14:37:54 -0700 Subject: [PATCH 14/15] refactor(ngBind): name link and compile functions For easier debugging. --- src/ng/directive/ngBind.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index e8eadf323199..e1891a1a960a 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -54,9 +54,9 @@ var ngBindDirective = ['$compile', function($compile) { return { restrict: 'AC', - compile: function(templateElement) { + compile: function ngBindCompile(templateElement) { $compile.$$addBindingClass(templateElement); - return function (scope, element, attr) { + return function ngBindLink(scope, element, attr) { $compile.$$addBindingInfo(element, attr.ngBind); scope.$watch(attr.ngBind, function ngBindWatchAction(value) { // We are purposefully using == here rather than === because we want to @@ -123,9 +123,9 @@ var ngBindDirective = ['$compile', function($compile) { */ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { return { - compile: function(templateElement) { + compile: function ngBindTemplateCompile(templateElement) { $compile.$$addBindingClass(templateElement); - return function(scope, element, attr) { + return function ngBindTemplateLink(scope, element, attr) { var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); $compile.$$addBindingInfo(element, interpolateFn.expressions); attr.$observe('ngBindTemplate', function(value) { @@ -133,7 +133,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate }); }; } - } + }; }]; @@ -185,10 +185,10 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { return { restrict: 'A', - compile: function (tElement, tAttrs) { + compile: function ngBindHtmlCompile(tElement, tAttrs) { $compile.$$addBindingClass(tElement); - return function (scope, element, attr) { + return function ngBindHtmlLink(scope, element, attr) { $compile.$$addBindingInfo(element, attr.ngBindHtml); var ngBindHtmlGetter = $parse(attr.ngBindHtml); var ngBindHtmlWatch = $parse(attr.ngBindHtml, function getStringValue(value) { From 3bd27ef8c8b0fb81e67fd8d5e70b17a76925515b Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Wed, 27 Aug 2014 13:16:39 -0700 Subject: [PATCH 15/15] docs(debugInfo): add docs for $compileProvider.debugInfoEnabled() --- docs/content/guide/index.ngdoc | 1 + docs/content/guide/production.ngdoc | 42 +++++++++++++++++++++++++++++ src/Angular.js | 15 ++--------- src/ng/compile.js | 3 +++ 4 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 docs/content/guide/production.ngdoc diff --git a/docs/content/guide/index.ngdoc b/docs/content/guide/index.ngdoc index 0ca0abdcb54d..93bc7b42d062 100644 --- a/docs/content/guide/index.ngdoc +++ b/docs/content/guide/index.ngdoc @@ -81,6 +81,7 @@ This is a short list of libraries with specific support and documentation for wo ### General +* **Docs Page:** {@link guide/production Running an AngularJS App in Production} * **Javascript minification: **[Background](http://thegreenpizza.github.io/2013/05/25/building-minification-safe-angular.js-applications/), [ng-annotate automation tool](https://github.com/olov/ng-annotate) * **Analytics and Logging:** [Angularyitcs (Google Analytics)](http://ngmodules.org/modules/angularytics), [Angulartics (Analytics)](https://github.com/luisfarzati/angulartics), [Logging Client-Side Errors](http://www.bennadel.com/blog/2542-Logging-Client-Side-Errors-With-AngularJS-And-Stacktrace-js.htm) * **SEO:** [By hand](http://www.yearofmoo.com/2012/11/angularjs-and-seo.html), [prerender.io](http://prerender.io/), [Brombone](http://www.brombone.com/), [SEO.js](http://getseojs.com/), [SEO4Ajax](http://www.seo4ajax.com/) diff --git a/docs/content/guide/production.ngdoc b/docs/content/guide/production.ngdoc new file mode 100644 index 000000000000..996e0486f5fc --- /dev/null +++ b/docs/content/guide/production.ngdoc @@ -0,0 +1,42 @@ +@ngdoc overview +@name Running in Production +@description + +# Running an AngularJS App in Production + +There are a few things you might consider when running your AngularJS application in production. + + +## Disabling Debug Data + +By default AngularJS attaches information about scopes to DOM nodes, and adds CSS classes +to data-bound elements. The information that is not included is: + +As a result of `ngBind`, `ngBindHtml` or `{{...}}` interpolations, binding data and CSS class +`ng-class` is attached to the corresponding element. + +Where the compiler has created a new scope, the scope and either `ng-scope` or `ng-isolated-scope` +CSS class are attached to the corresponding element. These scope references can then be accessed via +`element.scope()` and `element.isolateScope()`. + +Tools like [Protractor](github.com/angular/protractor) and +[Batarang](https://github.com/angular/angularjs-batarang) need this information to run, +but you can disable this in production for a significant performance boost with: + +```js +myApp.config(['$compileProvider', function ($compileProvider) { + $compileProvider.debugInfoEnabled(false); +}]); +``` + +If you wish to debug an application with this information then you should open up a debug +console in the browser then call this method directly in this console: + +```js +angular.reloadWithDebugInfo(); +``` + +The page should reload and the debug information should now be available. + +For more see the docs pages on {@link ng.$compileProvider#debugInfoEnabled `$compileProvider`} +and {@link ng/function/angular.reloadWithDebugInfo `angular.reloadWithDebugInfo`}. diff --git a/src/Angular.js b/src/Angular.js index 635314e77775..d3c80e1cc841 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1450,20 +1450,9 @@ function bootstrap(element, modules, config) { * @module ng * @description * Use this function to reload the current application with debug information turned on. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. * - * To improve performance adding various debugging information can be disabled. - * See {@link $compileProvider#debugInfoEnabled}. - * - * This overrides any setting of `$compileProvider.debugInfoEnabled()` that you defined in your - * modules. If you wish to debug an application via this information then you should open up a debug - * console in the browser then call this method directly in this console: - * - * ```js - * angular.reloadWithDebugInfo(); - * ``` - * - * The page should reload and the debug information should now be available. - * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. */ function reloadWithDebugInfo() { window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; diff --git a/src/ng/compile.js b/src/ng/compile.js index 5b6daa1f5848..140359a89f21 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -685,6 +685,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * `ng-binding` CSS class * * `$binding` data property containing an array of the binding expressions * + * You may want to use this in production for a significant performance boost. See + * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. + * * The default value is true. */ var debugInfoEnabled = true;