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/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/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/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/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" }, 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"); } } 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..d3c80e1cc841 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,21 @@ 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. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. + * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. + */ +function reloadWithDebugInfo() { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + 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/src/ng/compile.js b/src/ng/compile.js index ac0eab395690..140359a89f21 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -668,6 +668,37 @@ 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 + * + * 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; + 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', @@ -858,6 +889,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 == '}}') @@ -867,6 +909,30 @@ 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); + } + + $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) { + 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; @@ -889,9 +955,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - var namespace = null; + + compile.$$addScopeClass($compileNodes); + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ + var namespace = null; assertArg(scope, 'scope'); if (!namespace) { namespace = detectNamespaceForChildElements(futureParentElement); @@ -914,7 +982,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); @@ -932,15 +1000,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 @@ -974,7 +1033,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(attrs.$$element, 'ng-scope'); + compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || @@ -1027,7 +1086,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - jqLite.data(node, '$scope', childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } @@ -1528,16 +1587,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { isolateScope = scope.$new(true); - if (templateDirective && (templateDirective === newIsolateScopeDirective || - templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $element.data('$isolateScope', isolateScope); - } else { - $element.data('$isolateScopeNoTemplate', isolateScope); - } - - - - safeAddClass($element, 'ng-isolate-scope'); + compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective))); + compile.$$addScopeClass($element, true); forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { var match = definition.match(LOCAL_REGEXP) || [], @@ -1950,17 +2002,17 @@ 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'); + 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(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - parent.data('$binding', bindings); - if (!hasCompileParent) safeAddClass(parent, 'ng-binding'); + 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/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/ngBind.js b/src/ng/directive/ngBind.js index d4dd81fc9121..e1891a1a960a 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -51,23 +51,23 @@ */ -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 ngBindCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + 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 + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); + }; + } + }; +}]; /** @@ -121,14 +121,18 @@ var ngBindDirective = ngDirective({ */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - 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); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); +var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { + return { + compile: function ngBindTemplateCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindTemplateLink(scope, element, attr) { + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + $compile.$$addBindingInfo(element, interpolateFn.expressions); + attr.$observe('ngBindTemplate', function(value) { + element.text(value); + }); + }; + } }; }]; @@ -178,14 +182,14 @@ 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 ngBindHtmlCompile(tElement, tAttrs) { + $compile.$$addBindingClass(tElement); - return function ngBindLink(scope, element, attr) { - element.data('$binding', attr.ngBindHtml); + return function ngBindHtmlLink(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/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'); 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); @@ -4230,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); @@ -4257,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); @@ -5200,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(); }); }); 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); })); 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); }); 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();