From 62494bae38126392075b5e707b24982aae0418cb Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 2 Feb 2017 12:54:51 +0100 Subject: [PATCH 022/719] chore(docs-app): update links in header menu They are now in the same order as angularjs.org Closes #14351 --- .../templates/app/indexPage.template.html | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/config/templates/app/indexPage.template.html b/docs/config/templates/app/indexPage.template.html index 5dc8fe9a4b5d..57eb4f64cd96 100644 --- a/docs/config/templates/app/indexPage.template.html +++ b/docs/config/templates/app/indexPage.template.html @@ -91,22 +91,22 @@

Learn @@ -114,12 +114,12 @@

Discuss From f779230f70d12cbc075971f14acf382a5d3fd1d0 Mon Sep 17 00:00:00 2001 From: Keith Walsh Date: Wed, 15 Feb 2017 06:59:02 -0500 Subject: [PATCH 023/719] docs($resource): add minor clarification Closes #15711 --- src/ngResource/resource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index 7505072c4fea..bb879b884903 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -174,7 +174,7 @@ function shallowClearAndCopy(src, dst) { * set `transformResponse` to an empty array: `transformResponse: []` * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for + * {@link ng.$cacheFactory $cacheFactory} is supplied, this cache will be used for * caching. * - **`timeout`** – `{number}` – timeout in milliseconds.
* **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are From db46d244917fe18e25bae2ea13062647fe25432c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 15 Feb 2017 20:26:24 +0100 Subject: [PATCH 024/719] chore(docs-app): add debounce to search input This fixes issues where the search results do not correctly reflect the search query. This happens in Firefox when you enter a search query very rapidly. There is probably an issue with the async behavior of the search / webworker, so this is just a workaround. --- docs/config/templates/app/indexPage.template.html | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config/templates/app/indexPage.template.html b/docs/config/templates/app/indexPage.template.html index 57eb4f64cd96..7aa72ab9b915 100644 --- a/docs/config/templates/app/indexPage.template.html +++ b/docs/config/templates/app/indexPage.template.html @@ -83,6 +83,7 @@

From 9a2efb7b66e3dc3a1ef43f4ca17a6e2632f51c3e Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 15 Jul 2016 12:33:01 +0300 Subject: [PATCH 025/719] fix($animate): improve detection on `ng-animate` in `classNameFilter` RegExp Fixes #14806 --- docs/content/error/$animate/nongcls.ngdoc | 8 +++++ src/ng/animate.js | 13 ++++---- test/ngAnimate/animateSpec.js | 38 +++++++++++------------ 3 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 docs/content/error/$animate/nongcls.ngdoc diff --git a/docs/content/error/$animate/nongcls.ngdoc b/docs/content/error/$animate/nongcls.ngdoc new file mode 100644 index 000000000000..b5774bb46627 --- /dev/null +++ b/docs/content/error/$animate/nongcls.ngdoc @@ -0,0 +1,8 @@ +@ngdoc error +@name $animate:nongcls +@fullName `ng-animate` class not allowed +@description + +This error occurs, when trying to set `$animateProvider.classNameFilter()` to a RegExp containing +the reserved `ng-animate` class. Since `.ng-animate` will be added/removed by `$animate` itself, +using it as part of the `classNameFilter` RegExp is not allowed. diff --git a/src/ng/animate.js b/src/ng/animate.js index 3a00b60d8ad6..3dffb1bb7e36 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -179,6 +179,7 @@ var $$CoreAnimateQueueProvider = /** @this */ function() { */ var $AnimateProvider = ['$provide', /** @this */ function($provide) { var provider = this; + var classNameFilter = null; this.$$registeredAnimations = Object.create(null); @@ -247,15 +248,15 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { */ this.classNameFilter = function(expression) { if (arguments.length === 1) { - this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; - if (this.$$classNameFilter) { - var reservedRegex = new RegExp('(\\s+|\\/)' + NG_ANIMATE_CLASSNAME + '(\\s+|\\/)'); - if (reservedRegex.test(this.$$classNameFilter.toString())) { - throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); + classNameFilter = (expression instanceof RegExp) ? expression : null; + if (classNameFilter) { + var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]'); + if (reservedRegex.test(classNameFilter.toString())) { + throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); } } } - return this.$$classNameFilter; + return classNameFilter; }; this.$get = ['$$animateQueue', function($$animateQueue) { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 9435d03841b7..c36215dbd5fa 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -255,29 +255,27 @@ describe('animations', function() { }); }); - it('should throw a minErr if a regex value is used which partially contains or fully matches the `ng-animate` CSS class', function() { + it('should throw a minErr if a regex value is used which partially contains or fully matches the `ng-animate` CSS class', module(function($animateProvider) { - assertError(/ng-animate/, true); - assertError(/first ng-animate last/, true); - assertError(/ng-animate-special/, false); - assertError(/first ng-animate-special last/, false); - assertError(/first ng-animate ng-animate-special last/, true); - - function assertError(regex, bool) { - var expectation = expect(function() { + expect(setFilter(/ng-animate/)).toThrowMinErr('$animate', 'nongcls'); + expect(setFilter(/first ng-animate last/)).toThrowMinErr('$animate', 'nongcls'); + expect(setFilter(/first ng-animate ng-animate-special last/)).toThrowMinErr('$animate', 'nongcls'); + expect(setFilter(/(ng-animate)/)).toThrowMinErr('$animate', 'nongcls'); + expect(setFilter(/(foo|ng-animate|bar)/)).toThrowMinErr('$animate', 'nongcls'); + expect(setFilter(/(foo|)ng-animate(|bar)/)).toThrowMinErr('$animate', 'nongcls'); + + expect(setFilter(/ng-animater/)).not.toThrow(); + expect(setFilter(/my-ng-animate/)).not.toThrow(); + expect(setFilter(/first ng-animater last/)).not.toThrow(); + expect(setFilter(/first my-ng-animate last/)).not.toThrow(); + + function setFilter(regex) { + return function() { $animateProvider.classNameFilter(regex); - }); - - var message = '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "ng-animate" CSS class.'; - - if (bool) { - expectation.toThrowMinErr('$animate', 'nongcls', message); - } else { - expectation.not.toThrowMinErr('$animate', 'nongcls', message); - } + }; } - }); - }); + }) + ); it('should complete the leave DOM operation in case the classNameFilter fails', function() { module(function($animateProvider) { From b55637a8f7b740add594e11cdde2b58943049ba3 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 15 Jul 2016 12:34:06 +0300 Subject: [PATCH 026/719] fix($animate): reset `classNameFilter` to `null` when a disallowed RegExp is used Closes #14913 --- src/ng/animate.js | 1 + test/ngAnimate/animateSpec.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/ng/animate.js b/src/ng/animate.js index 3dffb1bb7e36..be650faee7e3 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -252,6 +252,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { if (classNameFilter) { var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]'); if (reservedRegex.test(classNameFilter.toString())) { + classNameFilter = null; throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); } } diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index c36215dbd5fa..ae40e568b2a3 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -277,6 +277,20 @@ describe('animations', function() { }) ); + it('should clear the `classNameFilter` if a disallowed RegExp is passed', + module(function($animateProvider) { + var validRegex = /no-ng-animate/; + var invalidRegex = /no ng-animate/; + + $animateProvider.classNameFilter(validRegex); + expect($animateProvider.classNameFilter()).toEqual(validRegex); + + // eslint-disable-next-line no-empty + try { $animateProvider.classNameFilter(invalidRegex); } catch (err) {} + expect($animateProvider.classNameFilter()).toBeNull(); + }) + ); + it('should complete the leave DOM operation in case the classNameFilter fails', function() { module(function($animateProvider) { $animateProvider.classNameFilter(/memorable-animation/); From b7ee5ee3c6050c60058f5bcd895fdd8064091d1b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 20 Feb 2017 20:09:11 +0000 Subject: [PATCH 027/719] chore(jenkins): disable unit testing on Safari There is a strange failure in the animation code that only appears to happen on Safari 10 on OS/X. While we investigate we are disabling this browser to allow the development (and doc generation) to continue. --- scripts/jenkins/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/jenkins/build.sh b/scripts/jenkins/build.sh index dbb4383d7d0b..e73292df4c92 100755 --- a/scripts/jenkins/build.sh +++ b/scripts/jenkins/build.sh @@ -12,7 +12,7 @@ set -xe # This is the default set of browsers to use on the CI server unless overridden via env variable if [[ -z "$BROWSERS" ]] then - BROWSERS="Chrome,Firefox,/Users/jenkins/bin/safari.sh" + BROWSERS="Chrome,Firefox" fi # CLEAN # From 892d236afcf772f2377eac6f75caa8f1e40e3159 Mon Sep 17 00:00:00 2001 From: waahhhh Date: Tue, 21 Feb 2017 15:20:14 +0100 Subject: [PATCH 028/719] docs(changelog): correct typo changed spelling from "auto-bootstraping" to "auto-bootstrapping". Closes #15729 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bf50927d8fd..86d79b997706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ ([05aab6](https://github.com/angular/angular.js/commit/05aab660ce74f526f2110d3b5faf9a5b4f4e664b) [#15603](https://github.com/angular/angular.js/issues/15603), [#15605](https://github.com/angular/angular.js/issues/15605)) -- **angularInit:** allow auto-bootstraping from inline script +- **angularInit:** allow auto-bootstrapping from inline script ([bb464d](https://github.com/angular/angular.js/commit/bb464d16b434b9e2de2fecf80c192d4741cba879), [#15567](https://github.com/angular/angular.js/issues/15567), [#15571](https://github.com/angular/angular.js/issues/15571)) @@ -84,7 +84,7 @@ ([e75fbc](https://github.com/angular/angular.js/commit/e75fbc494e6a0da6a9231b40bb0382431b62be07), [#15586](https://github.com/angular/angular.js/issues/15586), [#15594](https://github.com/angular/angular.js/issues/15594)) -- **angularInit:** allow auto-bootstraping from inline script +- **angularInit:** allow auto-bootstrapping from inline script ([41aa91](https://github.com/angular/angular.js/commit/41aa9125b9aaf771addb250642f524a4e6f9d8d3), [#15567](https://github.com/angular/angular.js/issues/15567), [#15571](https://github.com/angular/angular.js/issues/15571)) From 295043d32e27fdea26cfe3f8a9a7d47c5d292f5a Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 22 Feb 2017 20:07:28 +0100 Subject: [PATCH 029/719] docs($compile): clarify to which element scope isolation applies Closes #13556 --- src/ng/compile.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 028901f0644d..d5e237c6adc2 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -277,10 +277,12 @@ * the directive's element. If multiple directives on the same element request a new scope, * only one new scope is created. * - * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The - * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent - * scope. This is useful when creating reusable components, which should not accidentally read or modify - * data in the parent scope. + * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's template. + * The 'isolate' scope differs from normal scope in that it does not prototypically + * inherit from its parent scope. This is useful when creating reusable components, which should not + * accidentally read or modify data in the parent scope. Note that an isolate scope + * directive without a `template` or `templateUrl` will not apply the isolate scope + * to its children elements. * * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the * directive's element. These local properties are useful for aliasing values for templates. The keys in From 603b66e1facb59cf4587f0c19112a3e78a4685ff Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 24 Feb 2017 11:09:11 +0100 Subject: [PATCH 030/719] docs(changelog): insert release notes for 1.2.32 They were only in the v1.2.x branch. --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d79b997706..41993763dd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3101,6 +3101,18 @@ validation), you can overwrite the built-in `step` validator with a custom direc }) ``` + +# 1.2.32 alternation-intention (2016-10-11) + +This release reverts the fix in 1.2.31 and provides an alternative fix that doesn't break Angular Material. + +## Reverts +- **input:** ensure that hidden input values are correct after history back + ([ed44dd065](https://github.com/angular/angular.js/commit/ed44dd0659f346ced78a112e4a2b30d3af4fd572)) + +## Bug Fixes +- **$compile:** ensure that hidden input values are correct after history back + ([b8a0ecdd6](https://github.com/angular/angular.js/commit/b8a0ecdd6189fb111734eb5b6d4d473d0dcf4c36)) From c357b1aba6f990820c6789ff43727e4754e7713a Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 24 Feb 2017 12:26:05 +0000 Subject: [PATCH 031/719] test(Angular): refactor auto bootstrap tests --- test/AngularSpec.js | 127 +++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 66 deletions(-) diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 9aaa2f5b8f83..627d206a6e7e 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1708,83 +1708,78 @@ describe('angular', function() { dealoc(appElement); }); - it('should bootstrap from an extension into an extension document for same-origin documents only', function() { - // IE does not support `document.currentScript` (nor extensions with protocol), so skip test. - if (msie) return; - - // Extension URLs are browser-specific, so we must choose a scheme that is supported by the browser to make - // sure that the URL is properly parsed. - var extensionScheme; - var userAgent = window.navigator.userAgent; - if (/Firefox\//.test(userAgent)) { - extensionScheme = 'moz-extension'; - } else if (/Edge\//.test(userAgent)) { - extensionScheme = 'ms-browser-extension'; - } else if (/Chrome\//.test(userAgent)) { - extensionScheme = 'chrome-extension'; - } else if (/Safari\//.test(userAgent)) { - extensionScheme = 'safari-extension'; - } else { - extensionScheme = 'browserext'; // Upcoming standard scheme. - } + // IE does not support `document.currentScript` (nor extensions with protocol), so skip tests. + if (!msie) { + describe('auto bootstrap restrictions', function() { - var src = extensionScheme + '://something'; - // Fake a minimal document object (the actual document.currentScript is readonly). - var fakeDoc = { - currentScript: { getAttribute: function() { return src; } }, - location: {protocol: extensionScheme + ':', origin: extensionScheme + '://something'}, - createElement: document.createElement.bind(document) - }; - expect(allowAutoBootstrap(fakeDoc)).toBe(true); + function createFakeDoc(attrs, protocol, currentScript) { - src = extensionScheme + '://something-else'; - expect(allowAutoBootstrap(fakeDoc)).toBe(false); - }); + protocol = protocol || 'http:'; + var origin = protocol + '//something'; - it('should bootstrap from a script with an empty or missing `src` attribute', function() { - // IE does not support `document.currentScript` (nor extensions with protocol), so skip test. - if (msie) return; + if (currentScript === undefined) { + currentScript = document.createElement('script'); + Object.keys(attrs).forEach(function(key) { currentScript.setAttribute(key, attrs[key]); }); + } - // Fake a minimal document object (the actual document.currentScript is readonly). - var src; - var fakeDoc = { - createElement: document.createElement.bind(document), - currentScript: {getAttribute: function() { return src; }}, - location: {origin: 'some-value', protocol: 'http:'} - }; + // Fake a minimal document object (the actual document.currentScript is readonly). + return { + currentScript: currentScript, + location: {protocol: protocol, origin: origin}, + createElement: document.createElement.bind(document) + }; + } - src = null; - expect(allowAutoBootstrap(fakeDoc)).toBe(true); + it('should bootstrap from an extension into an extension document for same-origin documents only', function() { + + // Extension URLs are browser-specific, so we must choose a scheme that is supported by the browser to make + // sure that the URL is properly parsed. + var protocol; + var userAgent = window.navigator.userAgent; + if (/Firefox\//.test(userAgent)) { + protocol = 'moz-extension:'; + } else if (/Edge\//.test(userAgent)) { + protocol = 'ms-browser-extension:'; + } else if (/Chrome\//.test(userAgent)) { + protocol = 'chrome-extension:'; + } else if (/Safari\//.test(userAgent)) { + protocol = 'safari-extension:'; + } else { + protocol = 'browserext:'; // Upcoming standard scheme. + } - src = ''; - expect(allowAutoBootstrap(fakeDoc)).toBe(true); - }); + expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something'}, protocol))).toBe(true); + expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something-else'}, protocol))).toBe(false); + }); - it('should not bootstrap from an extension into a non-extension document', function() { - // IE does not support `document.currentScript` (nor extensions with protocol), so skip test. - if (msie) return; + it('should bootstrap from a script with empty or no source (e.g. src, href or xlink:href attributes)', function() { - var src = 'resource://something'; - // Fake a minimal document object (the actual document.currentScript is readonly). - var fakeDoc = { - currentScript: { getAttribute: function() { return src; } }, - location: {protocol: 'http:'}, - createElement: document.createElement.bind(document) - }; - expect(allowAutoBootstrap(fakeDoc)).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({src: null}))).toBe(true); + expect(allowAutoBootstrap(createFakeDoc({src: ''}))).toBe(true); - src = 'file://whatever'; - expect(allowAutoBootstrap(fakeDoc)).toBe(true); - }); + expect(allowAutoBootstrap(createFakeDoc({href: null}))).toBe(true); + expect(allowAutoBootstrap(createFakeDoc({href: ''}))).toBe(true); - it('should not bootstrap if bootstrapping is disabled', function() { - isAutoBootstrapAllowed = false; - angularInit(jqLite('
')[0], bootstrapSpy); - expect(bootstrapSpy).not.toHaveBeenCalled(); - isAutoBootstrapAllowed = true; - }); - }); + expect(allowAutoBootstrap(createFakeDoc({'xlink:href': null}))).toBe(true); + expect(allowAutoBootstrap(createFakeDoc({'xlink:href': ''}))).toBe(true); + }); + + + it('should not bootstrap from an extension into a non-extension document', function() { + expect(allowAutoBootstrap(createFakeDoc({src: 'resource://something'}))).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({src: 'file://whatever'}))).toBe(true); + }); + + it('should not bootstrap if bootstrapping is disabled', function() { + isAutoBootstrapAllowed = false; + angularInit(jqLite('
')[0], bootstrapSpy); + expect(bootstrapSpy).not.toHaveBeenCalled(); + isAutoBootstrapAllowed = true; + }); + }); + } + }); describe('AngularJS service', function() { it('should override services', function() { From a649758655843275cc477fb638f8e55f72a4eaa6 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 24 Feb 2017 12:27:19 +0000 Subject: [PATCH 032/719] fix(Angular): do not auto bootstrap if the script source is bad and inside SVG --- src/Angular.js | 50 ++++++++++++++++++++++++++------------------- test/AngularSpec.js | 13 ++++++++++++ 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 48c31ea1a0b9..9d6dd758bf7f 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1532,33 +1532,41 @@ function getNgAttribute(element, ngAttr) { function allowAutoBootstrap(document) { var script = document.currentScript; - var src = script && script.getAttribute('src'); - if (!src) { + if (!script) { + // IE does not have `document.currentScript` return true; } - var link = document.createElement('a'); - link.href = src; + var srcs = [script.getAttribute('src'), script.getAttribute('href'), script.getAttribute('xlink:href')]; - if (document.location.origin === link.origin) { - // Same-origin resources are always allowed, even for non-whitelisted schemes. - return true; - } - // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. - // This is to prevent angular.js bundled with browser extensions from being used to bypass the - // content security policy in web pages and other browser extensions. - switch (link.protocol) { - case 'http:': - case 'https:': - case 'ftp:': - case 'blob:': - case 'file:': - case 'data:': + return srcs.every(function(src) { + if (!src) { return true; - default: - return false; - } + } + + var link = document.createElement('a'); + link.href = src; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); } // Cached as it has to run during loading so that document.currentScript is available. diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 627d206a6e7e..37b39586517a 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1771,6 +1771,19 @@ describe('angular', function() { expect(allowAutoBootstrap(createFakeDoc({src: 'file://whatever'}))).toBe(true); }); + it('should not bootstrap from an extension into a non-extension document, via SVG script', function() { + + // SVG script tags don't use the `src` attribute to load their source. + // Instead they use `href` or the deprecated `xlink:href` attributes. + + expect(allowAutoBootstrap(createFakeDoc({href: 'resource://something'}))).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({'xlink:href': 'resource://something'}))).toBe(false); + + expect(allowAutoBootstrap(createFakeDoc({src: 'http://something', href: 'resource://something'}))).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({href: 'http://something', 'xlink:href': 'resource://something'}))).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({src: 'resource://something', href: 'http://something', 'xlink:href': 'http://something'}))).toBe(false); + }); + it('should not bootstrap if bootstrapping is disabled', function() { isAutoBootstrapAllowed = false; angularInit(jqLite('
')[0], bootstrapSpy); From ebe90051eda8a3328e5993cca1663e28d03113d0 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 24 Feb 2017 14:53:22 +0000 Subject: [PATCH 033/719] fix(Angular): do not auto bootstrap if the currentScript has been clobbered --- src/Angular.js | 5 +++++ test/AngularSpec.js | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/Angular.js b/src/Angular.js index 9d6dd758bf7f..1322cfd35aad 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1538,6 +1538,11 @@ function allowAutoBootstrap(document) { return true; } + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + var srcs = [script.getAttribute('src'), script.getAttribute('href'), script.getAttribute('xlink:href')]; return srcs.every(function(src) { diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 37b39586517a..647553bff9ec 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1784,6 +1784,13 @@ describe('angular', function() { expect(allowAutoBootstrap(createFakeDoc({src: 'resource://something', href: 'http://something', 'xlink:href': 'http://something'}))).toBe(false); }); + it('should not bootstrap if the currentScript property has been clobbered', function() { + + var img = document.createElement('img'); + img.setAttribute('src', ''); + expect(allowAutoBootstrap(createFakeDoc({}, 'http:', img))).toBe(false); + }); + it('should not bootstrap if bootstrapping is disabled', function() { isAutoBootstrapAllowed = false; angularInit(jqLite('
')[0], bootstrapSpy); From 4f69d38f097fab76e683105d1c758706e6cbe1a9 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 13 Feb 2017 21:24:13 +0200 Subject: [PATCH 034/719] fix($sanitize): prevent clobbered elements from freezing the browser Closes #15699 --- docs/content/error/$sanitize/elclob.ngdoc | 11 +++++++++++ src/ngSanitize/sanitize.js | 23 +++++++++++++++++++---- test/ngSanitize/sanitizeSpec.js | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 docs/content/error/$sanitize/elclob.ngdoc diff --git a/docs/content/error/$sanitize/elclob.ngdoc b/docs/content/error/$sanitize/elclob.ngdoc new file mode 100644 index 000000000000..1e9e9db42725 --- /dev/null +++ b/docs/content/error/$sanitize/elclob.ngdoc @@ -0,0 +1,11 @@ +@ngdoc error +@name $sanitize:elclob +@fullName Failed to sanitize html because the element is clobbered +@description + +This error occurs when `$sanitize` sanitizer is unable to traverse the HTML because one or more of the elements in the +HTML have been "clobbered". This could be a sign that the payload contains code attempting to cause a DoS attack on the +browser. + +Typically clobbering breaks the `nextSibling` property on an element so that it points to one of its child nodes. This +makes it impossible to walk the HTML tree without getting stuck in an infinite loop, which causes the browser to freeze. \ No newline at end of file diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index f5f7490c2f67..73168716a5d2 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -18,6 +18,7 @@ var forEach; var isDefined; var lowercase; var noop; +var nodeContains; var htmlParser; var htmlSanitizeWriter; @@ -218,6 +219,11 @@ function $SanitizeProvider() { htmlParser = htmlParserImpl; htmlSanitizeWriter = htmlSanitizeWriterImpl; + nodeContains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise + return !!(this.compareDocumentPosition(arg) & 16); + }; + // Regular Expressions for parsing tags and attributes var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, // Match everything outside of normal chars and " (quote character) @@ -381,12 +387,12 @@ function $SanitizeProvider() { if (node.nodeType === 1) { handler.end(node.nodeName.toLowerCase()); } - nextNode = node.nextSibling; + nextNode = getNonDescendant('nextSibling', node); if (!nextNode) { while (nextNode == null) { - node = node.parentNode; + node = getNonDescendant('parentNode', node); if (node === inertBodyElement) break; - nextNode = node.nextSibling; + nextNode = getNonDescendant('nextSibling', node); if (node.nodeType === 1) { handler.end(node.nodeName.toLowerCase()); } @@ -518,8 +524,17 @@ function $SanitizeProvider() { stripCustomNsAttrs(nextNode); } - node = node.nextSibling; + node = getNonDescendant('nextSibling', node); + } + } + + function getNonDescendant(propName, node) { + // An element is clobbered if its `propName` property points to one of its descendants + var nextNode = node[propName]; + if (nextNode && nodeContains.call(node, nextNode)) { + throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText); } + return nextNode; } } diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index d7e3105f155b..dd8c2ba97d43 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -246,6 +246,26 @@ describe('HTML', function() { .toEqual('

text1text2

'); }); + it('should remove clobbered elements', function() { + inject(function($sanitize) { + expect(function() { + $sanitize('
'); + }).toThrowMinErr('$sanitize', 'elclob'); + + expect(function() { + $sanitize('
'); + }).toThrowMinErr('$sanitize', 'elclob'); + + expect(function() { + $sanitize('
'); + }).toThrowMinErr('$sanitize', 'elclob'); + + expect(function() { + $sanitize('
'); + }).toThrowMinErr('$sanitize', 'elclob'); + }); + }); + describe('SVG support', function() { From 19bc52127f8393ffccac563b21267adb75c19275 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 27 Feb 2017 11:53:44 +0000 Subject: [PATCH 035/719] fix(Angular): do not autobootstrap if the `src` exists but is empty In Chrome an empty `src` attribute will be ignored, but in Firefox it seems happy to prepend the `base[href]` and try to load whatever that is. --- src/Angular.js | 8 ++++++-- test/AngularSpec.js | 13 +++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 1322cfd35aad..3d4d4ed9092f 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1543,15 +1543,19 @@ function allowAutoBootstrap(document) { return false; } - var srcs = [script.getAttribute('src'), script.getAttribute('href'), script.getAttribute('xlink:href')]; + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; return srcs.every(function(src) { if (!src) { return true; } + if (!src.value) { + return false; + } var link = document.createElement('a'); - link.href = src; + link.href = src.value; if (document.location.origin === link.origin) { // Same-origin resources are always allowed, even for non-whitelisted schemes. diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 647553bff9ec..e67b8782eb50 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1752,16 +1752,17 @@ describe('angular', function() { expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something-else'}, protocol))).toBe(false); }); - it('should bootstrap from a script with empty or no source (e.g. src, href or xlink:href attributes)', function() { + it('should bootstrap from a script with no source (e.g. src, href or xlink:href attributes)', function() { expect(allowAutoBootstrap(createFakeDoc({src: null}))).toBe(true); - expect(allowAutoBootstrap(createFakeDoc({src: ''}))).toBe(true); - expect(allowAutoBootstrap(createFakeDoc({href: null}))).toBe(true); - expect(allowAutoBootstrap(createFakeDoc({href: ''}))).toBe(true); - expect(allowAutoBootstrap(createFakeDoc({'xlink:href': null}))).toBe(true); - expect(allowAutoBootstrap(createFakeDoc({'xlink:href': ''}))).toBe(true); + }); + + it('should not bootstrap from a script with an empty source (e.g. `src=""`)', function() { + expect(allowAutoBootstrap(createFakeDoc({src: ''}))).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({href: ''}))).toBe(false); + expect(allowAutoBootstrap(createFakeDoc({'xlink:href': ''}))).toBe(false); }); From e269c14425a3209040f65c022658770e00a36f16 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 1 Mar 2017 13:27:17 +0100 Subject: [PATCH 036/719] fix($controller): remove instantiating controllers defined on window This also removes the likewise deprecated `$controllerProvider.allowGlobals()` method. Closes #15349 Closes #15762 BREAKING CHANGE: The option to instantiate controllers from constructors on the global `window` object has been removed. Likewise, the deprecated `$controllerProvider.allowGlobals()` method that could enable this behavior, has been removed. This behavior had been deprecated since AngularJS v1.3.0, because polluting the global scope is bad. To migrate, remove the call to $controllerProvider.allowGlobals() in the config, and register your controller via the Module API or the $controllerProvider, e.g. ``` angular.module('myModule', []).controller('myController', function() {...}); angular.module('myModule', []).config(function($controllerProvider) { $controllerProvider.register('myController', function() {...}); }); ``` --- docs/content/tutorial/step_02.ngdoc | 34 ++++++++--------------------- src/ng/controller.js | 23 ++----------------- src/ng/directive/ngController.js | 4 ---- src/ngMock/angular-mocks.js | 2 -- test/ng/controllerSpec.js | 15 ------------- 5 files changed, 11 insertions(+), 67 deletions(-) diff --git a/docs/content/tutorial/step_02.ngdoc b/docs/content/tutorial/step_02.ngdoc index a9bcc5e22836..ba2edc0b9085 100644 --- a/docs/content/tutorial/step_02.ngdoc +++ b/docs/content/tutorial/step_02.ngdoc @@ -150,38 +150,22 @@ To learn more about AngularJS scopes, see the {@link ng.$rootScope.Scope Angular # Testing -The "AngularJS way" of separating controller from the view, makes it easy to test code as it is being -developed. If our controller were available on the global namespace, we could simply instantiate it -with a mock scope object: +## Testing Controllers -
-```js -describe('PhoneListController', function() { +The "AngularJS way" of separating the controller from the view makes it easy to test code as it is being +developed. In the section "Model and Controller" we have registered our controller via a constructor +function on the `phonecatApp` module. - it('should create a `phones` model with 3 phones', function() { - var scope = {}; - var ctrl = new PhoneListController(scope); - - expect(scope.phones.length).toBe(3); - }); +In tests, we use an AngularJS service, `$controller`, which will retrieve a controller by name. It +also takes a second argument - a map of dependencies that should be injected. -}); -``` +The following test instantiates `PhoneListController` with a mock scope object, +and verifies that the phones array property on the scope contains three records. -The test instantiates `PhoneListController` and verifies that the phones array property on the -scope contains three records. This example demonstrates how easy it is to create a unit test for +This example demonstrates how easy it is to create a unit test for code in AngularJS. Since testing is such a critical part of software development, we make it easy to create tests in AngularJS so that developers are encouraged to write them. - -## Testing non-global Controllers - -In practice, you will not want to have your controller functions in the global namespace. Instead, -you can see that we have registered it via a constructor function on the `phonecatApp` module. - -In this case AngularJS provides a service, `$controller`, which will retrieve your controller by name. -Here is the same test using `$controller`: -
**`app/app.spec.js`:** diff --git a/src/ng/controller.js b/src/ng/controller.js index 12a883e3b08e..1d707b243890 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -26,8 +26,7 @@ function identifierForController(controller, ident) { * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { - var controllers = {}, - globals = false; + var controllers = {}; /** * @ngdoc method @@ -55,21 +54,6 @@ function $ControllerProvider() { } }; - /** - * @ngdoc method - * @name $controllerProvider#allowGlobals - * @description If called, allows `$controller` to find controller constructors on `window` - * - * @deprecated - * sinceVersion="v1.3.0" - * removeVersion="v1.7.0" - * This method of finding controllers has been deprecated. - */ - this.allowGlobals = function() { - globals = true; - }; - - this.$get = ['$injector', '$window', function($injector, $window) { /** @@ -83,8 +67,6 @@ function $ControllerProvider() { * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this @@ -124,8 +106,7 @@ function $ControllerProvider() { identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || - (globals ? getter($window, constructor, true) : undefined); + : getter(locals.$scope, constructor, true); if (!expression) { throw $controllerMinErr('ctrlreg', diff --git a/src/ng/directive/ngController.js b/src/ng/directive/ngController.js index af339ee4ca5a..014095288000 100644 --- a/src/ng/directive/ngController.js +++ b/src/ng/directive/ngController.js @@ -31,10 +31,6 @@ * The controller instance can be published into a scope property by specifying * `ng-controller="as propertyName"`. * - * If the current `$controllerProvider` is configured to use globals (via - * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may - * also be the name of a globally accessible constructor function (deprecated, not recommended). - * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and * greeting are methods declared on the controller (see source tab). These methods can diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index f35f462ec89b..978802bf5b9c 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -2256,8 +2256,6 @@ angular.mock.$RootElementProvider = function() { * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this diff --git a/test/ng/controllerSpec.js b/test/ng/controllerSpec.js index 8fca250c9fba..56bfcf404bbf 100644 --- a/test/ng/controllerSpec.js +++ b/test/ng/controllerSpec.js @@ -95,21 +95,6 @@ describe('$controller', function() { }); - it('should instantiate a controller defined on window if allowGlobals is set', - inject(function($window) { - var scope = {}; - var Foo = function() {}; - - $controllerProvider.allowGlobals(); - - $window.a = {Foo: Foo}; - - var foo = $controller('a.Foo', {$scope: scope}); - expect(foo).toBeDefined(); - expect(foo instanceof Foo).toBe(true); - })); - - it('should throw ctrlfmt if name contains spaces', function() { expect(function() { $controller('ctrl doom'); From 49aba51e6bfd3f6d59c0f28628cc983d84438cfa Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 8 Oct 2016 15:17:37 +0100 Subject: [PATCH 037/719] feat(Module): add `info()` method The new `info()` method lets developers store arbitrary information about their module for consumption later. Closes #15225 --- docs/content/error/ng/aobj.ngdoc | 7 ++++++ src/loader.js | 42 ++++++++++++++++++++++++++++++++ test/loaderSpec.js | 31 +++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 docs/content/error/ng/aobj.ngdoc diff --git a/docs/content/error/ng/aobj.ngdoc b/docs/content/error/ng/aobj.ngdoc new file mode 100644 index 000000000000..101fb172393b --- /dev/null +++ b/docs/content/error/ng/aobj.ngdoc @@ -0,0 +1,7 @@ +@ngdoc error +@name ng:aobj +@fullName Invalid Argument +@description + +The argument passed should be an object. Check the value that was passed to the function where +this error was thrown. diff --git a/src/loader.js b/src/loader.js index 36da047fe20b..05c24ce3e28f 100644 --- a/src/loader.js +++ b/src/loader.js @@ -79,6 +79,9 @@ function setupModuleLoader(window) { * @returns {angular.Module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + + var info = {}; + var assertNotHasOwnProperty = function(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); @@ -114,6 +117,45 @@ function setupModuleLoader(window) { _configBlocks: configBlocks, _runBlocks: runBlocks, + /** + * @ngdoc method + * @name angular.Module#info + * @module ng + * + * @param {Object=} info Information about the module + * @returns {Object|Module} The current info object for this module if called as a getter, + * or `this` if called as a setter. + * + * @description + * Read and write custom information about this module. + * For example you could put the version of the module in here. + * + * ```js + * angular.module('myModule', []).info({ version: '1.0.0' }); + * ``` + * + * The version could then be read back out by accessing the module elsewhere: + * + * ``` + * var version = angular.module('myModule').info().version; + * ``` + * + * You can also retrieve this information during runtime via the + * {@link $injector#modules `$injector.modules`} property: + * + * ```js + * var version = $injector.modules['myModule'].info().version; + * ``` + */ + info: function(value) { + if (isDefined(value)) { + if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value'); + info = value; + return this; + } + return info; + }, + /** * @ngdoc property * @name angular.Module#requires diff --git a/test/loaderSpec.js b/test/loaderSpec.js index c4f7a33989bd..7166f218c3e8 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -156,4 +156,35 @@ describe('module loader', function() { it('should expose `$$minErr` on the `angular` object', function() { expect(window.angular.$$minErr).toEqual(jasmine.any(Function)); }); + + describe('Module', function() { + describe('info()', function() { + var theModule; + + beforeEach(function() { + theModule = angular.module('theModule', []); + }); + + it('should default to an empty object', function() { + expect(theModule.info()).toEqual({}); + }); + + it('should store the object passed as a param', function() { + theModule.info({ version: '1.2' }); + expect(theModule.info()).toEqual({ version: '1.2' }); + }); + + it('should throw if the parameter is not an object', function() { + expect(function() { + theModule.info('some text'); + }).toThrowMinErr('ng', 'aobj'); + }); + + it('should completely replace the previous info object', function() { + theModule.info({ value: 'X' }); + theModule.info({ newValue: 'Y' }); + expect(theModule.info()).toEqual({ newValue: 'Y' }); + }); + }); + }); }); From 550f309dba4d3c1530c128b1c97a1678c2be0da1 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 8 Oct 2016 16:16:49 +0100 Subject: [PATCH 038/719] feat($injector): add new `modules` property The `modules` property is a hash of the modules loaded into the injector at bootstrap time. This can be used to access the module's info. --- src/auto/injector.js | 24 ++++++++++++++++++++++++ test/auto/injectorSpec.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/auto/injector.js b/src/auto/injector.js index f7022ec3023e..70e3c6eb4592 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -180,6 +180,28 @@ function annotate(fn, strictDi, name) { * As an array of injection names, where the last item in the array is the function to call. */ +/** + * @ngdoc property + * @name $injector#modules + * @type {Object} + * @description + * A hash containing all the modules that have been loaded into the + * $injector. + * + * You can use this property to find out information about a module via the + * {@link angular.Module#info `myModule.info(...)`} method. + * + * For example: + * + * ``` + * var info = $injector.modules['ngAnimate'].info(); + * ``` + * + * **Do not use this property to attempt to modify the modules after the application + * has been bootstrapped.** + */ + + /** * @ngdoc method * @name $injector#get @@ -673,6 +695,7 @@ function createInjector(modulesToLoad, strictDi) { instanceInjector = protoInstanceInjector; providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + instanceInjector.modules = providerInjector.modules = createMap(); var runBlocks = loadModules(modulesToLoad); instanceInjector = protoInstanceInjector.get('$injector'); instanceInjector.strictDi = strictDi; @@ -768,6 +791,7 @@ function createInjector(modulesToLoad, strictDi) { try { if (isString(module)) { moduleFn = angularModule(module); + instanceInjector.modules[module] = moduleFn; runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks); diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index 397d95fda04d..2cdb34ed1675 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -2,6 +2,37 @@ /* globals support: false */ +describe('injector.modules', function() { + it('should expose the loaded module info on the instance injector', function() { + var test1 = angular.module('test1', ['test2']).info({ version: '1.1' }); + var test2 = angular.module('test2', []).info({ version: '1.2' }); + module('test1'); + inject(['$injector', function($injector) { + expect(Object.keys($injector.modules)).toEqual(['ng', 'ngLocale', 'ngMock', 'test1', 'test2']); + expect($injector.modules['test1'].info()).toEqual({ version: '1.1' }); + expect($injector.modules['test2'].info()).toEqual({ version: '1.2' }); + }]); + }); + + it('should expose the loaded module info on the provider injector', function() { + var providerInjector; + var test1 = angular.module('test1', ['test2']).info({ version: '1.1' }); + var test2 = angular.module('test2', []) + .info({ version: '1.2' }) + .provider('test', ['$injector', function($injector) { + providerInjector = $injector; + return { $get: function() {} }; + }]); + module('test1'); + // needed to ensure that the provider blocks are executed + inject(); + + expect(Object.keys(providerInjector.modules)).toEqual(['ng', 'ngLocale', 'ngMock', 'test1', 'test2']); + expect(providerInjector.modules['test1'].info()).toEqual({ version: '1.1' }); + expect(providerInjector.modules['test2'].info()).toEqual({ version: '1.2' }); + }); +}); + describe('injector', function() { var providers; var injector; From 60e294cbd8f0c34ed03046778a0be3befc6b6982 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 27 Feb 2017 21:34:48 +0000 Subject: [PATCH 039/719] feat(info): add `angularVersion` info to each module You can now check what version of AngularJS a core module is designed for: ``` var angularVersion = $injector.modules['myModule'].info().angularVersion; ``` --- src/AngularPublic.js | 3 ++- src/loader.prefix | 5 ++++- src/ngAnimate/module.js | 1 + src/ngAria/aria.js | 1 + src/ngCookies/cookies.js | 1 + src/ngMessageFormat/messageFormatService.js | 1 + src/ngMessages/messages.js | 1 + src/ngMock/angular-mocks.js | 5 +++-- src/ngParseExt/module.js | 3 ++- src/ngResource/resource.js | 1 + src/ngRoute/route.js | 1 + src/ngSanitize/sanitize.js | 4 +++- src/ngTouch/touch.js | 2 ++ 13 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/AngularPublic.js b/src/AngularPublic.js index e90814e8d1a9..827e97399ab0 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -265,5 +265,6 @@ function publishExternalAPI(angular) { $$cookieReader: $$CookieReaderProvider }); } - ]); + ]) + .info({ angularVersion: '"NG_VERSION_FULL"' }); } diff --git a/src/loader.prefix b/src/loader.prefix index 2783f7834268..b5188555461e 100644 --- a/src/loader.prefix +++ b/src/loader.prefix @@ -5,4 +5,7 @@ */ 'use strict'; (function() { - function isFunction(value) {return typeof value === 'function';}; + function isFunction(value) {return typeof value === 'function';} + function isDefined(value) {return typeof value !== 'undefined';} + function isObject(value) {return value !== null && typeof value === 'object';} + diff --git a/src/ngAnimate/module.js b/src/ngAnimate/module.js index ee83e27c9772..704021450add 100644 --- a/src/ngAnimate/module.js +++ b/src/ngAnimate/module.js @@ -756,6 +756,7 @@ angular.module('ngAnimate', [], function initAngularHelpers() { isFunction = angular.isFunction; isElement = angular.isElement; }) + .info({ angularVersion: '"NG_VERSION_FULL"' }) .directive('ngAnimateSwap', ngAnimateSwapDirective) .directive('ngAnimateChildren', $$AnimateChildrenDirective) diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js index d46822d57a25..639b370a6f75 100644 --- a/src/ngAria/aria.js +++ b/src/ngAria/aria.js @@ -54,6 +54,7 @@ * {@link guide/accessibility Developer Guide}. */ var ngAriaModule = angular.module('ngAria', ['ng']). + info({ angularVersion: '"NG_VERSION_FULL"' }). provider('$aria', $AriaProvider); /** diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index fb6047d09c23..6901da62aaf8 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -17,6 +17,7 @@ angular.module('ngCookies', ['ng']). + info({ angularVersion: '"NG_VERSION_FULL"' }). /** * @ngdoc provider * @name $cookiesProvider diff --git a/src/ngMessageFormat/messageFormatService.js b/src/ngMessageFormat/messageFormatService.js index 2e83e6674982..9f93950073a5 100644 --- a/src/ngMessageFormat/messageFormatService.js +++ b/src/ngMessageFormat/messageFormatService.js @@ -216,6 +216,7 @@ var toJson; var $$stringify; var module = window['angular']['module']('ngMessageFormat', ['ng']); +module['info']({ 'angularVersion': '"NG_VERSION_FULL"' }); module['factory']('$$messageFormat', $$MessageFormatFactory); module['config'](['$provide', function($provide) { $interpolateMinErr = window['angular']['$interpolateMinErr']; diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index b02ccd3db478..9de67aefc666 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -267,6 +267,7 @@ angular.module('ngMessages', [], function initAngularHelpers() { isString = angular.isString; jqLite = angular.element; }) + .info({ angularVersion: '"NG_VERSION_FULL"' }) /** * @ngdoc directive diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 978802bf5b9c..3cc6c7bf8ad1 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -790,6 +790,7 @@ angular.mock.TzDate.prototype = Date.prototype; * You need to require the `ngAnimateMock` module in your test suite for instance `beforeEach(module('ngAnimateMock'))` */ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) + .info({ angularVersion: '"NG_VERSION_FULL"' }) .config(['$provide', function($provide) { @@ -2404,7 +2405,7 @@ angular.module('ngMock', ['ng']).provider({ $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); $provide.decorator('$controller', createControllerDecorator($compileProvider)); $provide.decorator('$httpBackend', angular.mock.$httpBackendDecorator); -}]); +}]).info({ angularVersion: '"NG_VERSION_FULL"' }); /** * @ngdoc module @@ -2419,7 +2420,7 @@ angular.module('ngMock', ['ng']).provider({ */ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}]); +}]).info({ angularVersion: '"NG_VERSION_FULL"' }); /** * @ngdoc service diff --git a/src/ngParseExt/module.js b/src/ngParseExt/module.js index eeebf8812e4b..eccdb29bd155 100644 --- a/src/ngParseExt/module.js +++ b/src/ngParseExt/module.js @@ -44,4 +44,5 @@ function isValidIdentifierContinue(ch, cp) { angular.module('ngParseExt', []) .config(['$parseProvider', function($parseProvider) { $parseProvider.setIdentifierFns(isValidIdentifierStart, isValidIdentifierContinue); - }]); + }]) + .info({ angularVersion: '"NG_VERSION_FULL"' }); diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index bb879b884903..060d66b7f120 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -429,6 +429,7 @@ function shallowClearAndCopy(src, dst) { * */ angular.module('ngResource', ['ng']). + info({ angularVersion: '"NG_VERSION_FULL"' }). provider('$resource', function ResourceProvider() { var PROTOCOL_AND_IPV6_REGEX = /^https?:\/\/\[[^\]]*][^/]*/; diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 12e1492a3426..b368f9c4b25b 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -27,6 +27,7 @@ var noop; /* global -ngRouteModule */ var ngRouteModule = angular. module('ngRoute', []). + info({ angularVersion: '"NG_VERSION_FULL"' }). provider('$route', $RouteProvider). // Ensure `$route` will be instantiated in time to capture the initial `$locationChangeSuccess` // event (unless explicitly disabled). This is necessary in case `ngView` is included in an diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index 73168716a5d2..ae78082f18dc 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -547,4 +547,6 @@ function sanitizeText(chars) { // define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); +angular.module('ngSanitize', []) + .provider('$sanitize', $SanitizeProvider) + .info({ angularVersion: '"NG_VERSION_FULL"' }); diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js index 5ae19d6b4a87..d52d1d89ddac 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -24,6 +24,8 @@ /* global -ngTouch */ var ngTouch = angular.module('ngTouch', []); +ngTouch.info({ angularVersion: '"NG_VERSION_FULL"' }); + ngTouch.provider('$touch', $TouchProvider); function nodeName_(element) { From 846fa1cdf64e8d58dc8c7845639906cea48bc828 Mon Sep 17 00:00:00 2001 From: jason-larigakis-hs Date: Fri, 3 Mar 2017 02:08:54 -0800 Subject: [PATCH 040/719] docs(guide/Providers): remove confusing section This part of the guide is called "Providers", which means the section no longer applies. Closes #15771 --- docs/content/guide/providers.ngdoc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/content/guide/providers.ngdoc b/docs/content/guide/providers.ngdoc index e8c80025cff6..ca701b97eded 100644 --- a/docs/content/guide/providers.ngdoc +++ b/docs/content/guide/providers.ngdoc @@ -188,11 +188,6 @@ myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]); Much simpler! -Note: Yes, we have called one of our service recipes 'Service'. We regret this and know that we'll -be somehow punished for our misdeed. It's like we named one of our offspring 'Child'. Boy, -that would mess with the teachers. - - ## Provider Recipe As already mentioned in the intro, the Provider recipe is the core recipe type and From 3dc0096dc4469808bd221610d71febe3f3e52d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 1 Mar 2017 14:47:10 +0100 Subject: [PATCH 041/719] fix($log): don't parse error stacks manually outside of IE/Edge IE/Edge display errors in such a way that it requires the user to click in 4 places to see the stack trace. There is no way to feature-detect it so there's a chance of the user agent sniffing to go wrong but since it's only about logging, this shouldn't break apps. Other browsers display errors in a sensible way and some of them map stack traces along source maps if available so it makes sense to let browsers display it as they want. Fixes #15590 Closes #15767 --- src/ng/log.js | 11 +++++++- test/ng/logSpec.js | 62 +++++++++++++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/ng/log.js b/src/ng/log.js index c9a58618751b..6849ed46de29 100644 --- a/src/ng/log.js +++ b/src/ng/log.js @@ -67,6 +67,15 @@ function $LogProvider() { }; this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method @@ -124,7 +133,7 @@ function $LogProvider() { function formatError(arg) { if (arg instanceof Error) { - if (arg.stack) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; diff --git a/test/ng/logSpec.js b/test/ng/logSpec.js index b485f1022931..4f4abec24cd0 100644 --- a/test/ng/logSpec.js +++ b/test/ng/logSpec.js @@ -7,7 +7,10 @@ describe('$log', function() { beforeEach(module(function($provide) { - $window = {navigator: {}, document: {}}; + $window = { + navigator: {userAgent: window.navigator.userAgent}, + document: {} + }; logger = ''; log = function() { logger += 'log;'; }; warn = function() { logger += 'warn;'; }; @@ -64,6 +67,13 @@ describe('$log', function() { } )); + it('should work if $window.navigator not defined', inject( + function() { + delete $window.navigator; + }, + function($log) {} + )); + describe('IE logging behavior', function() { function removeApplyFunctionForIE() { log.apply = log.call = @@ -131,12 +141,12 @@ describe('$log', function() { $log.debug(); expect(logger).toEqual('log;warn;info;error;'); } - )); + )); }); describe('$log.error', function() { - var e, $log, errorArgs; + var e, $log; function TestError() { Error.prototype.constructor.apply(this, arguments); @@ -148,38 +158,44 @@ describe('$log', function() { TestError.prototype = Object.create(Error.prototype); TestError.prototype.constructor = TestError; - beforeEach(function() { - e = new TestError(''); - var mockWindow = { - console: { - error: function() { - errorArgs = [].slice.call(arguments, 0); - } - } - }; - $log = new $LogProvider().$get[1](mockWindow); - }); + beforeEach(inject( + function() { + e = new TestError(''); + $window.console = { + error: jasmine.createSpy('error') + }; + }, + function(_$log_) { + $log = _$log_; + } + )); it('should pass error if does not have trace', function() { $log.error('abc', e); - expect(errorArgs).toEqual(['abc', e]); - }); - - - it('should print stack', function() { - e.stack = 'stack'; - $log.error('abc', e); - expect(errorArgs).toEqual(['abc', 'stack']); + expect($window.console.error).toHaveBeenCalledWith('abc', e); }); + if (msie || /\bEdge\//.test(window.navigator.userAgent)) { + it('should print stack', function() { + e.stack = 'stack'; + $log.error('abc', e); + expect($window.console.error).toHaveBeenCalledWith('abc', 'stack'); + }); + } else { + it('should print a raw error', function() { + e.stack = 'stack'; + $log.error('abc', e); + expect($window.console.error).toHaveBeenCalledWith('abc', e); + }); + } it('should print line', function() { e.message = 'message'; e.sourceURL = 'sourceURL'; e.line = '123'; $log.error('abc', e); - expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']); + expect($window.console.error).toHaveBeenCalledWith('abc', 'message\nsourceURL:123'); }); }); From cc793a1364d5861afd262846ef4c621812ea353c Mon Sep 17 00:00:00 2001 From: mohamed amr Date: Thu, 2 Mar 2017 18:24:17 +0200 Subject: [PATCH 042/719] test(errorHandlingConfig): add tests for `errorHandlingConfig()` (independent of `minErr`) Closes #15770 --- test/AngularSpec.js | 25 +++++++++++++++++++++++++ test/minErrSpec.js | 9 ++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/test/AngularSpec.js b/test/AngularSpec.js index e67b8782eb50..5e9ef2167429 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -7,6 +7,7 @@ Float32Array, Float64Array, */ describe('angular', function() { var element, document; + var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth; beforeEach(function() { document = window.document; @@ -14,6 +15,30 @@ describe('angular', function() { afterEach(function() { dealoc(element); + minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage; + }); + + describe('errorHandlingConfig', function() { + it('should get default objectMaxDepth', function() { + expect(errorHandlingConfig().objectMaxDepth).toBe(5); + }); + + it('should set objectMaxDepth', function() { + errorHandlingConfig({objectMaxDepth: 3}); + expect(errorHandlingConfig().objectMaxDepth).toBe(3); + }); + + it('should not change objectMaxDepth when undefined is supplied', function() { + errorHandlingConfig({objectMaxDepth: undefined}); + expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage); + }); + + they('should set objectMaxDepth to NaN when $prop is supplied', + [NaN, null, true, false, -1, 0], function(maxDepth) { + errorHandlingConfig({objectMaxDepth: maxDepth}); + expect(errorHandlingConfig().objectMaxDepth).toBeNaN(); + } + ); }); describe('case', function() { diff --git a/test/minErrSpec.js b/test/minErrSpec.js index cc38354e8cbc..62f78c2c3ad1 100644 --- a/test/minErrSpec.js +++ b/test/minErrSpec.js @@ -78,32 +78,27 @@ describe('minErr', function() { var myError = testError('26', 'a when objectMaxDepth is default=5 is {0}', a); expect(myError.message).toMatch(/a when objectMaxDepth is default=5 is {"b":{"c":{"d":{"e":{"f":"..."}}}}}/); - expect(errorHandlingConfig().objectMaxDepth).toBe(5); errorHandlingConfig({objectMaxDepth: 1}); myError = testError('26', 'a when objectMaxDepth is set to 1 is {0}', a); expect(myError.message).toMatch(/a when objectMaxDepth is set to 1 is {"b":"..."}/); - expect(errorHandlingConfig().objectMaxDepth).toBe(1); errorHandlingConfig({objectMaxDepth: 2}); myError = testError('26', 'a when objectMaxDepth is set to 2 is {0}', a); expect(myError.message).toMatch(/a when objectMaxDepth is set to 2 is {"b":{"c":"..."}}/); - expect(errorHandlingConfig().objectMaxDepth).toBe(2); errorHandlingConfig({objectMaxDepth: undefined}); myError = testError('26', 'a when objectMaxDepth is set to undefined is {0}', a); expect(myError.message).toMatch(/a when objectMaxDepth is set to undefined is {"b":{"c":"..."}}/); - expect(errorHandlingConfig().objectMaxDepth).toBe(2); }); they('should handle arguments that are objects and ignore max depth when objectMaxDepth = $prop', [NaN, null, true, false, -1, 0], function(maxDepth) { - var a = {b: {c: {d: 1}}}; + var a = {b: {c: {d: {e: {f: {g: 1}}}}}}; errorHandlingConfig({objectMaxDepth: maxDepth}); var myError = testError('26', 'a is {0}', a); - expect(myError.message).toMatch(/a is {"b":{"c":{"d":1}}}/); - expect(errorHandlingConfig().objectMaxDepth).toBeNaN(); + expect(myError.message).toMatch(/a is {"b":{"c":{"d":{"e":{"f":{"g":1}}}}}}/); } ); From 6ccbfa65d60a3dc396d0cf6da21b993ad74653fd Mon Sep 17 00:00:00 2001 From: Raphael Jamet Date: Tue, 21 Feb 2017 16:19:24 +0100 Subject: [PATCH 043/719] feat($compile): lower the `xlink:href` security context for SVG's `a` and `image` elements Previously, `xlink:href` on SVG's `` and `` elements, was `$sce.RESOURCE_URL`. While this makes sense for other `xlink:href` usecases, it was an overkill for these elements. This commit lowers the `xlink:href` security context for these specific elements, treating it in the same way as `a[href]` or `img[src]` respectively. The `xlink:href` security context for other elements is not affected. BREAKING CHANGE: In the unlikely case that an app relied on RESOURCE_URL whitelisting for the purpose of binding to the `xlink:href` property of SVG's `` or `` elements and if the values do not pass the regular URL sanitization, they will break. To fix this you need to ensure that the values used for binding to the affected `xlink:href` contexts are considered safe URLs, e.g. by whitelisting them in `$compileProvider`'s `aHrefSanitizationWhitelist` (for `` elements) or `imgSrcSanitizationWhitelist` (for `` elements). Closes #15736 --- src/ng/compile.js | 11 +++++++---- test/ng/compileSpec.js | 43 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index d5e237c6adc2..92e271e44732 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1673,9 +1673,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) || - (nodeName === 'img' && key === 'src')) { + (nodeName === 'img' && key === 'src') || + (nodeName === 'image' && key === 'xlinkHref')) { // sanitize a[href] and img[src] values - this[key] = value = $$sanitizeUri(value, key === 'src'); + this[key] = value = $$sanitizeUri(value, nodeName === 'img' || nodeName === 'image'); } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) { // sanitize img[srcset] values var result = ''; @@ -3256,8 +3257,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) { return $sce.RESOURCE_URL; } - // maction[xlink:href] can source SVG. It's not limited to . - } else if (attrNormalizedName === 'xlinkHref' || + } else if ( + // Some xlink:href are okay, most aren't + (attrNormalizedName === 'xlinkHref' && (tag !== 'image' && tag !== 'a')) || + // Formaction (tag === 'form' && attrNormalizedName === 'action') || // If relative URLs can go where they are not expected to, then // all sorts of trust issues can arise. diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 59c685753f99..791c21ee062c 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -11122,23 +11122,47 @@ describe('$compile', function() { }); it('should use $$sanitizeUri when working with svg and xlink:href', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); + inject(function($compile, $rootScope) { + var elementA = $compile('')($rootScope); + var elementImage = $compile('')($rootScope); + + //both of these fail the RESOURCE_URL test, that shouldn't be run + $rootScope.testUrl = 'https://bad.example.org'; + $$sanitizeUri.and.returnValue('https://clean.example.org'); + + $rootScope.$apply(); + expect(elementA.find('a').attr('xlink:href')).toBe('https://clean.example.org'); + expect(elementImage.find('image').attr('xlink:href')).toBe('https://clean.example.org'); + // is navigational, so the second argument should be false to reach the aHref whitelist + expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'aTag' , false); + // is media inclusion, it should use the imgSrc whitelist + expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'imageTag', true); + }); + }); + + it('should use $$sanitizeUri when working with svg and xlink:href through ng-href', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); - $rootScope.testUrl = 'evilUrl'; + //both of these fail the RESOURCE_URL test, that shouldn't be run + $rootScope.testUrl = 'https://bad.example.org'; + $$sanitizeUri.and.returnValue('https://clean.example.org'); - $$sanitizeUri.and.returnValue('someSanitizedUrl'); $rootScope.$apply(); - expect(element.find('a').prop('href').baseVal).toBe('someSanitizedUrl'); + expect(element.find('a').prop('href').baseVal).toBe('https://clean.example.org'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); }); }); - it('should use $$sanitizeUri when working with svg and xlink:href', function() { + it('should use $$sanitizeUri when working with svg and xlink:href through ng-href', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); @@ -11153,6 +11177,17 @@ describe('$compile', function() { expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); }); }); + + it('should have a RESOURCE_URL context for xlink:href by default', function() { + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + $rootScope.testUrl = 'https://bad.example.org'; + + expect(function() { + $rootScope.$apply(); + }).toThrowError(/\$sce:insecurl/); + }); + }); }); describe('interpolation on HTML DOM event handler attributes onclick, onXYZ, formaction', function() { From b779091ffdfed7a1d5baf292172b09149d9ba407 Mon Sep 17 00:00:00 2001 From: diegomrsantos Date: Tue, 7 Mar 2017 20:13:25 +0100 Subject: [PATCH 044/719] docs(guide/migration): add info for 1.4 (ng)Pattern BC Breaking change was introduced in commit 0e001084ffff8674efad289d37cb16cc4e46b50a. This content being included in the migration guide is taken from the commit message of commit 0e001084ffff8674efad289d37cb16cc4e46b50a. Closes #15758 Closes #15765 --- docs/content/guide/migration.ngdoc | 83 ++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/docs/content/guide/migration.ngdoc b/docs/content/guide/migration.ngdoc index 69f164bfaf70..ea99c55e070e 100644 --- a/docs/content/guide/migration.ngdoc +++ b/docs/content/guide/migration.ngdoc @@ -1455,7 +1455,7 @@ For more info on the topic, you can take a look at this ## Migrating from 1.3 to 1.4 AngularJS 1.4 fixes major animation issues and introduces a new API for `ngCookies`. Further, there -are changes to `ngMessages`, `$compile`, `ngRepeat`, `ngOptions `and some fixes to core filters: +are changes to `ngMessages`, `$compile`, `ngRepeat`, `ngOptions`, `ngPattern`, `pattern` and some fixes to core filters: `limitTo` and `filter`. The reason for the ngAnimate refactor was to fix timing issues and to expose new APIs to allow @@ -1469,9 +1469,9 @@ to render error messages with ngMessages that are listed with a directive such a involves pulling error message data from a server and then displaying that data via the mechanics of ngMessages. Be sure to read the breaking change involved with `ngMessagesInclude` to upgrade your template code. -Other changes, such as the ordering of elements with ngRepeat and ngOptions, may also affect the behavior of your -application. And be sure to also read up on the changes to `$cookies`. The migration jump from 1.3 to 1.4 should be -relatively straightforward otherwise. +Other changes, such as the ordering of elements with ngRepeat and ngOptions and the way ngPattern and pattern directives +validate the regex, may also affect the behavior of your application. And be sure to also read up on the changes to `$cookies`. +The migration jump from 1.3 to 1.4 should be relatively straightforward otherwise. @@ -1575,7 +1575,7 @@ class based animations (animations triggered via ngClass) in order to ensure tha -### Forms (`ngMessages`, `ngOptions`, `select`) +### Forms (`ngMessages`, `ngOptions`, `select`, `ngPattern` and `pattern`) #### ngMessages The ngMessages module has also been subject to an internal refactor to allow it to be more flexible @@ -1683,6 +1683,79 @@ ngModelCtrl.$formatters.push(function(value) { }); ``` +#### ngPattern and pattern + +Due to [0e001084](https://github.com/angular/angular.js/commit/0e001084ffff8674efad289d37cb16cc4e46b50a), +The `ngPattern` and `pattern` directives will validate the regex +against the `$viewValue` of `ngModel`, i.e. the value of the model +before the $parsers are applied. Previously, the `$modelValue` +(the result of the $parsers) was validated. + +This fixes issues where `input[date]` and `input[number]` cannot +be validated because the `$viewValue` string is parsed into +`Date` and `Number` respectively (starting with Angular 1.3). +It also brings the directives in line with HTML5 constraint +validation, which validates against the input value. + +This change is unlikely to cause applications to fail, because even +in Angular 1.2, the value that was validated by pattern could have +been manipulated by the $parsers, as all validation was done +inside this pipeline. + +If you rely on the pattern being validated against the `$modelValue`, +you must create your own validator directive that overwrites +the built-in pattern validator: + +``` +.directive('patternModelOverwrite', function patternModelOverwriteDirective() { + return { + restrict: 'A', + require: '?ngModel', + priority: 1, + compile: function() { + var regexp, patternExp; + + return { + pre: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + attr.$observe('pattern', function(regex) { + /** + * The built-in directive will call our overwritten validator + * (see below). We just need to update the regex. + * The preLink fn guaranetees our observer is called first. + */ + if (isString(regex) && regex.length > 0) { + regex = new RegExp('^' + regex + '$'); + } + + if (regex && !regex.test) { + //The built-in validator will throw at this point + return; + } + + regexp = regex || undefined; + }); + + }, + post: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + regexp, patternExp = attr.ngPattern || attr.pattern; + + //The postLink fn guarantees we overwrite the built-in pattern validator + ctrl.$validators.pattern = function(value) { + return ctrl.$isEmpty(value) || + isUndefined(regexp) || + regexp.test(value); + }; + } + }; + } + }; +}); +``` + ### form From 728ce7237241774fede8e46bf810a78b61d5faef Mon Sep 17 00:00:00 2001 From: Pablo Targa Date: Thu, 23 Feb 2017 09:55:31 +0000 Subject: [PATCH 045/719] docs(ngAnimate): update staggering config for use with css animations Closes #15743 --- src/ngAnimate/module.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ngAnimate/module.js b/src/ngAnimate/module.js index 704021450add..f79b6c479819 100644 --- a/src/ngAnimate/module.js +++ b/src/ngAnimate/module.js @@ -177,6 +177,10 @@ * /* As of 1.4.4, this must always be set: it signals ngAnimate * to not accidentally inherit a delay property from another CSS class */ * transition-duration: 0s; + * + * /* if you are using animations instead of transitions you should configure as follows: + * animation-delay: 0.1s; + * animation-duration: 0s; */ * } * .my-animation.ng-enter.ng-enter-active { * /* standard transition styles */ From a59f46b37db4cc88db9721108f294f31bf71e549 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 8 Mar 2017 11:22:48 +0100 Subject: [PATCH 046/719] docs(changelog): add release notes for 1.6.3 --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41993763dd1a..689fa366bb58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ + +# 1.6.3 scriptalicious-bootstrapping (2017-03-08) + + +## Security Related +These fixes are relevant only to AngularJS apps that are part of a browser extension. + +- **Angular:** + - do not auto-bootstrap if the `src` exists but is empty + ([3536e8](https://github.com/angular/angular.js/commit/3536e83d8a085b02bd6dcec8324800b7e6c734e4)) + - do not auto bootstrap if the currentScript has been clobbered + ([95f964](https://github.com/angular/angular.js/commit/95f964b827b6f5b5aab10af54f7831316c7a9935)) + - do not auto-bootstrap if the script source is bad and inside SVG + ([c8f78a](https://github.com/angular/angular.js/commit/c8f78a8ca9debc33a6deaf951f344b8d372bf210)) + + +## Bug Fixes +- **$log:** don't parse error stacks manually outside of IE/Edge + ([64e5af](https://github.com/angular/angular.js/commit/64e5afc4786fdfd850c6bdb488a5aa2b8b077f74), + [#15590](https://github.com/angular/angular.js/issues/15590), + [#15767](https://github.com/angular/angular.js/issues/15767)) +- **$sanitize:** prevent clobbered elements from freezing the browser + ([3bb1dd](https://github.com/angular/angular.js/commit/3bb1dd5d7f7dcde6fea5a3148f8f10e92f451e9d), + [#15699](https://github.com/angular/angular.js/issues/15699)) +- **$animate:** + - reset `classNameFilter` to `null` when a disallowed RegExp is used + ([a584fb](https://github.com/angular/angular.js/commit/a584fb6e1569fc1dd85e23b251a7c126edc2dd5b), + [#14913](https://github.com/angular/angular.js/issues/14913)) + - improve detection on `ng-animate` in `classNameFilter` RegExp + ([1f1331](https://github.com/angular/angular.js/commit/1f13313f403381581e1c31c57ebfe7a96546c6e4), + [#14806](https://github.com/angular/angular.js/issues/14806)) +- **filterFilter:** don't throw if `key.charAt` is not a function + ([f27d19](https://github.com/angular/angular.js/commit/f27d19ed606bf05ba41698159ebbc5fbc195033e), + [#15644](https://github.com/angular/angular.js/issues/15644), + [#15660](https://github.com/angular/angular.js/issues/15660)) +- **select:** + - add attribute "selected" for select[multiple] + ([851367](https://github.com/angular/angular.js/commit/8513674911300b27d518383a905fde9b3f25f7ae)) + - keep original selection when using shift to add options in IE/Edge + ([97b74a](https://github.com/angular/angular.js/commit/97b74ad6fbcbc4b63e37e9eb44962d6f8de83e8b), + [#15675](https://github.com/angular/angular.js/issues/15675), + [#15676](https://github.com/angular/angular.js/issues/15676)) +- **$jsonpCallbacks:** allow `$window` to be mocked in unit tests + ([5ca0de](https://github.com/angular/angular.js/commit/5ca0de64873c32ab2f540a3226e73c4175a15c50), + [#15685](https://github.com/angular/angular.js/issues/15685), + [#15686](https://github.com/angular/angular.js/issues/15686)) + + +## New Features +- **info:** add `angularVersion` info to each module + ([1e582e](https://github.com/angular/angular.js/commit/1e582e4fa486f340150bba95927f1b26d9142de2)) +- **$injector:** add new `modules` property + ([742123](https://github.com/angular/angular.js/commit/7421235f247e5b7113345401bc5727cfbf81ddc2)) +- **Module:** add `info()` method + ([09ba69](https://github.com/angular/angular.js/commit/09ba69078de6ba52c70571b82b6205929f6facc5), + [#15225](https://github.com/angular/angular.js/issues/15225)) +- **errorHandlingConfig:** make the depth for object stringification in errors configurable + ([4a5eaf](https://github.com/angular/angular.js/commit/4a5eaf7bec85ceca8b934ebaff4d1834a1a09f57), + [#15402](https://github.com/angular/angular.js/issues/15402), + [#15433](https://github.com/angular/angular.js/issues/15433)) + + # 1.6.2 llamacorn-lovehug (2017-02-07) From 4d43ee3327fe5fbeb6bc3ac3e91426e574d8c25f Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 8 Mar 2017 12:44:24 +0100 Subject: [PATCH 047/719] docs(changelog): move bootstrap fixes to Bug Fix section --- CHANGELOG.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 689fa366bb58..5dd51d499b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,7 @@ # 1.6.3 scriptalicious-bootstrapping (2017-03-08) -## Security Related -These fixes are relevant only to AngularJS apps that are part of a browser extension. - +## Bug Fixes - **Angular:** - do not auto-bootstrap if the `src` exists but is empty ([3536e8](https://github.com/angular/angular.js/commit/3536e83d8a085b02bd6dcec8324800b7e6c734e4)) @@ -12,9 +10,6 @@ These fixes are relevant only to AngularJS apps that are part of a browser exten ([95f964](https://github.com/angular/angular.js/commit/95f964b827b6f5b5aab10af54f7831316c7a9935)) - do not auto-bootstrap if the script source is bad and inside SVG ([c8f78a](https://github.com/angular/angular.js/commit/c8f78a8ca9debc33a6deaf951f344b8d372bf210)) - - -## Bug Fixes - **$log:** don't parse error stacks manually outside of IE/Edge ([64e5af](https://github.com/angular/angular.js/commit/64e5afc4786fdfd850c6bdb488a5aa2b8b077f74), [#15590](https://github.com/angular/angular.js/issues/15590), @@ -34,7 +29,7 @@ These fixes are relevant only to AngularJS apps that are part of a browser exten [#15644](https://github.com/angular/angular.js/issues/15644), [#15660](https://github.com/angular/angular.js/issues/15660)) - **select:** - - add attribute "selected" for select[multiple] + - add attribute "selected" for `select[multiple]` ([851367](https://github.com/angular/angular.js/commit/8513674911300b27d518383a905fde9b3f25f7ae)) - keep original selection when using shift to add options in IE/Edge ([97b74a](https://github.com/angular/angular.js/commit/97b74ad6fbcbc4b63e37e9eb44962d6f8de83e8b), From 1daa4f2231a89ee88345689f001805ffffa9e7de Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 8 Mar 2017 14:33:34 +0100 Subject: [PATCH 048/719] fix(Angular): remove `angular.lowercase` and `angular.uppercase` Closes #15445 BREAKING CHANGE: The helper functions `angular.lowercase` `and angular.uppercase` have been removed. These functions have been deprecated since 1.5.0. They are internally used, but should not be exposed as they contain special locale handling (for Turkish) to maintain internal consistency regardless of user-set locale. Developers should generally use the built-ins `toLowerCase` and `toUpperCase` or `toLocaleLowerCase` and `toLocaleUpperCase` for special cases. Further, we generally discourage using the angular.x helpers in application code. --- src/Angular.js | 20 ++------------------ src/AngularPublic.js | 6 +++--- src/ngMock/angular-mocks.js | 4 ++-- src/ngSanitize/sanitize.js | 2 +- src/ngTouch/touch.js | 2 +- test/ng/compileSpec.js | 2 +- test/ng/directive/ngBindSpec.js | 20 ++++++++++---------- test/ngSanitize/directive/ngBindHtmlSpec.js | 6 +++--- 8 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 3d4d4ed9092f..af6379e22ad1 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -173,15 +173,7 @@ function isValidObjectMaxDepth(maxDepth) { } /** - * @ngdoc function - * @name angular.lowercase - * @module ng - * @kind function - * - * @deprecated - * sinceVersion="1.5.0" - * removeVersion="1.7.0" - * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead. + * @private * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. @@ -190,15 +182,7 @@ function isValidObjectMaxDepth(maxDepth) { var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; /** - * @ngdoc function - * @name angular.uppercase - * @module ng - * @kind function - * - * @deprecated - * sinceVersion="1.5.0" - * removeVersion="1.7.0" - * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead. + * @private * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 827e97399ab0..c18889911a50 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -150,8 +150,6 @@ function publishExternalAPI(angular) { 'isArray': isArray, 'version': version, 'isDate': isDate, - 'lowercase': lowercase, - 'uppercase': uppercase, 'callbacks': {$$counter: 0}, 'getTestability': getTestability, 'reloadWithDebugInfo': reloadWithDebugInfo, @@ -159,7 +157,9 @@ function publishExternalAPI(angular) { '$$csp': csp, '$$encodeUriSegment': encodeUriSegment, '$$encodeUriQuery': encodeUriQuery, - '$$stringify': stringify + '$$lowercase': lowercase, + '$$stringify': stringify, + '$$uppercase': uppercase }); angularModule = setupModuleLoader(window); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 3cc6c7bf8ad1..70adeb8f7843 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -2073,13 +2073,13 @@ function MockXhr() { var header = this.$$respHeaders[name]; if (header) return header; - name = angular.lowercase(name); + name = angular.$$lowercase(name); header = this.$$respHeaders[name]; if (header) return header; header = undefined; angular.forEach(this.$$respHeaders, function(headerVal, headerName) { - if (!header && angular.lowercase(headerName) === name) header = headerVal; + if (!header && angular.$$lowercase(headerName) === name) header = headerVal; }); return header; }; diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index ae78082f18dc..3aed567e985e 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -213,7 +213,7 @@ function $SanitizeProvider() { extend = angular.extend; forEach = angular.forEach; isDefined = angular.isDefined; - lowercase = angular.lowercase; + lowercase = angular.$$lowercase; noop = angular.noop; htmlParser = htmlParserImpl; diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js index d52d1d89ddac..2ab37acb4c93 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -29,7 +29,7 @@ ngTouch.info({ angularVersion: '"NG_VERSION_FULL"' }); ngTouch.provider('$touch', $TouchProvider); function nodeName_(element) { - return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName)); + return angular.$$lowercase(element.nodeName || (element[0] && element[0].nodeName)); } /** diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 791c21ee062c..84a5b8810f6f 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -11388,7 +11388,7 @@ describe('$compile', function() { element = $compile('')($rootScope); $rootScope.html = $sce.trustAsHtml('
hello
'); $rootScope.$digest(); - expect(angular.lowercase(element.attr('srcdoc'))).toEqual('
hello
'); + expect(lowercase(element.attr('srcdoc'))).toEqual('
hello
'); })); }); } diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index b4e80b2f62d5..1d5cba43415e 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -176,17 +176,17 @@ describe('ngBind*', function() { element = $compile('
')($rootScope); $rootScope.html = '
hello
'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('
hello
'); + expect(lowercase(element.html())).toEqual('
hello
'); })); it('should update html', inject(function($rootScope, $compile, $sce) { element = $compile('
')($rootScope); $rootScope.html = 'hello'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('hello'); + expect(lowercase(element.html())).toEqual('hello'); $rootScope.html = 'goodbye'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('goodbye'); + expect(lowercase(element.html())).toEqual('goodbye'); })); it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) { @@ -220,17 +220,17 @@ describe('ngBind*', function() { element = $compile('
')($rootScope); $rootScope.html = $sce.trustAsHtml('
hello
'); $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('
hello
'); + expect(lowercase(element.html())).toEqual('
hello
'); })); it('should update html', inject(function($rootScope, $compile, $sce) { element = $compile('
')($rootScope); $rootScope.html = $sce.trustAsHtml('hello'); $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('hello'); + expect(lowercase(element.html())).toEqual('hello'); $rootScope.html = $sce.trustAsHtml('goodbye'); $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('goodbye'); + expect(lowercase(element.html())).toEqual('goodbye'); })); it('should not cause infinite recursion for trustAsHtml object watches', @@ -243,7 +243,7 @@ describe('ngBind*', function() { return $sce.trustAsHtml('
hello
'); }; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('
hello
'); + expect(lowercase(element.html())).toEqual('
hello
'); })); it('should handle custom $sce objects', function() { @@ -266,10 +266,10 @@ describe('ngBind*', function() { var html = 'hello'; $rootScope.getHtml = function() { return $sce.trustAsHtml(html); }; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('hello'); + expect(lowercase(element.html())).toEqual('hello'); html = 'goodbye'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('goodbye'); + expect(lowercase(element.html())).toEqual('goodbye'); }); }); @@ -280,7 +280,7 @@ describe('ngBind*', function() { element = $compile('
')($rootScope); $rootScope.html = '
hello
'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('
hello
'); + expect(lowercase(element.html())).toEqual('
hello
'); })); }); }); diff --git a/test/ngSanitize/directive/ngBindHtmlSpec.js b/test/ngSanitize/directive/ngBindHtmlSpec.js index 6759c6d20fcd..74eb0f231baa 100644 --- a/test/ngSanitize/directive/ngBindHtmlSpec.js +++ b/test/ngSanitize/directive/ngBindHtmlSpec.js @@ -8,7 +8,7 @@ describe('ngBindHtml', function() { var element = $compile('
')($rootScope); $rootScope.html = '
hello
'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('
hello
'); + expect(lowercase(element.html())).toEqual('
hello
'); })); @@ -18,11 +18,11 @@ describe('ngBindHtml', function() { angular.forEach([null, undefined, ''], function(val) { $rootScope.html = 'some val'; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('some val'); + expect(lowercase(element.html())).toEqual('some val'); $rootScope.html = val; $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual(''); + expect(lowercase(element.html())).toEqual(''); }); })); }); From 28bad725b122d72fb1f62914a3798d88a6c8e7f1 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Thu, 9 Mar 2017 20:16:05 +0000 Subject: [PATCH 049/719] fix(dateFilter): correctly handle newlines in `format` string Fixes #15794 Closes #15792 --- src/ng/filter/filters.js | 4 +++- test/ng/filter/filtersSpec.js | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js index e6d4ed06a0eb..3f83895f74c2 100644 --- a/src/ng/filter/filters.js +++ b/src/ng/filter/filters.js @@ -475,7 +475,7 @@ var DATE_FORMATS = { GGGG: longEraGetter }; -var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/, NUMBER_STRING = /^-?\d+$/; /** @@ -534,6 +534,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * + * Any other characters in the `format` string will be output as-is. + * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js index bdd9b81a2417..8e3a54a0b2df 100644 --- a/test/ng/filter/filtersSpec.js +++ b/test/ng/filter/filtersSpec.js @@ -504,6 +504,10 @@ describe('filters', function() { expect(date(morning, 'yy/xxx')).toEqual('10/xxx'); }); + it('should allow newlines in format', function() { + expect(date(midnight, 'EEE\nMMM d\'\n\'yy/xxx\n')).toEqual('Fri\nSep 3\n10/xxx\n'); + }); + it('should support various iso8061 date strings with timezone as input', function() { var format = 'yyyy-MM-dd ss'; From 5a13cacf9ff33150e17745dc1e35608daf26f774 Mon Sep 17 00:00:00 2001 From: eeeqxxtg Date: Tue, 14 Mar 2017 15:07:23 +0800 Subject: [PATCH 050/719] docs(changelog): fix typo (resourceUrlWhiteList --> resourceUrlWhitelist) Closes #15809 --- CHANGELOG.md | 4 ++-- docs/content/guide/migration.ngdoc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd51d499b47..0f77b08402e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1156,7 +1156,7 @@ You configure this list in a module configuration block: ```js appModule.config(['$sceDelegateProvider', function($sceDelegateProvider) { - $sceDelegateProvider.resourceUrlWhiteList([ + $sceDelegateProvider.resourceUrlWhitelist([ // Allow same origin resource loads. 'self', // Allow JSONP calls that match this pattern @@ -2567,7 +2567,7 @@ You configure this list in a module configuration block: ```js appModule.config(['$sceDelegateProvider', function($sceDelegateProvider) { - $sceDelegateProvider.resourceUrlWhiteList([ + $sceDelegateProvider.resourceUrlWhitelist([ // Allow same origin resource loads. 'self', // Allow JSONP calls that match this pattern diff --git a/docs/content/guide/migration.ngdoc b/docs/content/guide/migration.ngdoc index ea99c55e070e..6a2c25787cff 100644 --- a/docs/content/guide/migration.ngdoc +++ b/docs/content/guide/migration.ngdoc @@ -588,7 +588,7 @@ trust a URL: ```js appModule.config(['$sceDelegateProvider', function($sceDelegateProvider) { - $sceDelegateProvider.resourceUrlWhiteList([ + $sceDelegateProvider.resourceUrlWhitelist([ // Allow same origin resource loads. 'self', // Allow JSONP calls that match this pattern From 11d9ad1eb25eaf5967195e424108207427835d50 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 14 Mar 2017 11:48:54 +0100 Subject: [PATCH 051/719] fix(ngTouch): remove ngClick override, `$touchProvider`, and `$touch` Closes #15761 Closes #15755 BREAKING CHANGE: The `ngClick` directive from the ngTouch module has been removed, and with it the corresponding `$touchProvider` and `$touch` service. If you have included ngTouch v1.5.0 or higher in your application, and have not changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch` service, then there are no migration steps for your code. Otherwise you must remove references to the provider and service. The `ngClick` override directive had been deprecated and by default disabled since v1.5.0, because of buggy behavior in edge cases, and a general trend to avoid special touch based overrides of click events. In modern browsers, it should not be necessary to use a touch override library: - Chrome, Firefox, Edge, and Safari remove the 300ms delay when `` is set. - Internet Explorer 10+, Edge, Safari, and Chrome remove the delay on elements that have the `touch-action` css property is set to `manipulation`. You can find out more in these articles: https://developers.google.com/web/updates/2013/12/300ms-tap-delay-gone-away https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_9_1.html#//apple_ref/doc/uid/TP40014305-CH10-SW8 https://blogs.msdn.microsoft.com/ie/2015/02/24/pointer-events-w3c-recommendation-interoperable-touch-and-removing-the-dreaded-300ms-tap-delay/ --- angularFiles.js | 1 - docs/content/misc/downloading.ngdoc | 2 +- src/ngTouch/directive/ngClick.js | 298 ---------- src/ngTouch/touch.js | 106 +--- test/ngTouch/directive/ngClickSpec.js | 750 -------------------------- 5 files changed, 3 insertions(+), 1154 deletions(-) delete mode 100644 src/ngTouch/directive/ngClick.js delete mode 100644 test/ngTouch/directive/ngClickSpec.js diff --git a/angularFiles.js b/angularFiles.js index 311a39139322..4c7b8cc361e4 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -146,7 +146,6 @@ var angularFiles = { 'ngTouch': [ 'src/ngTouch/touch.js', 'src/ngTouch/swipe.js', - 'src/ngTouch/directive/ngClick.js', 'src/ngTouch/directive/ngSwipe.js' ], 'ngAria': [ diff --git a/docs/content/misc/downloading.ngdoc b/docs/content/misc/downloading.ngdoc index 83806b34f835..8131946f2472 100644 --- a/docs/content/misc/downloading.ngdoc +++ b/docs/content/misc/downloading.ngdoc @@ -111,7 +111,7 @@ The set of files included in each version directory are: * __`angular-route.js`__ — Routing and deep-linking services and directives for AngularJS apps. ({@link module:ngRoute API docs}) * __`angular-sanitize.js`__ — Functionality to sanitize HTML. ({@link module:ngSanitize API docs}) - * __`angular-touch.js`__ — Touch events and other helpers for touch-enabled devices. + * __`angular-touch.js`__ — Touch events for touch-enabled devices. ({@link module:ngTouch API docs}) diff --git a/src/ngTouch/directive/ngClick.js b/src/ngTouch/directive/ngClick.js deleted file mode 100644 index 7436343b1273..000000000000 --- a/src/ngTouch/directive/ngClick.js +++ /dev/null @@ -1,298 +0,0 @@ -'use strict'; - -/* global ngTouch: false, - nodeName_: false -*/ - -/** - * @ngdoc directive - * @name ngClick - * @deprecated - * sinceVersion="v1.5.0" - * This directive is deprecated and **disabled** by default. - * The directive will receive no further support and might be removed from future releases. - * If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled} - * function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick). - * To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/) - * gives a good overview. - * - * @description - * A more powerful replacement for the default ngClick designed to be used on touchscreen - * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending - * the click event. This version handles them immediately, and then prevents the - * following click event from propagating. - * - * Requires the {@link ngTouch `ngTouch`} module to be installed. - * - * This directive can fall back to using an ordinary click event, and so works on desktop - * browsers as well as mobile. - * - * This directive also sets the CSS class `ng-click-active` while the element is being held - * down (by a mouse click or touch) so you can restyle the depressed element if you wish. - * - * @element ANY - * @param {expression} ngClick {@link guide/expression Expression} to evaluate - * upon tap. (Event object is available as `$event`) - * - * @example - - - - count: {{ count }} - - - angular.module('ngClickExample', ['ngTouch']); - - - */ - -var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement', - function($parse, $timeout, $rootElement) { - var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag. - var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers. - var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click - var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks. - - var ACTIVE_CLASS_NAME = 'ng-click-active'; - var lastPreventedTime; - var touchCoordinates; - var lastLabelClickCoordinates; - - - // TAP EVENTS AND GHOST CLICKS - // - // Why tap events? - // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're - // double-tapping, and then fire a click event. - // - // This delay sucks and makes mobile apps feel unresponsive. - // So we detect touchstart, touchcancel and touchend ourselves and determine when - // the user has tapped on something. - // - // What happens when the browser then generates a click event? - // The browser, of course, also detects the tap and fires a click after a delay. This results in - // tapping/clicking twice. We do "clickbusting" to prevent it. - // - // How does it work? - // We attach global touchstart and click handlers, that run during the capture (early) phase. - // So the sequence for a tap is: - // - global touchstart: Sets an "allowable region" at the point touched. - // - element's touchstart: Starts a touch - // (- touchcancel ends the touch, no click follows) - // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold - // too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). - // - preventGhostClick() removes the allowable region the global touchstart created. - // - The browser generates a click event. - // - The global click handler catches the click, and checks whether it was in an allowable region. - // - If preventGhostClick was called, the region will have been removed, the click is busted. - // - If the region is still there, the click proceeds normally. Therefore clicks on links and - // other elements without ngTap on them work normally. - // - // This is an ugly, terrible hack! - // Yeah, tell me about it. The alternatives are using the slow click events, or making our users - // deal with the ghost clicks, so I consider this the least of evils. Fortunately AngularJS - // encapsulates this ugly logic away from the user. - // - // Why not just put click handlers on the element? - // We do that too, just to be sure. If the tap event caused the DOM to change, - // it is possible another element is now in that position. To take account for these possibly - // distinct elements, the handlers are global and care only about coordinates. - - // Checks if the coordinates are close enough to be within the region. - function hit(x1, y1, x2, y2) { - return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD; - } - - // Checks a list of allowable regions against a click location. - // Returns true if the click should be allowed. - // Splices out the allowable region from the list after it has been used. - function checkAllowableRegions(touchCoordinates, x, y) { - for (var i = 0; i < touchCoordinates.length; i += 2) { - if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) { - touchCoordinates.splice(i, i + 2); - return true; // allowable region - } - } - return false; // No allowable region; bust it. - } - - // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick - // was called recently. - function onClick(event) { - if (Date.now() - lastPreventedTime > PREVENT_DURATION) { - return; // Too old. - } - - var touches = event.touches && event.touches.length ? event.touches : [event]; - var x = touches[0].clientX; - var y = touches[0].clientY; - // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label - // and on the input element). Depending on the exact browser, this second click we don't want - // to bust has either (0,0), negative coordinates, or coordinates equal to triggering label - // click event - if (x < 1 && y < 1) { - return; // offscreen - } - if (lastLabelClickCoordinates && - lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) { - return; // input click triggered by label click - } - // reset label click coordinates on first subsequent click - if (lastLabelClickCoordinates) { - lastLabelClickCoordinates = null; - } - // remember label click coordinates to prevent click busting of trigger click event on input - if (nodeName_(event.target) === 'label') { - lastLabelClickCoordinates = [x, y]; - } - - // Look for an allowable region containing this click. - // If we find one, that means it was created by touchstart and not removed by - // preventGhostClick, so we don't bust it. - if (checkAllowableRegions(touchCoordinates, x, y)) { - return; - } - - // If we didn't find an allowable region, bust the click. - event.stopPropagation(); - event.preventDefault(); - - // Blur focused form elements - if (event.target && event.target.blur) { - event.target.blur(); - } - } - - - // Global touchstart handler that creates an allowable region for a click event. - // This allowable region can be removed by preventGhostClick if we want to bust it. - function onTouchStart(event) { - var touches = event.touches && event.touches.length ? event.touches : [event]; - var x = touches[0].clientX; - var y = touches[0].clientY; - touchCoordinates.push(x, y); - - $timeout(function() { - // Remove the allowable region. - for (var i = 0; i < touchCoordinates.length; i += 2) { - if (touchCoordinates[i] === x && touchCoordinates[i + 1] === y) { - touchCoordinates.splice(i, i + 2); - return; - } - } - }, PREVENT_DURATION, false); - } - - // On the first call, attaches some event handlers. Then whenever it gets called, it creates a - // zone around the touchstart where clicks will get busted. - function preventGhostClick(x, y) { - if (!touchCoordinates) { - $rootElement[0].addEventListener('click', onClick, true); - $rootElement[0].addEventListener('touchstart', onTouchStart, true); - touchCoordinates = []; - } - - lastPreventedTime = Date.now(); - - checkAllowableRegions(touchCoordinates, x, y); - } - - // Actual linking function. - return function(scope, element, attr) { - var clickHandler = $parse(attr.ngClick), - tapping = false, - tapElement, // Used to blur the element after a tap. - startTime, // Used to check if the tap was held too long. - touchStartX, - touchStartY; - - function resetState() { - tapping = false; - element.removeClass(ACTIVE_CLASS_NAME); - } - - element.on('touchstart', function(event) { - tapping = true; - tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. - // Hack for Safari, which can target text nodes instead of containers. - if (tapElement.nodeType === 3) { - tapElement = tapElement.parentNode; - } - - element.addClass(ACTIVE_CLASS_NAME); - - startTime = Date.now(); - - // Use jQuery originalEvent - var originalEvent = event.originalEvent || event; - var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent]; - var e = touches[0]; - touchStartX = e.clientX; - touchStartY = e.clientY; - }); - - element.on('touchcancel', function(event) { - resetState(); - }); - - element.on('touchend', function(event) { - var diff = Date.now() - startTime; - - // Use jQuery originalEvent - var originalEvent = event.originalEvent || event; - var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ? - originalEvent.changedTouches : - ((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]); - var e = touches[0]; - var x = e.clientX; - var y = e.clientY; - var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2)); - - if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { - // Call preventGhostClick so the clickbuster will catch the corresponding click. - preventGhostClick(x, y); - - // Blur the focused element (the button, probably) before firing the callback. - // This doesn't work perfectly on Android Chrome, but seems to work elsewhere. - // I couldn't get anything to work reliably on Android Chrome. - if (tapElement) { - tapElement.blur(); - } - - if (!angular.isDefined(attr.disabled) || attr.disabled === false) { - element.triggerHandler('click', [event]); - } - } - - resetState(); - }); - - // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click - // something else nearby. - element.onclick = function(event) { }; - - // Actual click handler. - // There are three different kinds of clicks, only two of which reach this point. - // - On desktop browsers without touch events, their clicks will always come here. - // - On mobile browsers, the simulated "fast" click will call this. - // - But the browser's follow-up slow click will be "busted" before it reaches this handler. - // Therefore it's safe to use this directive on both mobile and desktop. - element.on('click', function(event, touchend) { - scope.$apply(function() { - clickHandler(scope, {$event: (touchend || event)}); - }); - }); - - element.on('mousedown', function(event) { - element.addClass(ACTIVE_CLASS_NAME); - }); - - element.on('mousemove mouseup', function(event) { - element.removeClass(ACTIVE_CLASS_NAME); - }); - - }; -}]; - diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js index 2ab37acb4c93..bdd0fffc7c94 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -1,7 +1,5 @@ 'use strict'; -/* global ngTouchClickDirectiveFactory: false */ - /** * @ngdoc module * @name ngTouch @@ -9,7 +7,7 @@ * * # ngTouch * - * The `ngTouch` module provides touch events and other helpers for touch-enabled devices. + * The `ngTouch` module provides helpers for touch-enabled devices. * The implementation is based on jQuery Mobile touch event handling * ([jquerymobile.com](http://jquerymobile.com/)). * @@ -21,111 +19,11 @@ */ // define ngTouch module -/* global -ngTouch */ +/* global ngTouch */ var ngTouch = angular.module('ngTouch', []); ngTouch.info({ angularVersion: '"NG_VERSION_FULL"' }); -ngTouch.provider('$touch', $TouchProvider); - function nodeName_(element) { return angular.$$lowercase(element.nodeName || (element[0] && element[0].nodeName)); } - -/** - * @ngdoc provider - * @name $touchProvider - * - * @description - * The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}. - */ -$TouchProvider.$inject = ['$provide', '$compileProvider']; -function $TouchProvider($provide, $compileProvider) { - - /** - * @ngdoc method - * @name $touchProvider#ngClickOverrideEnabled - * - * @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the - * current ngClickOverrideEnabled state - * @returns {*} current value if used as getter or itself (chaining) if used as setter - * - * @kind function - * - * @description - * Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled, - * the default ngClick directive will be replaced by a version that eliminates the 300ms delay for - * click events on browser for touch-devices. - * - * The default is `false`. - * - */ - var ngClickOverrideEnabled = false; - var ngClickDirectiveAdded = false; - // eslint-disable-next-line no-invalid-this - this.ngClickOverrideEnabled = function(enabled) { - if (angular.isDefined(enabled)) { - - if (enabled && !ngClickDirectiveAdded) { - ngClickDirectiveAdded = true; - - // Use this to identify the correct directive in the delegate - ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch'; - $compileProvider.directive('ngClick', ngTouchClickDirectiveFactory); - - $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { - if (ngClickOverrideEnabled) { - // drop the default ngClick directive - $delegate.shift(); - } else { - // drop the ngTouch ngClick directive if the override has been re-disabled (because - // we cannot de-register added directives) - var i = $delegate.length - 1; - while (i >= 0) { - if ($delegate[i].$$moduleName === 'ngTouch') { - $delegate.splice(i, 1); - break; - } - i--; - } - } - - return $delegate; - }]); - } - - ngClickOverrideEnabled = enabled; - return this; - } - - return ngClickOverrideEnabled; - }; - - /** - * @ngdoc service - * @name $touch - * @kind object - * - * @description - * Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method. - * - */ - // eslint-disable-next-line no-invalid-this - this.$get = function() { - return { - /** - * @ngdoc method - * @name $touch#ngClickOverrideEnabled - * - * @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider}, - * i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled. - * - * @kind function - */ - ngClickOverrideEnabled: function() { - return ngClickOverrideEnabled; - } - }; - }; - -} diff --git a/test/ngTouch/directive/ngClickSpec.js b/test/ngTouch/directive/ngClickSpec.js deleted file mode 100644 index 9e11f8e0c9c8..000000000000 --- a/test/ngTouch/directive/ngClickSpec.js +++ /dev/null @@ -1,750 +0,0 @@ -'use strict'; - -describe('ngClick (touch)', function() { - var element, time, orig_now; - - // TODO(braden): Once we have other touch-friendly browsers on CI, allow them here. - // Currently Firefox and IE refuse to fire touch events. - // Enable iPhone for manual testing. - if (!/chrome|iphone/i.test(window.navigator.userAgent)) { - return; - } - - function mockTime() { - return time; - } - - - describe('config', function() { - beforeEach(module('ngTouch')); - - it('should expose ngClickOverrideEnabled in the $touchProvider', function() { - var _$touchProvider; - - module(function($touchProvider) { - _$touchProvider = $touchProvider; - }); - - inject(function() { - expect(_$touchProvider.ngClickOverrideEnabled).toEqual(jasmine.any(Function)); - }); - }); - - - it('should return "false" for ngClickOverrideEnabled by default', function() { - var enabled; - - module(function($touchProvider) { - enabled = $touchProvider.ngClickOverrideEnabled(); - }); - - inject(function() { - expect(enabled).toBe(false); - }); - }); - - - it('should not apply the ngClick override directive by default', function() { - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toBeUndefined(); - }); - }); - }); - - describe('interaction with custom ngClick directives', function() { - - it('should not remove other ngClick directives when removing ngTouch ngClick in the decorator', function() { - // Add another ngClick before ngTouch - module(function($compileProvider) { - $compileProvider.directive('ngClick', function() { - return {}; - }); - }); - - module('ngTouch'); - - module(function($touchProvider) { - $touchProvider.ngClickOverrideEnabled(true); - $touchProvider.ngClickOverrideEnabled(false); - }); - - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toBeUndefined(); - }); - }); - - }); - - describe('directive', function() { - - beforeEach(function() { - module('ngTouch'); - module(function($touchProvider) { - $touchProvider.ngClickOverrideEnabled(true); - }); - orig_now = Date.now; - time = 0; - Date.now = mockTime; - }); - - afterEach(function() { - dealoc(element); - Date.now = orig_now; - }); - - it('should not apply the ngClick override directive if ngClickOverrideEnabled has been set to false again', function() { - module(function($touchProvider) { - // beforeEach calls this with "true" - $touchProvider.ngClickOverrideEnabled(false); - }); - - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toBeUndefined(); - }); - }); - - - it('should get called on a tap', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toEqual(true); - })); - - - it('should pass event object', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.event).toBeDefined(); - })); - - if (window.jQuery) { - it('should not unwrap a jQuery-wrapped event object on click', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - - browserTrigger(element, 'click', { - keys: [], - x: 10, - y: 10 - }); - expect($rootScope.event.originalEvent).toBeDefined(); - expect($rootScope.event.originalEvent.clientX).toBe(10); - expect($rootScope.event.originalEvent.clientY).toBe(10); - })); - - it('should not unwrap a jQuery-wrapped event object on touchstart/touchend', - inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - - expect($rootScope.event.originalEvent).toBeDefined(); - })); - } - - - it('should not click if the touch is held too long', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.count = 0; - $rootScope.$digest(); - - expect($rootScope.count).toBe(0); - - time = 10; - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 900; - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(0); - })); - - - it('should not click if the touchend is too far away', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 400, - y: 400 - }); - - expect($rootScope.tapped).toBeUndefined(); - })); - - - it('should not prevent click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchmove'); - browserTrigger(element, 'touchend',{ - keys: [], - x: 15, - y: 15 - }); - - expect($rootScope.tapped).toEqual(true); - })); - - it('should add the CSS class while the element is held down, and then remove it', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - var CSS_CLASS = 'ng-click-active'; - - expect(element.hasClass(CSS_CLASS)).toBe(false); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - expect(element.hasClass(CSS_CLASS)).toBe(true); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - expect(element.hasClass(CSS_CLASS)).toBe(false); - expect($rootScope.tapped).toBe(true); - })); - - it('should click when target element is an SVG', inject( - function($rootScope, $compile, $rootElement) { - element = $compile('')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - browserTrigger(element, 'click', {x:1, y:1}); - - expect($rootScope.tapped).toEqual(true); - })); - - describe('the clickbuster', function() { - var element1, element2; - - beforeEach(inject(function($rootElement, $document) { - $document.find('body').append($rootElement); - })); - - afterEach(inject(function($document) { - $document.find('body').empty(); - })); - - - it('should cancel the following click event', inject(function($rootScope, $compile, $rootElement, $document) { - element = $compile('
')($rootScope); - $rootElement.append(element); - - $rootScope.count = 0; - $rootScope.$digest(); - - expect($rootScope.count).toBe(0); - - // Fire touchstart at 10ms, touchend at 50ms, the click at 300ms. - time = 10; - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 50; - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(1); - - time = 100; - browserTrigger(element, 'click',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(1); - })); - - - it('should cancel the following click event even when the element has changed', inject( - function($rootScope, $compile, $rootElement) { - $rootElement.append( - '
x
' + - '
y
' - ); - $compile($rootElement)($rootScope); - - element1 = $rootElement.find('div').eq(0); - element2 = $rootElement.find('div').eq(1); - - $rootScope.count1 = 0; - $rootScope.count2 = 0; - - $rootScope.$digest(); - - expect($rootScope.count1).toBe(0); - expect($rootScope.count2).toBe(0); - - time = 10; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 50; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count1).toBe(1); - - time = 100; - browserTrigger(element2, 'click',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count1).toBe(1); - expect($rootScope.count2).toBe(0); - })); - - - it('should not cancel clicks on distant elements', inject(function($rootScope, $compile, $rootElement) { - $rootElement.append( - '
x
' + - '
y
' - ); - $compile($rootElement)($rootScope); - - element1 = $rootElement.find('div').eq(0); - element2 = $rootElement.find('div').eq(1); - - $rootScope.count1 = 0; - $rootScope.count2 = 0; - - $rootScope.$digest(); - - expect($rootScope.count1).toBe(0); - expect($rootScope.count2).toBe(0); - - time = 10; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 50; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count1).toBe(1); - - time = 90; - // Verify that it is blurred so we don't get soft-keyboard - element1[0].blur = jasmine.createSpy('blur'); - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 - }); - expect(element1[0].blur).toHaveBeenCalled(); - - expect($rootScope.count1).toBe(1); - - time = 100; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 130; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count1).toBe(2); - - // Click on other element that should go through. - time = 150; - browserTrigger(element2, 'touchstart',{ - keys: [], - x: 100, - y: 120 - }); - browserTrigger(element2, 'touchend',{ - keys: [], - x: 100, - y: 120 - }); - browserTrigger(element2, 'click',{ - keys: [], - x: 100, - y: 120 - }); - - expect($rootScope.count2).toBe(1); - - // Click event for the element that should be busted. - time = 200; - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count1).toBe(2); - expect($rootScope.count2).toBe(1); - })); - - - it('should not cancel clicks that come long after', inject(function($rootScope, $compile) { - element1 = $compile('
')($rootScope); - - $rootScope.count = 0; - - $rootScope.$digest(); - - expect($rootScope.count).toBe(0); - - time = 10; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 50; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - expect($rootScope.count).toBe(1); - - time = 2700; - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(2); - })); - - - describe('when clicking on a label immediately following a touch event', function() { - var touch = function(element, x, y) { - time = 10; - browserTrigger(element, 'touchstart',{ - keys: [], - x: x, - y: y - }); - - time = 50; - browserTrigger(element, 'touchend',{ - keys: [], - x: x, - y: y - }); - }; - - var click = function(element, x, y) { - browserTrigger(element, 'click',{ - keys: [], - x: x, - y: y - }); - }; - - var $rootScope; - var container, otherElement, input, label; - beforeEach(inject(function(_$rootScope_, $compile, $rootElement) { - $rootScope = _$rootScope_; - var container = $compile('
' + - '' + - '
')($rootScope); - $rootElement.append(container); - otherElement = container.children()[0]; - input = container.children()[1]; - label = container.children()[2]; - - $rootScope.selection = 'initial'; - - $rootScope.$digest(); - })); - - - afterEach(function() { - dealoc(label); - dealoc(input); - dealoc(otherElement); - dealoc(container); - }); - - - it('should not cancel input clicks with (0,0) coordinates', function() { - touch(otherElement, 100, 100); - - time = 500; - click(label, 10, 10); - click(input, 0, 0); - - expect($rootScope.selection).toBe('radio1'); - }); - - - it('should not cancel input clicks with negative coordinates', function() { - touch(otherElement, 100, 100); - - time = 500; - click(label, 10, 10); - click(input, -1, -1); - - expect($rootScope.selection).toBe('radio1'); - }); - - - it('should not cancel input clicks with positive coordinates identical to label click', function() { - touch(otherElement, 100, 100); - - time = 500; - click(label, 10, 10); - click(input, 10, 10); - - expect($rootScope.selection).toBe('radio1'); - }); - - - it('should cancel input clicks with positive coordinates different than label click', function() { - touch(otherElement, 100, 100); - - time = 500; - click(label, 10, 10); - click(input, 11, 11); - - expect($rootScope.selection).toBe('initial'); - }); - - - it('should blur the other element on click', function() { - var blurSpy = spyOn(otherElement, 'blur'); - touch(otherElement, 10, 10); - - time = 500; - click(label, 10, 10); - - expect(blurSpy).toHaveBeenCalled(); - }); - }); - }); - - - describe('click fallback', function() { - - it('should treat a click as a tap on desktop', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeFalsy(); - - browserTrigger(element, 'click'); - expect($rootScope.tapped).toEqual(true); - })); - - - it('should pass event object', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - - browserTrigger(element, 'click'); - expect($rootScope.event).toBeDefined(); - })); - }); - - - describe('disabled state', function() { - it('should not trigger click if ngDisabled is true', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.disabled = true; - $rootScope.$digest(); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.event).toBeUndefined(); - })); - it('should trigger click if ngDisabled is false', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.disabled = false; - $rootScope.$digest(); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.event).toBeDefined(); - })); - it('should not trigger click if regular disabled is true', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.event).toBeUndefined(); - })); - it('should not trigger click if regular disabled is present', inject(function($rootScope, $compile) { - element = $compile('')($rootScope); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.event).toBeUndefined(); - })); - it('should trigger click if regular disabled is not present', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.event).toBeDefined(); - })); - }); - - - describe('the normal click event', function() { - it('should be capturable by other handlers', inject(function($rootScope, $compile) { - var called = false; - - element = $compile('
')($rootScope); - - element.on('click', function() { - called = true; - }); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect(called).toEqual(true); - })); - }); - }); -}); From c80fa1cfe13b3b08b934e7e8ee073661f2b1ab8b Mon Sep 17 00:00:00 2001 From: Chirag Bhatia Date: Sun, 19 Feb 2017 15:56:17 +0530 Subject: [PATCH 052/719] fix($http): throw more informative error on invalid JSON response Fixes #15695 Closes #15724 --- docs/content/error/$http/baddata.ngdoc | 14 ++++++++++++++ src/ng/http.js | 7 ++++++- test/ng/httpSpec.js | 10 ++++------ 3 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 docs/content/error/$http/baddata.ngdoc diff --git a/docs/content/error/$http/baddata.ngdoc b/docs/content/error/$http/baddata.ngdoc new file mode 100644 index 000000000000..9349b76639c5 --- /dev/null +++ b/docs/content/error/$http/baddata.ngdoc @@ -0,0 +1,14 @@ +@ngdoc error +@name $http:baddata +@fullName Bad JSON Data +@description + +The default @{link ng.$http#default-transformations `transformResponse`} will try to parse the +response as JSON if the `Content-Type` header is `application/json` or the response looks like a +valid JSON-stringified object or array. +This error occurs when that data is not a valid JSON object. + +The error message should provide additional context such as the actual response. + +To resolve this error, make sure you pass valid JSON data to `transformResponse` or use an +appropriate `Content-Type` header for non-JSON data. diff --git a/src/ng/http.js b/src/ng/http.js index a4b54f68fad5..fe67455e3ca9 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -138,7 +138,12 @@ function defaultHttpResponseTransform(data, headers) { if (tempData) { var contentType = headers('Content-Type'); if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { - data = fromJson(tempData); + try { + data = fromJson(tempData); + } catch (e) { + throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' + + 'Parse error: "{1}"', data, e); + } } } } diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 1318a742125a..f03582bdde23 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -1369,17 +1369,15 @@ describe('$http', function() { } ); - it('should forward json deserialization errors to the http error handler', - function() { + it('should return JSON data with error message if JSON is invalid', function() { var errCallback = jasmine.createSpy('error'); - - $httpBackend.expect('GET', '/url').respond('abcd', {'Content-Type': 'application/json'}); - $http({method: 'GET', url: '/url'}).then(callback).catch(errCallback); + $httpBackend.expect('GET', '/url').respond('{abcd}', {'Content-Type': 'application/json'}); + $http.get('/url').then(callback).catch(errCallback); $httpBackend.flush(); expect(callback).not.toHaveBeenCalled(); expect(errCallback).toHaveBeenCalledOnce(); - expect(errCallback.calls.mostRecent().args[0]).toEqual(jasmine.any(SyntaxError)); + expect(errCallback.calls.mostRecent().args[0]).toEqualMinErr('$http', 'baddata'); }); }); From 38f8c97af74649ce224b6dd45f433cc665acfbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Mon, 6 Mar 2017 22:37:39 +0100 Subject: [PATCH 053/719] fix($compile): remove the preAssignBindingsEnabled flag Closes #15782 BREAKING CHANGE: Previously, the `$compileProvider.preAssignBindingsEnabled` flag was supported. The flag controlled whether bindings were available inside the controller constructor or only in the `$onInit` hook. The bindings are now no longer available in the constructor. To migrate your code: 1. If you haven't invoked `$compileProvider.preAssignBindingsEnabled()` you don't have to do anything to migrate. 2. If you specified `$compileProvider.preAssignBindingsEnabled(false)`, you can remove that statement - since AngularJS 1.6.0 this is the default so your app should still work even in AngularJS 1.6 after such removal. Afterwards, migrating to AngularJS 1.7.0 shouldn't require any further action. 3. If you specified `$compileProvider.preAssignBindingsEnabled(true)` you need to first migrate your code so that the flag can be flipped to `false`. The instructions on how to do that are available in the "Migrating from 1.5 to 1.6" guide: https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6 Afterwards, remove the `$compileProvider.preAssignBindingsEnabled(true)` statement. --- src/ng/compile.js | 60 +- src/ngMock/angular-mocks.js | 18 +- test/ng/compileSpec.js | 10985 ++++++++++++++--------------- test/ngMock/angular-mocksSpec.js | 143 +- 4 files changed, 5467 insertions(+), 5739 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 92e271e44732..7c89444fcb5a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1372,36 +1372,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; - /** - * @ngdoc method - * @name $compileProvider#preAssignBindingsEnabled - * - * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the - * current preAssignBindingsEnabled state - * @returns {*} current value if used as getter or itself (chaining) if used as setter - * - * @kind function - * - * @description - * Call this method to enable/disable whether directive controllers are assigned bindings before - * calling the controller's constructor. - * If enabled (true), the compiler assigns the value of each of the bindings to the - * properties of the controller object before the constructor of this object is called. - * - * If disabled (false), the compiler calls the constructor first before assigning bindings. - * - * The default value is true in AngularJS 1.5.x but will switch to false in AngularJS 1.6.x. - */ - var preAssignBindingsEnabled = false; - this.preAssignBindingsEnabled = function(enabled) { - if (isDefined(enabled)) { - preAssignBindingsEnabled = enabled; - return this; - } - return preAssignBindingsEnabled; - }; - - var TTL = 10; /** * @ngdoc method @@ -2722,33 +2692,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (preAssignBindingsEnabled) { - if (bindings) { - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } else { - controller.bindingInfo = {}; - } - - var controllerResult = controller(); - if (controllerResult !== controller.instance) { - // If the controller constructor has a return value, overwrite the instance - // from setupControllers - controller.instance = controllerResult; - $element.data('$' + controllerDirective.name + 'Controller', controllerResult); - if (controller.bindingInfo.removeWatches) { - controller.bindingInfo.removeWatches(); - } - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } - } else { - controller.instance = controller(); - $element.data('$' + controllerDirective.name + 'Controller', controller.instance); - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } - } // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy forEach(controllerDirectives, function(controllerDirective, name) { diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 70adeb8f7843..6d3337fe9d98 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -2207,11 +2207,6 @@ angular.mock.$RootElementProvider = function() { * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. * - * Depending on the value of - * {@link ng.$compileProvider#preAssignBindingsEnabled `preAssignBindingsEnabled()`}, the properties - * will be bound before or after invoking the constructor. - * - * * ## Example * * ```js @@ -2267,22 +2262,13 @@ angular.mock.$RootElementProvider = function() { * the `bindToController` feature and simplify certain kinds of tests. * @return {Object} Instance of given controller. */ -function createControllerDecorator(compileProvider) { +function createControllerDecorator() { angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { return function(expression, locals, later, ident) { if (later && typeof later === 'object') { - var preAssignBindingsEnabled = compileProvider.preAssignBindingsEnabled(); - var instantiate = $delegate(expression, locals, true, ident); - if (preAssignBindingsEnabled) { - angular.extend(instantiate.instance, later); - } - var instance = instantiate(); - if (!preAssignBindingsEnabled || instance !== instantiate.instance) { - angular.extend(instance, later); - } - + angular.extend(instance, later); return instance; } return $delegate(expression, locals, later, ident); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 84a5b8810f6f..8422fc767139 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -169,17 +169,6 @@ describe('$compile', function() { inject(); }); - it('should allow preAssignBindingsEnabled to be configured', function() { - module(function($compileProvider) { - expect($compileProvider.preAssignBindingsEnabled()).toBe(false); // the default - $compileProvider.preAssignBindingsEnabled(true); - expect($compileProvider.preAssignBindingsEnabled()).toBe(true); - $compileProvider.preAssignBindingsEnabled(false); - expect($compileProvider.preAssignBindingsEnabled()).toBe(false); - }); - inject(); - }); - it('should allow onChangesTtl to be configured', function() { module(function($compileProvider) { expect($compileProvider.onChangesTtl()).toBe(10); // the default @@ -3937,6524 +3926,6412 @@ describe('$compile', function() { }); }); - forEach([true, false], function(preAssignBindingsEnabled) { - describe((preAssignBindingsEnabled ? 'with' : 'without') + ' pre-assigned bindings', function() { - beforeEach(module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(preAssignBindingsEnabled); - })); - - describe('controller lifecycle hooks', function() { + describe('controller lifecycle hooks', function() { - describe('$onInit', function() { + describe('$onInit', function() { - it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() { + it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() { - function check() { - expect(this.element.controller('d1').id).toEqual(1); - expect(this.element.controller('d2').id).toEqual(2); - } + function check() { + expect(this.element.controller('d1').id).toEqual(1); + expect(this.element.controller('d2').id).toEqual(2); + } - function Controller1($element) { this.id = 1; this.element = $element; } - Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); + function Controller1($element) { this.id = 1; this.element = $element; } + Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); - function Controller2($element) { this.id = 2; this.element = $element; } - Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); + function Controller2($element) { this.id = 2; this.element = $element; } + Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); - angular.module('my', []) - .directive('d1', valueFn({ controller: Controller1 })) - .directive('d2', valueFn({ controller: Controller2 })); + angular.module('my', []) + .directive('d1', valueFn({ controller: Controller1 })) + .directive('d2', valueFn({ controller: Controller2 })); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce(); - expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce(); - }); - }); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce(); + expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce(); + }); + }); - it('should continue to trigger other `$onInit` hooks if one throws an error', function() { - function ThrowingController() { - this.$onInit = function() { - throw new Error('bad hook'); - }; - } - function LoggingController($log) { - this.$onInit = function() { - $log.info('onInit'); - }; - } + it('should continue to trigger other `$onInit` hooks if one throws an error', function() { + function ThrowingController() { + this.$onInit = function() { + throw new Error('bad hook'); + }; + } + function LoggingController($log) { + this.$onInit = function() { + $log.info('onInit'); + }; + } - angular.module('my', []) - .component('c1', { - controller: ThrowingController, - bindings: {'prop': '<'} - }) - .component('c2', { - controller: LoggingController, - bindings: {'prop': '<'} - }) - .config(function($exceptionHandlerProvider) { - // We need to test with the exceptionHandler not rethrowing... - $exceptionHandlerProvider.mode('log'); - }); + angular.module('my', []) + .component('c1', { + controller: ThrowingController, + bindings: {'prop': '<'} + }) + .component('c2', { + controller: LoggingController, + bindings: {'prop': '<'} + }) + .config(function($exceptionHandlerProvider) { + // We need to test with the exceptionHandler not rethrowing... + $exceptionHandlerProvider.mode('log'); + }); - module('my'); - inject(function($compile, $rootScope, $exceptionHandler, $log) { + module('my'); + inject(function($compile, $rootScope, $exceptionHandler, $log) { - // Setup the directive with bindings that will keep updating the bound value forever - element = $compile('
')($rootScope); + // Setup the directive with bindings that will keep updating the bound value forever + element = $compile('
')($rootScope); - // The first component's error should be logged - expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); + // The first component's error should be logged + expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); - // The second component's hook should still be called - expect($log.info.logs.pop()).toEqual(['onInit']); - }); - }); + // The second component's hook should still be called + expect($log.info.logs.pop()).toEqual(['onInit']); }); + }); + }); - describe('$onDestroy', function() { + describe('$onDestroy', function() { - it('should call `$onDestroy`, if provided, on the controller when its scope is destroyed', function() { + it('should call `$onDestroy`, if provided, on the controller when its scope is destroyed', function() { - function TestController() { this.count = 0; } - TestController.prototype.$onDestroy = function() { this.count++; }; + function TestController() { this.count = 0; } + TestController.prototype.$onDestroy = function() { this.count++; }; - angular.module('my', []) - .directive('d1', valueFn({ scope: true, controller: TestController })) - .directive('d2', valueFn({ scope: {}, controller: TestController })) - .directive('d3', valueFn({ controller: TestController })); + angular.module('my', []) + .directive('d1', valueFn({ scope: true, controller: TestController })) + .directive('d2', valueFn({ scope: {}, controller: TestController })) + .directive('d3', valueFn({ controller: TestController })); - module('my'); - inject(function($compile, $rootScope) { + module('my'); + inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); + element = $compile('
')($rootScope); - $rootScope.$apply('show = [true, true, true]'); - var d1Controller = element.find('d1').controller('d1'); - var d2Controller = element.find('d2').controller('d2'); - var d3Controller = element.find('d3').controller('d3'); + $rootScope.$apply('show = [true, true, true]'); + var d1Controller = element.find('d1').controller('d1'); + var d2Controller = element.find('d2').controller('d2'); + var d3Controller = element.find('d3').controller('d3'); - expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([0,0,0]); - $rootScope.$apply('show = [false, true, true]'); - expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,0,0]); - $rootScope.$apply('show = [false, false, true]'); - expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,0]); - $rootScope.$apply('show = [false, false, false]'); - expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,1]); - }); - }); + expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([0,0,0]); + $rootScope.$apply('show = [false, true, true]'); + expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,0,0]); + $rootScope.$apply('show = [false, false, true]'); + expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,0]); + $rootScope.$apply('show = [false, false, false]'); + expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,1]); + }); + }); - it('should call `$onDestroy` top-down (the same as `scope.$broadcast`)', function() { - var log = []; - function ParentController() { log.push('parent created'); } - ParentController.prototype.$onDestroy = function() { log.push('parent destroyed'); }; - function ChildController() { log.push('child created'); } - ChildController.prototype.$onDestroy = function() { log.push('child destroyed'); }; - function GrandChildController() { log.push('grand child created'); } - GrandChildController.prototype.$onDestroy = function() { log.push('grand child destroyed'); }; + it('should call `$onDestroy` top-down (the same as `scope.$broadcast`)', function() { + var log = []; + function ParentController() { log.push('parent created'); } + ParentController.prototype.$onDestroy = function() { log.push('parent destroyed'); }; + function ChildController() { log.push('child created'); } + ChildController.prototype.$onDestroy = function() { log.push('child destroyed'); }; + function GrandChildController() { log.push('grand child created'); } + GrandChildController.prototype.$onDestroy = function() { log.push('grand child destroyed'); }; - angular.module('my', []) - .directive('parent', valueFn({ scope: true, controller: ParentController })) - .directive('child', valueFn({ scope: true, controller: ChildController })) - .directive('grandChild', valueFn({ scope: true, controller: GrandChildController })); + angular.module('my', []) + .directive('parent', valueFn({ scope: true, controller: ParentController })) + .directive('child', valueFn({ scope: true, controller: ChildController })) + .directive('grandChild', valueFn({ scope: true, controller: GrandChildController })); - module('my'); - inject(function($compile, $rootScope) { + module('my'); + inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.$apply('show = true'); - expect(log).toEqual(['parent created', 'child created', 'grand child created']); - log = []; - $rootScope.$apply('show = false'); - expect(log).toEqual(['parent destroyed', 'child destroyed', 'grand child destroyed']); - }); - }); + element = $compile('')($rootScope); + $rootScope.$apply('show = true'); + expect(log).toEqual(['parent created', 'child created', 'grand child created']); + log = []; + $rootScope.$apply('show = false'); + expect(log).toEqual(['parent destroyed', 'child destroyed', 'grand child destroyed']); }); + }); + }); - describe('$postLink', function() { + describe('$postLink', function() { - it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() { + it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() { - var log = []; + var log = []; - function Controller1() { } - Controller1.prototype.$postLink = function() { log.push('d1 view init'); }; + function Controller1() { } + Controller1.prototype.$postLink = function() { log.push('d1 view init'); }; - function Controller2() { } - Controller2.prototype.$postLink = function() { log.push('d2 view init'); }; + function Controller2() { } + Controller2.prototype.$postLink = function() { log.push('d2 view init'); }; - angular.module('my', []) - .directive('d1', valueFn({ - controller: Controller1, - link: { pre: function(s, e) { log.push('d1 pre: ' + e.text()); }, post: function(s, e) { log.push('d1 post: ' + e.text()); } }, - template: '' - })) - .directive('d2', valueFn({ - controller: Controller2, - link: { pre: function(s, e) { log.push('d2 pre: ' + e.text()); }, post: function(s, e) { log.push('d2 post: ' + e.text()); } }, - template: 'loaded' - })); + angular.module('my', []) + .directive('d1', valueFn({ + controller: Controller1, + link: { pre: function(s, e) { log.push('d1 pre: ' + e.text()); }, post: function(s, e) { log.push('d1 post: ' + e.text()); } }, + template: '' + })) + .directive('d2', valueFn({ + controller: Controller2, + link: { pre: function(s, e) { log.push('d2 pre: ' + e.text()); }, post: function(s, e) { log.push('d2 post: ' + e.text()); } }, + template: 'loaded' + })); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - expect(log).toEqual([ - 'd1 pre: loaded', - 'd2 pre: loaded', - 'd2 post: loaded', - 'd2 view init', - 'd1 post: loaded', - 'd1 view init' - ]); - }); - }); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + 'd1 pre: loaded', + 'd2 pre: loaded', + 'd2 post: loaded', + 'd2 view init', + 'd1 post: loaded', + 'd1 view init' + ]); }); + }); + }); - describe('$doCheck', function() { - it('should call `$doCheck`, if provided, for each digest cycle, after $onChanges and $onInit', function() { - var log = []; + describe('$doCheck', function() { + it('should call `$doCheck`, if provided, for each digest cycle, after $onChanges and $onInit', function() { + var log = []; - function TestController() { } - TestController.prototype.$doCheck = function() { log.push('$doCheck'); }; - TestController.prototype.$onChanges = function() { log.push('$onChanges'); }; - TestController.prototype.$onInit = function() { log.push('$onInit'); }; + function TestController() { } + TestController.prototype.$doCheck = function() { log.push('$doCheck'); }; + TestController.prototype.$onChanges = function() { log.push('$onChanges'); }; + TestController.prototype.$onInit = function() { log.push('$onInit'); }; - angular.module('my', []) - .component('dcc', { - controller: TestController, - bindings: { 'prop1': '<' } - }); + angular.module('my', []) + .component('dcc', { + controller: TestController, + bindings: { 'prop1': '<' } + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - expect(log).toEqual([ - '$onChanges', - '$onInit', - '$doCheck' - ]); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + '$onChanges', + '$onInit', + '$doCheck' + ]); - // Clear log - log = []; + // Clear log + log = []; - $rootScope.$apply(); - expect(log).toEqual([ - '$doCheck', - '$doCheck' - ]); + $rootScope.$apply(); + expect(log).toEqual([ + '$doCheck', + '$doCheck' + ]); - // Clear log - log = []; + // Clear log + log = []; - $rootScope.$apply('val = 2'); - expect(log).toEqual([ - '$doCheck', - '$onChanges', - '$doCheck' - ]); - }); + $rootScope.$apply('val = 2'); + expect(log).toEqual([ + '$doCheck', + '$onChanges', + '$doCheck' + ]); + }); + }); + + it('should work if $doCheck is provided in the constructor', function() { + var log = []; + + function TestController() { + this.$doCheck = function() { log.push('$doCheck'); }; + this.$onChanges = function() { log.push('$onChanges'); }; + this.$onInit = function() { log.push('$onInit'); }; + } + + angular.module('my', []) + .component('dcc', { + controller: TestController, + bindings: { 'prop1': '<' } }); - it('should work if $doCheck is provided in the constructor', function() { - var log = []; + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + '$onChanges', + '$onInit', + '$doCheck' + ]); - function TestController() { - this.$doCheck = function() { log.push('$doCheck'); }; - this.$onChanges = function() { log.push('$onChanges'); }; - this.$onInit = function() { log.push('$onInit'); }; - } + // Clear log + log = []; - angular.module('my', []) - .component('dcc', { - controller: TestController, - bindings: { 'prop1': '<' } - }); + $rootScope.$apply(); + expect(log).toEqual([ + '$doCheck', + '$doCheck' + ]); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - expect(log).toEqual([ - '$onChanges', - '$onInit', - '$doCheck' - ]); + // Clear log + log = []; - // Clear log - log = []; + $rootScope.$apply('val = 2'); + expect(log).toEqual([ + '$doCheck', + '$onChanges', + '$doCheck' + ]); + }); + }); + }); - $rootScope.$apply(); - expect(log).toEqual([ - '$doCheck', - '$doCheck' - ]); + describe('$onChanges', function() { - // Clear log - log = []; + it('should call `$onChanges`, if provided, when a one-way (`<`) or interpolation (`@`) bindings are updated', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - $rootScope.$apply('val = 2'); - expect(log).toEqual([ - '$doCheck', - '$onChanges', - '$doCheck' - ]); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' } }); - }); - describe('$onChanges', function() { + module('my'); + inject(function($compile, $rootScope) { + // Setup a watch to indicate some complicated updated logic + $rootScope.$watch('val', function(val, oldVal) { $rootScope.val2 = val * 2; }); + // Setup the directive with two bindings + element = $compile('')($rootScope); - it('should call `$onChanges`, if provided, when a one-way (`<`) or interpolation (`@`) bindings are updated', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + expect(log).toEqual([ + { + prop1: jasmine.objectContaining({currentValue: undefined}), + prop2: jasmine.objectContaining({currentValue: undefined}), + attr: jasmine.objectContaining({currentValue: ''}) + } + ]); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' } - }); + // Clear the initial changes from the log + log = []; - module('my'); - inject(function($compile, $rootScope) { - // Setup a watch to indicate some complicated updated logic - $rootScope.$watch('val', function(val, oldVal) { $rootScope.val2 = val * 2; }); - // Setup the directive with two bindings - element = $compile('')($rootScope); - - expect(log).toEqual([ - { - prop1: jasmine.objectContaining({currentValue: undefined}), - prop2: jasmine.objectContaining({currentValue: undefined}), - attr: jasmine.objectContaining({currentValue: ''}) - } - ]); + // Update val to trigger the onChanges + $rootScope.$apply('val = 42'); - // Clear the initial changes from the log - log = []; + // Now we should have a single changes entry in the log + expect(log).toEqual([ + { + prop1: jasmine.objectContaining({currentValue: 42}), + prop2: jasmine.objectContaining({currentValue: 84}) + } + ]); - // Update val to trigger the onChanges - $rootScope.$apply('val = 42'); + // Clear the log + log = []; - // Now we should have a single changes entry in the log - expect(log).toEqual([ - { - prop1: jasmine.objectContaining({currentValue: 42}), - prop2: jasmine.objectContaining({currentValue: 84}) - } - ]); - - // Clear the log - log = []; - - // Update val to trigger the onChanges - $rootScope.$apply('val = 17'); - // Now we should have a single changes entry in the log - expect(log).toEqual([ - { - prop1: jasmine.objectContaining({previousValue: 42, currentValue: 17}), - prop2: jasmine.objectContaining({previousValue: 84, currentValue: 34}) - } - ]); - - // Clear the log - log = []; - - // Update val3 to trigger the "other" two-way binding - $rootScope.$apply('val3 = 63'); - // onChanges should not have been called - expect(log).toEqual([]); - - // Update val4 to trigger the "attr" interpolation binding - $rootScope.$apply('val4 = 22'); - // onChanges should not have been called - expect(log).toEqual([ - { - attr: jasmine.objectContaining({previousValue: '', currentValue: '22'}) - } - ]); - }); - }); + // Update val to trigger the onChanges + $rootScope.$apply('val = 17'); + // Now we should have a single changes entry in the log + expect(log).toEqual([ + { + prop1: jasmine.objectContaining({previousValue: 42, currentValue: 17}), + prop2: jasmine.objectContaining({previousValue: 84, currentValue: 34}) + } + ]); + // Clear the log + log = []; - it('should trigger `$onChanges` even if the inner value already equals the new outer value', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + // Update val3 to trigger the "other" two-way binding + $rootScope.$apply('val3 = 63'); + // onChanges should not have been called + expect(log).toEqual([]); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<' } - }); + // Update val4 to trigger the "attr" interpolation binding + $rootScope.$apply('val4 = 22'); + // onChanges should not have been called + expect(log).toEqual([ + { + attr: jasmine.objectContaining({previousValue: '', currentValue: '22'}) + } + ]); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.$apply('val = 1'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 1})}); + it('should trigger `$onChanges` even if the inner value already equals the new outer value', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - element.isolateScope().$ctrl.prop1 = 2; - $rootScope.$apply('val = 2'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: 1, currentValue: 2})}); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop1': '<' } }); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); - it('should trigger `$onChanges` for literal expressions when expression input value changes (simple value)', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + $rootScope.$apply('val = 1'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 1})}); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<' } - }); + element.isolateScope().$ctrl.prop1 = 2; + $rootScope.$apply('val = 2'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: 1, currentValue: 2})}); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.$apply('val = 1'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [1]})}); + it('should trigger `$onChanges` for literal expressions when expression input value changes (simple value)', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - $rootScope.$apply('val = 2'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [1], currentValue: [2]})}); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop1': '<' } }); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); - it('should trigger `$onChanges` for literal expressions when expression input value changes (complex value)', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + $rootScope.$apply('val = 1'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [1]})}); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<' } - }); + $rootScope.$apply('val = 2'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [1], currentValue: [2]})}); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.$apply('val = [1]'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})}); + it('should trigger `$onChanges` for literal expressions when expression input value changes (complex value)', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - $rootScope.$apply('val = [2]'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[2]]})}); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop1': '<' } }); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); - it('should trigger `$onChanges` for literal expressions when expression input value changes instances, even when equal', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + $rootScope.$apply('val = [1]'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})}); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<' } - }); + $rootScope.$apply('val = [2]'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[2]]})}); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.$apply('val = [1]'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})}); + it('should trigger `$onChanges` for literal expressions when expression input value changes instances, even when equal', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - $rootScope.$apply('val = [1]'); - expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[1]]})}); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop1': '<' } }); + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); - it('should pass the original value as `previousValue` even if there were multiple changes in a single digest', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + $rootScope.$apply('val = [1]'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})}); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop': '<' } - }); + $rootScope.$apply('val = [1]'); + expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[1]]})}); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - // We add this watch after the compilation to ensure that it will run after the binding watchers - // therefore triggering the thing that this test is hoping to enforce - $rootScope.$watch('a', function(val) { $rootScope.b = val * 2; }); + it('should pass the original value as `previousValue` even if there were multiple changes in a single digest', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: undefined})}]); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop': '<' } + }); - // Clear the initial values from the log - log = []; + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); - // Update val to trigger the onChanges - $rootScope.$apply('a = 42'); - // Now the change should have the real previous value (undefined), not the intermediate one (42) - expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: 126})}]); + // We add this watch after the compilation to ensure that it will run after the binding watchers + // therefore triggering the thing that this test is hoping to enforce + $rootScope.$watch('a', function(val) { $rootScope.b = val * 2; }); - // Clear the log - log = []; + expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: undefined})}]); - // Update val to trigger the onChanges - $rootScope.$apply('a = 7'); - // Now the change should have the real previous value (126), not the intermediate one, (91) - expect(log).toEqual([{prop: jasmine.objectContaining({previousValue: 126, currentValue: 21})}]); - }); - }); + // Clear the initial values from the log + log = []; + // Update val to trigger the onChanges + $rootScope.$apply('a = 42'); + // Now the change should have the real previous value (undefined), not the intermediate one (42) + expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: 126})}]); - it('should trigger an initial onChanges call for each binding with the `isFirstChange()` returning true', function() { - var log = []; - function TestController() { } - TestController.prototype.$onChanges = function(change) { log.push(change); }; + // Clear the log + log = []; - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop': '<', attr: '@' } - }); + // Update val to trigger the onChanges + $rootScope.$apply('a = 7'); + // Now the change should have the real previous value (126), not the intermediate one, (91) + expect(log).toEqual([{prop: jasmine.objectContaining({previousValue: 126, currentValue: 21})}]); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - $rootScope.$apply('a = 7'); - element = $compile('')($rootScope); + it('should trigger an initial onChanges call for each binding with the `isFirstChange()` returning true', function() { + var log = []; + function TestController() { } + TestController.prototype.$onChanges = function(change) { log.push(change); }; - expect(log).toEqual([ - { - prop: jasmine.objectContaining({currentValue: 7}), - attr: jasmine.objectContaining({currentValue: '7'}) - } - ]); - expect(log[0].prop.isFirstChange()).toEqual(true); - expect(log[0].attr.isFirstChange()).toEqual(true); - - log = []; - $rootScope.$apply('a = 9'); - expect(log).toEqual([ - { - prop: jasmine.objectContaining({previousValue: 7, currentValue: 9}), - attr: jasmine.objectContaining({previousValue: '7', currentValue: '9'}) - } - ]); - expect(log[0].prop.isFirstChange()).toEqual(false); - expect(log[0].attr.isFirstChange()).toEqual(false); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop': '<', attr: '@' } }); + module('my'); + inject(function($compile, $rootScope) { + + $rootScope.$apply('a = 7'); + element = $compile('')($rootScope); - it('should trigger an initial onChanges call for each binding even if the hook is defined in the constructor', function() { - var log = []; - function TestController() { - this.$onChanges = function(change) { log.push(change); }; + expect(log).toEqual([ + { + prop: jasmine.objectContaining({currentValue: 7}), + attr: jasmine.objectContaining({currentValue: '7'}) } + ]); + expect(log[0].prop.isFirstChange()).toEqual(true); + expect(log[0].attr.isFirstChange()).toEqual(true); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop': '<', attr: '@' } - }); + log = []; + $rootScope.$apply('a = 9'); + expect(log).toEqual([ + { + prop: jasmine.objectContaining({previousValue: 7, currentValue: 9}), + attr: jasmine.objectContaining({previousValue: '7', currentValue: '9'}) + } + ]); + expect(log[0].prop.isFirstChange()).toEqual(false); + expect(log[0].attr.isFirstChange()).toEqual(false); + }); + }); - module('my'); - inject(function($compile, $rootScope) { - $rootScope.$apply('a = 7'); - element = $compile('')($rootScope); - expect(log).toEqual([ - { - prop: jasmine.objectContaining({currentValue: 7}), - attr: jasmine.objectContaining({currentValue: '7'}) - } - ]); - expect(log[0].prop.isFirstChange()).toEqual(true); - expect(log[0].attr.isFirstChange()).toEqual(true); - - log = []; - $rootScope.$apply('a = 10'); - expect(log).toEqual([ - { - prop: jasmine.objectContaining({previousValue: 7, currentValue: 10}), - attr: jasmine.objectContaining({previousValue: '7', currentValue: '10'}) - } - ]); - expect(log[0].prop.isFirstChange()).toEqual(false); - expect(log[0].attr.isFirstChange()).toEqual(false); - }); + it('should trigger an initial onChanges call for each binding even if the hook is defined in the constructor', function() { + var log = []; + function TestController() { + this.$onChanges = function(change) { log.push(change); }; + } + + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop': '<', attr: '@' } }); - it('should clean up `@`-binding observers when re-assigning bindings', function() { - var constructorSpy = jasmine.createSpy('constructor'); - var prototypeSpy = jasmine.createSpy('prototype'); + module('my'); + inject(function($compile, $rootScope) { + $rootScope.$apply('a = 7'); + element = $compile('')($rootScope); - function TestController() { - return {$onChanges: constructorSpy}; + expect(log).toEqual([ + { + prop: jasmine.objectContaining({currentValue: 7}), + attr: jasmine.objectContaining({currentValue: '7'}) } - TestController.prototype.$onChanges = prototypeSpy; + ]); + expect(log[0].prop.isFirstChange()).toEqual(true); + expect(log[0].attr.isFirstChange()).toEqual(true); - module(function($compileProvider) { - $compileProvider.component('test', { - bindings: {attr: '@'}, - controller: TestController - }); - }); + log = []; + $rootScope.$apply('a = 10'); + expect(log).toEqual([ + { + prop: jasmine.objectContaining({previousValue: 7, currentValue: 10}), + attr: jasmine.objectContaining({previousValue: '7', currentValue: '10'}) + } + ]); + expect(log[0].prop.isFirstChange()).toEqual(false); + expect(log[0].attr.isFirstChange()).toEqual(false); + }); + }); - inject(function($compile, $rootScope) { - var template = ''; - $rootScope.a = 'foo'; + it('should clean up `@`-binding observers when re-assigning bindings', function() { + var constructorSpy = jasmine.createSpy('constructor'); + var prototypeSpy = jasmine.createSpy('prototype'); - element = $compile(template)($rootScope); - $rootScope.$digest(); - expect(constructorSpy).toHaveBeenCalled(); - expect(prototypeSpy).not.toHaveBeenCalled(); + function TestController() { + return {$onChanges: constructorSpy}; + } + TestController.prototype.$onChanges = prototypeSpy; - constructorSpy.calls.reset(); - $rootScope.$apply('a = "bar"'); - expect(constructorSpy).toHaveBeenCalled(); - expect(prototypeSpy).not.toHaveBeenCalled(); - }); + module(function($compileProvider) { + $compileProvider.component('test', { + bindings: {attr: '@'}, + controller: TestController }); + }); - it('should not call `$onChanges` twice even when the initial value is `NaN`', function() { - var onChangesSpy = jasmine.createSpy('$onChanges'); - - module(function($compileProvider) { - $compileProvider.component('test', { - bindings: {prop: '<', attr: '@'}, - controller: function TestController() { - this.$onChanges = onChangesSpy; - } - }); - }); - - inject(function($compile, $rootScope) { - var template = '' + - ''; - $rootScope.a = 'foo'; - $rootScope.b = NaN; + inject(function($compile, $rootScope) { + var template = ''; + $rootScope.a = 'foo'; - element = $compile(template)($rootScope); - $rootScope.$digest(); + element = $compile(template)($rootScope); + $rootScope.$digest(); + expect(constructorSpy).toHaveBeenCalled(); + expect(prototypeSpy).not.toHaveBeenCalled(); - expect(onChangesSpy).toHaveBeenCalledTimes(2); - expect(onChangesSpy.calls.argsFor(0)[0]).toEqual({ - prop: jasmine.objectContaining({currentValue: 'foo'}), - attr: jasmine.objectContaining({currentValue: 'foo'}) - }); - expect(onChangesSpy.calls.argsFor(1)[0]).toEqual({ - prop: jasmine.objectContaining({currentValue: NaN}), - attr: jasmine.objectContaining({currentValue: 'NaN'}) - }); + constructorSpy.calls.reset(); + $rootScope.$apply('a = "bar"'); + expect(constructorSpy).toHaveBeenCalled(); + expect(prototypeSpy).not.toHaveBeenCalled(); + }); + }); - onChangesSpy.calls.reset(); - $rootScope.$apply('a = "bar"; b = 42'); + it('should not call `$onChanges` twice even when the initial value is `NaN`', function() { + var onChangesSpy = jasmine.createSpy('$onChanges'); - expect(onChangesSpy).toHaveBeenCalledTimes(2); - expect(onChangesSpy.calls.argsFor(0)[0]).toEqual({ - prop: jasmine.objectContaining({previousValue: 'foo', currentValue: 'bar'}), - attr: jasmine.objectContaining({previousValue: 'foo', currentValue: 'bar'}) - }); - expect(onChangesSpy.calls.argsFor(1)[0]).toEqual({ - prop: jasmine.objectContaining({previousValue: NaN, currentValue: 42}), - attr: jasmine.objectContaining({previousValue: 'NaN', currentValue: '42'}) - }); - }); + module(function($compileProvider) { + $compileProvider.component('test', { + bindings: {prop: '<', attr: '@'}, + controller: function TestController() { + this.$onChanges = onChangesSpy; + } }); + }); + inject(function($compile, $rootScope) { + var template = '' + + ''; + $rootScope.a = 'foo'; + $rootScope.b = NaN; - it('should only trigger one extra digest however many controllers have changes', function() { - var log = []; - function TestController1() { } - TestController1.prototype.$onChanges = function(change) { log.push(['TestController1', change]); }; - function TestController2() { } - TestController2.prototype.$onChanges = function(change) { log.push(['TestController2', change]); }; - - angular.module('my', []) - .component('c1', { - controller: TestController1, - bindings: {'prop': '<'} - }) - .component('c2', { - controller: TestController2, - bindings: {'prop': '<'} - }); + element = $compile(template)($rootScope); + $rootScope.$digest(); - module('my'); - inject(function($compile, $rootScope) { + expect(onChangesSpy).toHaveBeenCalledTimes(2); + expect(onChangesSpy.calls.argsFor(0)[0]).toEqual({ + prop: jasmine.objectContaining({currentValue: 'foo'}), + attr: jasmine.objectContaining({currentValue: 'foo'}) + }); + expect(onChangesSpy.calls.argsFor(1)[0]).toEqual({ + prop: jasmine.objectContaining({currentValue: NaN}), + attr: jasmine.objectContaining({currentValue: 'NaN'}) + }); - // Create a watcher to count the number of digest cycles - var watchCount = 0; - $rootScope.$watch(function() { watchCount++; }); + onChangesSpy.calls.reset(); + $rootScope.$apply('a = "bar"; b = 42'); - // Setup two sibling components with bindings that will change - element = $compile('
')($rootScope); + expect(onChangesSpy).toHaveBeenCalledTimes(2); + expect(onChangesSpy.calls.argsFor(0)[0]).toEqual({ + prop: jasmine.objectContaining({previousValue: 'foo', currentValue: 'bar'}), + attr: jasmine.objectContaining({previousValue: 'foo', currentValue: 'bar'}) + }); + expect(onChangesSpy.calls.argsFor(1)[0]).toEqual({ + prop: jasmine.objectContaining({previousValue: NaN, currentValue: 42}), + attr: jasmine.objectContaining({previousValue: 'NaN', currentValue: '42'}) + }); + }); + }); - // Clear out initial changes - log = []; - // Update val to trigger the onChanges - $rootScope.$apply('val1 = 42; val2 = 17'); + it('should only trigger one extra digest however many controllers have changes', function() { + var log = []; + function TestController1() { } + TestController1.prototype.$onChanges = function(change) { log.push(['TestController1', change]); }; + function TestController2() { } + TestController2.prototype.$onChanges = function(change) { log.push(['TestController2', change]); }; - expect(log).toEqual([ - ['TestController1', {prop: jasmine.objectContaining({currentValue: 42})}], - ['TestController2', {prop: jasmine.objectContaining({currentValue: 17})}] - ]); - // A single apply should only trigger three turns of the digest loop - expect(watchCount).toEqual(3); - }); + angular.module('my', []) + .component('c1', { + controller: TestController1, + bindings: {'prop': '<'} + }) + .component('c2', { + controller: TestController2, + bindings: {'prop': '<'} }); + module('my'); + inject(function($compile, $rootScope) { - it('should cope with changes occurring inside `$onChanges()` hooks', function() { - var log = []; - function OuterController() {} - OuterController.prototype.$onChanges = function(change) { - log.push(['OuterController', change]); - // Make a change to the inner component - this.b = this.prop1 * 2; - }; + // Create a watcher to count the number of digest cycles + var watchCount = 0; + $rootScope.$watch(function() { watchCount++; }); - function InnerController() { } - InnerController.prototype.$onChanges = function(change) { log.push(['InnerController', change]); }; + // Setup two sibling components with bindings that will change + element = $compile('
')($rootScope); - angular.module('my', []) - .component('outer', { - controller: OuterController, - bindings: {'prop1': '<'}, - template: '' - }) - .component('inner', { - controller: InnerController, - bindings: {'prop2': '<'} - }); + // Clear out initial changes + log = []; - module('my'); - inject(function($compile, $rootScope) { + // Update val to trigger the onChanges + $rootScope.$apply('val1 = 42; val2 = 17'); + + expect(log).toEqual([ + ['TestController1', {prop: jasmine.objectContaining({currentValue: 42})}], + ['TestController2', {prop: jasmine.objectContaining({currentValue: 17})}] + ]); + // A single apply should only trigger three turns of the digest loop + expect(watchCount).toEqual(3); + }); + }); - // Setup the directive with two bindings - element = $compile('')($rootScope); - // Clear out initial changes - log = []; + it('should cope with changes occurring inside `$onChanges()` hooks', function() { + var log = []; + function OuterController() {} + OuterController.prototype.$onChanges = function(change) { + log.push(['OuterController', change]); + // Make a change to the inner component + this.b = this.prop1 * 2; + }; - // Update val to trigger the onChanges - $rootScope.$apply('a = 42'); + function InnerController() { } + InnerController.prototype.$onChanges = function(change) { log.push(['InnerController', change]); }; - expect(log).toEqual([ - ['OuterController', {prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 42})}], - ['InnerController', {prop2: jasmine.objectContaining({previousValue: NaN, currentValue: 84})}] - ]); - }); + angular.module('my', []) + .component('outer', { + controller: OuterController, + bindings: {'prop1': '<'}, + template: '' + }) + .component('inner', { + controller: InnerController, + bindings: {'prop2': '<'} }); + module('my'); + inject(function($compile, $rootScope) { + + // Setup the directive with two bindings + element = $compile('')($rootScope); - it('should throw an error if `$onChanges()` hooks are not stable', function() { - function TestController() {} - TestController.prototype.$onChanges = function(change) { - this.onChange(); - }; + // Clear out initial changes + log = []; - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: {'prop': '<', onChange: '&'} - }); + // Update val to trigger the onChanges + $rootScope.$apply('a = 42'); - module('my'); - inject(function($compile, $rootScope) { + expect(log).toEqual([ + ['OuterController', {prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 42})}], + ['InnerController', {prop2: jasmine.objectContaining({previousValue: NaN, currentValue: 84})}] + ]); + }); + }); - // Setup the directive with bindings that will keep updating the bound value forever - element = $compile('')($rootScope); - // Update val to trigger the unstable onChanges, which will result in an error - expect(function() { - $rootScope.$apply('a = 42'); - }).toThrowMinErr('$compile', 'infchng'); + it('should throw an error if `$onChanges()` hooks are not stable', function() { + function TestController() {} + TestController.prototype.$onChanges = function(change) { + this.onChange(); + }; - dealoc(element); - element = $compile('')($rootScope); - $rootScope.$apply('b = 24'); - $rootScope.$apply('b = 48'); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: {'prop': '<', onChange: '&'} }); + module('my'); + inject(function($compile, $rootScope) { - it('should log an error if `$onChanges()` hooks are not stable', function() { - function TestController() {} - TestController.prototype.$onChanges = function(change) { - this.onChange(); - }; + // Setup the directive with bindings that will keep updating the bound value forever + element = $compile('')($rootScope); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: {'prop': '<', onChange: '&'} - }) - .config(function($exceptionHandlerProvider) { - // We need to test with the exceptionHandler not rethrowing... - $exceptionHandlerProvider.mode('log'); - }); + // Update val to trigger the unstable onChanges, which will result in an error + expect(function() { + $rootScope.$apply('a = 42'); + }).toThrowMinErr('$compile', 'infchng'); + + dealoc(element); + element = $compile('')($rootScope); + $rootScope.$apply('b = 24'); + $rootScope.$apply('b = 48'); + }); + }); - module('my'); - inject(function($compile, $rootScope, $exceptionHandler) { - // Setup the directive with bindings that will keep updating the bound value forever - element = $compile('')($rootScope); + it('should log an error if `$onChanges()` hooks are not stable', function() { + function TestController() {} + TestController.prototype.$onChanges = function(change) { + this.onChange(); + }; - // Update val to trigger the unstable onChanges, which will result in an error - $rootScope.$apply('a = 42'); - expect($exceptionHandler.errors.length).toEqual(1); - expect($exceptionHandler.errors[0]). - toEqualMinErr('$compile', 'infchng', '10 $onChanges() iterations reached.'); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: {'prop': '<', onChange: '&'} + }) + .config(function($exceptionHandlerProvider) { + // We need to test with the exceptionHandler not rethrowing... + $exceptionHandlerProvider.mode('log'); }); + module('my'); + inject(function($compile, $rootScope, $exceptionHandler) { - it('should continue to trigger other `$onChanges` hooks if one throws an error', function() { - function ThrowingController() { - this.$onChanges = function(change) { - throw new Error('bad hook'); - }; - } - function LoggingController($log) { - this.$onChanges = function(change) { - $log.info('onChange'); - }; - } + // Setup the directive with bindings that will keep updating the bound value forever + element = $compile('')($rootScope); - angular.module('my', []) - .component('c1', { - controller: ThrowingController, - bindings: {'prop': '<'} - }) - .component('c2', { - controller: LoggingController, - bindings: {'prop': '<'} - }) - .config(function($exceptionHandlerProvider) { - // We need to test with the exceptionHandler not rethrowing... - $exceptionHandlerProvider.mode('log'); - }); + // Update val to trigger the unstable onChanges, which will result in an error + $rootScope.$apply('a = 42'); + expect($exceptionHandler.errors.length).toEqual(1); + expect($exceptionHandler.errors[0]). + toEqualMinErr('$compile', 'infchng', '10 $onChanges() iterations reached.'); + }); + }); - module('my'); - inject(function($compile, $rootScope, $exceptionHandler, $log) { - // Setup the directive with bindings that will keep updating the bound value forever - element = $compile('
')($rootScope); + it('should continue to trigger other `$onChanges` hooks if one throws an error', function() { + function ThrowingController() { + this.$onChanges = function(change) { + throw new Error('bad hook'); + }; + } + function LoggingController($log) { + this.$onChanges = function(change) { + $log.info('onChange'); + }; + } - // The first component's error should be logged - expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); + angular.module('my', []) + .component('c1', { + controller: ThrowingController, + bindings: {'prop': '<'} + }) + .component('c2', { + controller: LoggingController, + bindings: {'prop': '<'} + }) + .config(function($exceptionHandlerProvider) { + // We need to test with the exceptionHandler not rethrowing... + $exceptionHandlerProvider.mode('log'); + }); - // The second component's changes should still be called - expect($log.info.logs.pop()).toEqual(['onChange']); + module('my'); + inject(function($compile, $rootScope, $exceptionHandler, $log) { - $rootScope.$apply('a = 42'); + // Setup the directive with bindings that will keep updating the bound value forever + element = $compile('
')($rootScope); - // The first component's error should be logged - var errors = $exceptionHandler.errors.pop(); - expect(errors[0]).toEqual(new Error('bad hook')); + // The first component's error should be logged + expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); - // The second component's changes should still be called - expect($log.info.logs.pop()).toEqual(['onChange']); - }); - }); + // The second component's changes should still be called + expect($log.info.logs.pop()).toEqual(['onChange']); + $rootScope.$apply('a = 42'); - it('should collect up all `$onChanges` errors into one throw', function() { - function ThrowingController() { - this.$onChanges = function(change) { - throw new Error('bad hook: ' + this.prop); - }; - } + // The first component's error should be logged + var errors = $exceptionHandler.errors.pop(); + expect(errors[0]).toEqual(new Error('bad hook')); - angular.module('my', []) - .component('c1', { - controller: ThrowingController, - bindings: {'prop': '<'} - }) - .config(function($exceptionHandlerProvider) { - // We need to test with the exceptionHandler not rethrowing... - $exceptionHandlerProvider.mode('log'); - }); + // The second component's changes should still be called + expect($log.info.logs.pop()).toEqual(['onChange']); + }); + }); + + + it('should collect up all `$onChanges` errors into one throw', function() { + function ThrowingController() { + this.$onChanges = function(change) { + throw new Error('bad hook: ' + this.prop); + }; + } + + angular.module('my', []) + .component('c1', { + controller: ThrowingController, + bindings: {'prop': '<'} + }) + .config(function($exceptionHandlerProvider) { + // We need to test with the exceptionHandler not rethrowing... + $exceptionHandlerProvider.mode('log'); + }); - module('my'); - inject(function($compile, $rootScope, $exceptionHandler, $log) { + module('my'); + inject(function($compile, $rootScope, $exceptionHandler, $log) { - // Setup the directive with bindings that will keep updating the bound value forever - element = $compile('
')($rootScope); + // Setup the directive with bindings that will keep updating the bound value forever + element = $compile('
')($rootScope); - // Both component's errors should be logged - expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: NaN')); - expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: undefined')); + // Both component's errors should be logged + expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: NaN')); + expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: undefined')); - $rootScope.$apply('a = 42'); + $rootScope.$apply('a = 42'); - // Both component's error should be logged - var errors = $exceptionHandler.errors.pop(); - expect(errors.pop()).toEqual(new Error('bad hook: 84')); - expect(errors.pop()).toEqual(new Error('bad hook: 42')); - }); - }); + // Both component's error should be logged + var errors = $exceptionHandler.errors.pop(); + expect(errors.pop()).toEqual(new Error('bad hook: 84')); + expect(errors.pop()).toEqual(new Error('bad hook: 42')); }); }); + }); + }); - describe('isolated locals', function() { - var componentScope, regularScope; - - beforeEach(module(function() { - directive('myComponent', function() { - return { - scope: { - attr: '@', - attrAlias: '@attr', - $attrAlias: '@$attr$', - ref: '=', - refAlias: '= ref', - $refAlias: '= $ref$', - reference: '=', - optref: '=?', - optrefAlias: '=? optref', - $optrefAlias: '=? $optref$', - optreference: '=?', - colref: '=*', - colrefAlias: '=* colref', - $colrefAlias: '=* $colref$', - owRef: '<', - owRefAlias: '< owRef', - $owRefAlias: '< $owRef$', - owOptref: '
'); - $rootScope.$apply(function() { - $rootScope.value = 'from-parent'; - }); - expect(element.find('input').val()).toBe('from-parent'); - expect(componentScope).not.toBe(regularScope); - expect(componentScope.$parent).toBe(regularScope); - })); + it('should give other directives the parent scope', inject(function($rootScope) { + compile('
'); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; + }); + expect(element.find('input').val()).toBe('from-parent'); + expect(componentScope).not.toBe(regularScope); + expect(componentScope.$parent).toBe(regularScope); + })); - it('should not give the isolate scope to other directive template', function() { - module(function() { - directive('otherTplDir', function() { - return { - template: 'value: {{value}}' - }; - }); - }); - inject(function($rootScope) { - compile('
'); + it('should not give the isolate scope to other directive template', function() { + module(function() { + directive('otherTplDir', function() { + return { + template: 'value: {{value}}' + }; + }); + }); - $rootScope.$apply(function() { - $rootScope.value = 'from-parent'; - }); + inject(function($rootScope) { + compile('
'); - expect(element.html()).toBe('value: from-parent'); - }); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; }); + expect(element.html()).toBe('value: from-parent'); + }); + }); - it('should not give the isolate scope to other directive template (with templateUrl)', function() { - module(function() { - directive('otherTplDir', function() { - return { - templateUrl: 'other.html' - }; - }); - }); - inject(function($rootScope, $templateCache) { - $templateCache.put('other.html', 'value: {{value}}'); - compile('
'); + it('should not give the isolate scope to other directive template (with templateUrl)', function() { + module(function() { + directive('otherTplDir', function() { + return { + templateUrl: 'other.html' + }; + }); + }); - $rootScope.$apply(function() { - $rootScope.value = 'from-parent'; - }); + inject(function($rootScope, $templateCache) { + $templateCache.put('other.html', 'value: {{value}}'); + compile('
'); - expect(element.html()).toBe('value: from-parent'); - }); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; }); + expect(element.html()).toBe('value: from-parent'); + }); + }); - it('should not give the isolate scope to regular child elements', function() { - inject(function($rootScope) { - compile('
value: {{value}}
'); - $rootScope.$apply(function() { - $rootScope.value = 'from-parent'; - }); + it('should not give the isolate scope to regular child elements', function() { + inject(function($rootScope) { + compile('
value: {{value}}
'); - expect(element.html()).toBe('value: from-parent'); - }); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; }); + expect(element.html()).toBe('value: from-parent'); + }); + }); + - it('should update parent scope when "="-bound NaN changes', inject(function($compile, $rootScope) { - $rootScope.num = NaN; - compile('
'); - var isolateScope = element.isolateScope(); - expect(isolateScope.reference).toBeNaN(); + it('should update parent scope when "="-bound NaN changes', inject(function($compile, $rootScope) { + $rootScope.num = NaN; + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.reference).toBeNaN(); - isolateScope.$apply(function(scope) { scope.reference = 64; }); - expect($rootScope.num).toBe(64); - })); + isolateScope.$apply(function(scope) { scope.reference = 64; }); + expect($rootScope.num).toBe(64); + })); - it('should update isolate scope when "="-bound NaN changes', inject(function($compile, $rootScope) { - $rootScope.num = NaN; - compile('
'); - var isolateScope = element.isolateScope(); - expect(isolateScope.reference).toBeNaN(); + it('should update isolate scope when "="-bound NaN changes', inject(function($compile, $rootScope) { + $rootScope.num = NaN; + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.reference).toBeNaN(); - $rootScope.$apply(function(scope) { scope.num = 64; }); - expect(isolateScope.reference).toBe(64); + $rootScope.$apply(function(scope) { scope.num = 64; }); + expect(isolateScope.reference).toBe(64); + })); + + + it('should be able to bind attribute names which are present in Object.prototype', function() { + module(function() { + directive('inProtoAttr', valueFn({ + scope: { + 'constructor': '@', + 'toString': '&', + + // Spidermonkey extension, may be obsolete in the future + 'watch': '=' + } })); + }); + inject(function($rootScope) { + expect(function() { + compile('
'); + }).not.toThrow(); + var isolateScope = element.isolateScope(); + + expect(typeof isolateScope.constructor).toBe('string'); + expect(isArray(isolateScope.watch)).toBe(true); + expect(typeof isolateScope.toString).toBe('function'); + expect($rootScope.value).toBeUndefined(); + isolateScope.toString(); + expect($rootScope.value).toBe(true); + }); + }); + it('should be able to interpolate attribute names which are present in Object.prototype', function() { + var attrs; + module(function() { + directive('attrExposer', valueFn({ + link: function($scope, $element, $attrs) { + attrs = $attrs; + } + })); + }); + inject(function($compile, $rootScope) { + $compile('
')($rootScope); + $rootScope.$apply(); + expect(attrs.toString).toBe('2'); + }); + }); - it('should be able to bind attribute names which are present in Object.prototype', function() { - module(function() { - directive('inProtoAttr', valueFn({ - scope: { - 'constructor': '@', - 'toString': '&', - // Spidermonkey extension, may be obsolete in the future - 'watch': '=' - } - })); - }); - inject(function($rootScope) { - expect(function() { - compile('
'); - }).not.toThrow(); - var isolateScope = element.isolateScope(); + it('should not initialize scope value if optional expression binding is not passed', inject(function($compile) { + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.optExpr).toBeUndefined(); + })); - expect(typeof isolateScope.constructor).toBe('string'); - expect(isArray(isolateScope.watch)).toBe(true); - expect(typeof isolateScope.toString).toBe('function'); - expect($rootScope.value).toBeUndefined(); - isolateScope.toString(); - expect($rootScope.value).toBe(true); - }); - }); - it('should be able to interpolate attribute names which are present in Object.prototype', function() { - var attrs; - module(function() { - directive('attrExposer', valueFn({ - link: function($scope, $element, $attrs) { - attrs = $attrs; - } - })); - }); - inject(function($compile, $rootScope) { - $compile('
')($rootScope); - $rootScope.$apply(); - expect(attrs.toString).toBe('2'); - }); - }); + it('should not initialize scope value if optional expression binding with Object.prototype name is not passed', inject(function($compile) { + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.constructor).toBe($rootScope.constructor); + })); - it('should not initialize scope value if optional expression binding is not passed', inject(function($compile) { - compile('
'); - var isolateScope = element.isolateScope(); - expect(isolateScope.optExpr).toBeUndefined(); - })); + it('should initialize scope value if optional expression binding is passed', inject(function($compile) { + compile('
'); + var isolateScope = element.isolateScope(); + expect(typeof isolateScope.optExpr).toBe('function'); + expect(isolateScope.optExpr()).toBe('did!'); + expect($rootScope.value).toBe('did!'); + })); - it('should not initialize scope value if optional expression binding with Object.prototype name is not passed', inject(function($compile) { - compile('
'); - var isolateScope = element.isolateScope(); - expect(isolateScope.constructor).toBe($rootScope.constructor); - })); + it('should initialize scope value if optional expression binding with Object.prototype name is passed', inject(function($compile) { + compile('
'); + var isolateScope = element.isolateScope(); + expect(typeof isolateScope.constructor).toBe('function'); + expect(isolateScope.constructor()).toBe('did!'); + expect($rootScope.value).toBe('did!'); + })); - it('should initialize scope value if optional expression binding is passed', inject(function($compile) { - compile('
'); - var isolateScope = element.isolateScope(); - expect(typeof isolateScope.optExpr).toBe('function'); - expect(isolateScope.optExpr()).toBe('did!'); - expect($rootScope.value).toBe('did!'); + it('should not overwrite @-bound property each digest when not present', function() { + module(function($compileProvider) { + $compileProvider.directive('testDir', valueFn({ + scope: {prop: '@'}, + controller: function($scope) { + $scope.prop = $scope.prop || 'default'; + this.getProp = function() { + return $scope.prop; + }; + }, + controllerAs: 'ctrl', + template: '

' })); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + var scope = element.isolateScope(); + expect(scope.ctrl.getProp()).toBe('default'); + + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('default'); + }); + }); - it('should initialize scope value if optional expression binding with Object.prototype name is passed', inject(function($compile) { - compile('
'); - var isolateScope = element.isolateScope(); - expect(typeof isolateScope.constructor).toBe('function'); - expect(isolateScope.constructor()).toBe('did!'); - expect($rootScope.value).toBe('did!'); + it('should ignore optional "="-bound property if value is the empty string', function() { + module(function($compileProvider) { + $compileProvider.directive('testDir', valueFn({ + scope: {prop: '=?'}, + controller: function($scope) { + $scope.prop = $scope.prop || 'default'; + this.getProp = function() { + return $scope.prop; + }; + }, + controllerAs: 'ctrl', + template: '

' })); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + var scope = element.isolateScope(); + expect(scope.ctrl.getProp()).toBe('default'); + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('default'); + scope.prop = 'foop'; + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('foop'); + }); + }); - it('should not overwrite @-bound property each digest when not present', function() { - module(function($compileProvider) { - $compileProvider.directive('testDir', valueFn({ - scope: {prop: '@'}, - controller: function($scope) { - $scope.prop = $scope.prop || 'default'; - this.getProp = function() { - return $scope.prop; - }; - }, - controllerAs: 'ctrl', - template: '

' - })); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - var scope = element.isolateScope(); - expect(scope.ctrl.getProp()).toBe('default'); + describe('bind-once', function() { - $rootScope.$digest(); - expect(scope.ctrl.getProp()).toBe('default'); + function countWatches(scope) { + var result = 0; + while (scope !== null) { + result += (scope.$$watchers && scope.$$watchers.length) || 0; + result += countWatches(scope.$$childHead); + scope = scope.$$nextSibling; + } + return result; + } + + it('should be possible to one-time bind a parameter on a component with a template', function() { + module(function() { + directive('otherTplDir', function() { + return { + scope: {param1: '=', param2: '='}, + template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}' + }; }); }); + inject(function($rootScope) { + compile('
'); + expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' + $rootScope.$digest(); + expect(element.html()).toBe('1:;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(6); - it('should ignore optional "="-bound property if value is the empty string', function() { - module(function($compileProvider) { - $compileProvider.directive('testDir', valueFn({ - scope: {prop: '=?'}, - controller: function($scope) { - $scope.prop = $scope.prop || 'default'; - this.getProp = function() { - return $scope.prop; - }; - }, - controllerAs: 'ctrl', - template: '

' - })); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - var scope = element.isolateScope(); - expect(scope.ctrl.getProp()).toBe('default'); - $rootScope.$digest(); - expect(scope.ctrl.getProp()).toBe('default'); - scope.prop = 'foop'; - $rootScope.$digest(); - expect(scope.ctrl.getProp()).toBe('foop'); - }); - }); + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:;3:foo;4:'); + expect(countWatches($rootScope)).toEqual(4); + $rootScope.foo = 'baz'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); + expect(countWatches($rootScope)).toEqual(3); - describe('bind-once', function() { + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); + }); + }); - function countWatches(scope) { - var result = 0; - while (scope !== null) { - result += (scope.$$watchers && scope.$$watchers.length) || 0; - result += countWatches(scope.$$childHead); - scope = scope.$$nextSibling; - } - return result; - } + it('should be possible to one-time bind a parameter on a component with a template', function() { + module(function() { + directive('otherTplDir', function() { + return { + scope: {param1: '@', param2: '@'}, + template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}' + }; + }); + }); - it('should be possible to one-time bind a parameter on a component with a template', function() { - module(function() { - directive('otherTplDir', function() { - return { - scope: {param1: '=', param2: '='}, - template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}' - }; - }); - }); + inject(function($rootScope) { + compile('
'); + expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> {{ }} + $rootScope.$digest(); + expect(element.html()).toBe('1:;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(4); // (- 2) -> bind-once in template - inject(function($rootScope) { - compile('
'); - expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' - $rootScope.$digest(); - expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(6); + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:foo;4:'); - expect(countWatches($rootScope)).toEqual(4); + $rootScope.foo = 'baz'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:bar;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - $rootScope.foo = 'baz'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); - expect(countWatches($rootScope)).toEqual(3); + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:;4:'); + }); + }); - $rootScope.bar = 'baz'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); - }); + it('should be possible to one-time bind a parameter on a component with a template', function() { + module(function() { + directive('otherTplDir', function() { + return { + scope: {param1: '=', param2: '='}, + templateUrl: 'other.html' + }; }); + }); - it('should be possible to one-time bind a parameter on a component with a template', function() { - module(function() { - directive('otherTplDir', function() { - return { - scope: {param1: '@', param2: '@'}, - template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}' - }; - }); - }); + inject(function($rootScope, $templateCache) { + $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'); + compile('
'); + $rootScope.$digest(); + expect(element.html()).toBe('1:;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' - inject(function($rootScope) { - compile('
'); - expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> {{ }} - $rootScope.$digest(); - expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(4); // (- 2) -> bind-once in template + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:;3:foo;4:'); + expect(countWatches($rootScope)).toEqual(4); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + $rootScope.foo = 'baz'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); + expect(countWatches($rootScope)).toEqual(3); - $rootScope.foo = 'baz'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:bar;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); + }); + }); - $rootScope.bar = 'baz'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:baz;3:;4:'); - }); + it('should be possible to one-time bind a parameter on a component with a template', function() { + module(function() { + directive('otherTplDir', function() { + return { + scope: {param1: '@', param2: '@'}, + templateUrl: 'other.html' + }; }); + }); - it('should be possible to one-time bind a parameter on a component with a template', function() { - module(function() { - directive('otherTplDir', function() { - return { - scope: {param1: '=', param2: '='}, - templateUrl: 'other.html' - }; - }); - }); + inject(function($rootScope, $templateCache) { + $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'); + compile('
'); + $rootScope.$digest(); + expect(element.html()).toBe('1:;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(4); // (4 - 2) -> template watch group, 2 -> {{ }} - inject(function($rootScope, $templateCache) { - $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'); - compile('
'); - $rootScope.$digest(); - expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:foo;4:'); - expect(countWatches($rootScope)).toEqual(4); + $rootScope.foo = 'baz'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:bar;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - $rootScope.foo = 'baz'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); - expect(countWatches($rootScope)).toEqual(3); + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:;4:'); + }); + }); - $rootScope.bar = 'baz'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); - }); + it('should continue with a digets cycle when there is a two-way binding from the child to the parent', function() { + module(function() { + directive('hello', function() { + return { + restrict: 'E', + scope: { greeting: '=' }, + template: '', + link: function(scope) { + scope.setGreeting = function() { scope.greeting = 'Hello!'; }; + } + }; }); + }); - it('should be possible to one-time bind a parameter on a component with a template', function() { - module(function() { - directive('otherTplDir', function() { - return { - scope: {param1: '@', param2: '@'}, - templateUrl: 'other.html' - }; - }); - }); + inject(function($rootScope) { + compile('
' + + '

{{greeting}}

' + + '
' + + '
'); + $rootScope.$digest(); + browserTrigger(element.find('button'), 'click'); + expect(element.find('p').text()).toBe('Hello!'); + }); + }); - inject(function($rootScope, $templateCache) { - $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'); - compile('
'); - $rootScope.$digest(); - expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(4); // (4 - 2) -> template watch group, 2 -> {{ }} + }); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); - $rootScope.foo = 'baz'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:bar;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + describe('attribute', function() { + it('should copy simple attribute', inject(function() { + compile('
'); - $rootScope.bar = 'baz'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:baz;3:;4:'); - }); - }); + expect(componentScope.attr).toEqual('some text'); + expect(componentScope.attrAlias).toEqual('some text'); + expect(componentScope.$attrAlias).toEqual('some other text'); + expect(componentScope.attrAlias).toEqual(componentScope.attr); + })); - it('should continue with a digets cycle when there is a two-way binding from the child to the parent', function() { - module(function() { - directive('hello', function() { - return { - restrict: 'E', - scope: { greeting: '=' }, - template: '', - link: function(scope) { - scope.setGreeting = function() { scope.greeting = 'Hello!'; }; - } - }; - }); - }); + it('should copy an attribute with spaces', inject(function() { + compile('
'); - inject(function($rootScope) { - compile('
' + - '

{{greeting}}

' + - '
' + - '
'); - $rootScope.$digest(); - browserTrigger(element.find('button'), 'click'); - expect(element.find('p').text()).toBe('Hello!'); - }); - }); + expect(componentScope.attr).toEqual(' some text '); + expect(componentScope.attrAlias).toEqual(' some text '); + expect(componentScope.$attrAlias).toEqual(' some other text '); + expect(componentScope.attrAlias).toEqual(componentScope.attr); + })); - }); + it('should set up the interpolation before it reaches the link function', inject(function() { + $rootScope.name = 'misko'; + compile('
'); + expect(componentScope.attr).toEqual('hello misko'); + expect(componentScope.attrAlias).toEqual('hello misko'); + expect(componentScope.$attrAlias).toEqual('hi misko'); + })); + it('should update when interpolated attribute updates', inject(function() { + compile('
'); - describe('attribute', function() { - it('should copy simple attribute', inject(function() { - compile('
'); + $rootScope.name = 'igor'; + $rootScope.$apply(); - expect(componentScope.attr).toEqual('some text'); - expect(componentScope.attrAlias).toEqual('some text'); - expect(componentScope.$attrAlias).toEqual('some other text'); - expect(componentScope.attrAlias).toEqual(componentScope.attr); - })); + expect(componentScope.attr).toEqual('hello igor'); + expect(componentScope.attrAlias).toEqual('hello igor'); + expect(componentScope.$attrAlias).toEqual('hi igor'); + })); + }); - it('should copy an attribute with spaces', inject(function() { - compile('
'); - expect(componentScope.attr).toEqual(' some text '); - expect(componentScope.attrAlias).toEqual(' some text '); - expect(componentScope.$attrAlias).toEqual(' some other text '); - expect(componentScope.attrAlias).toEqual(componentScope.attr); - })); + describe('object reference', function() { + it('should update local when origin changes', inject(function() { + compile('
'); + expect(componentScope.ref).toBeUndefined(); + expect(componentScope.refAlias).toBe(componentScope.ref); + expect(componentScope.$refAlias).toBe(componentScope.ref); - it('should set up the interpolation before it reaches the link function', inject(function() { - $rootScope.name = 'misko'; - compile('
'); - expect(componentScope.attr).toEqual('hello misko'); - expect(componentScope.attrAlias).toEqual('hello misko'); - expect(componentScope.$attrAlias).toEqual('hi misko'); - })); + $rootScope.name = 'misko'; + $rootScope.$apply(); - it('should update when interpolated attribute updates', inject(function() { - compile('
'); + expect($rootScope.name).toBe('misko'); + expect(componentScope.ref).toBe('misko'); + expect(componentScope.refAlias).toBe('misko'); + expect(componentScope.$refAlias).toBe('misko'); - $rootScope.name = 'igor'; - $rootScope.$apply(); + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.ref).toBe($rootScope.name); + expect(componentScope.refAlias).toBe($rootScope.name); + expect(componentScope.$refAlias).toBe($rootScope.name); + })); - expect(componentScope.attr).toEqual('hello igor'); - expect(componentScope.attrAlias).toEqual('hello igor'); - expect(componentScope.$attrAlias).toEqual('hi igor'); - })); - }); + it('should update local when both change', inject(function() { + compile('
'); + $rootScope.name = {mark:123}; + componentScope.ref = 'misko'; - describe('object reference', function() { - it('should update local when origin changes', inject(function() { - compile('
'); - expect(componentScope.ref).toBeUndefined(); - expect(componentScope.refAlias).toBe(componentScope.ref); - expect(componentScope.$refAlias).toBe(componentScope.ref); + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.ref).toBe($rootScope.name); + expect(componentScope.refAlias).toBe($rootScope.name); + expect(componentScope.$refAlias).toBe($rootScope.name); - $rootScope.name = 'misko'; - $rootScope.$apply(); + $rootScope.name = 'igor'; + componentScope.ref = {}; + $rootScope.$apply(); + expect($rootScope.name).toEqual('igor'); + expect(componentScope.ref).toBe($rootScope.name); + expect(componentScope.refAlias).toBe($rootScope.name); + expect(componentScope.$refAlias).toBe($rootScope.name); + })); - expect($rootScope.name).toBe('misko'); - expect(componentScope.ref).toBe('misko'); - expect(componentScope.refAlias).toBe('misko'); - expect(componentScope.$refAlias).toBe('misko'); + it('should not break if local and origin both change to the same value', inject(function() { + $rootScope.name = 'aaa'; - $rootScope.name = {}; - $rootScope.$apply(); - expect(componentScope.ref).toBe($rootScope.name); - expect(componentScope.refAlias).toBe($rootScope.name); - expect(componentScope.$refAlias).toBe($rootScope.name); - })); + compile('
'); + //change both sides to the same item within the same digest cycle + componentScope.ref = 'same'; + $rootScope.name = 'same'; + $rootScope.$apply(); - it('should update local when both change', inject(function() { - compile('
'); - $rootScope.name = {mark:123}; - componentScope.ref = 'misko'; + //change origin back to its previous value + $rootScope.name = 'aaa'; + $rootScope.$apply(); - $rootScope.$apply(); - expect($rootScope.name).toEqual({mark:123}); - expect(componentScope.ref).toBe($rootScope.name); - expect(componentScope.refAlias).toBe($rootScope.name); - expect(componentScope.$refAlias).toBe($rootScope.name); + expect($rootScope.name).toBe('aaa'); + expect(componentScope.ref).toBe('aaa'); + })); - $rootScope.name = 'igor'; - componentScope.ref = {}; - $rootScope.$apply(); - expect($rootScope.name).toEqual('igor'); - expect(componentScope.ref).toBe($rootScope.name); - expect(componentScope.refAlias).toBe($rootScope.name); - expect(componentScope.$refAlias).toBe($rootScope.name); - })); + it('should complain on non assignable changes', inject(function() { + compile('
'); + $rootScope.name = 'world'; + $rootScope.$apply(); + expect(componentScope.ref).toBe('hello world'); - it('should not break if local and origin both change to the same value', inject(function() { - $rootScope.name = 'aaa'; + componentScope.ref = 'ignore me'; + expect(function() { $rootScope.$apply(); }). + toThrowMinErr('$compile', 'nonassign', 'Expression \'\'hello \' + name\' in attribute \'ref\' used with directive \'myComponent\' is non-assignable!'); + expect(componentScope.ref).toBe('hello world'); + // reset since the exception was rethrown which prevented phase clearing + $rootScope.$$phase = null; - compile('
'); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.ref).toBe('hello misko'); + })); - //change both sides to the same item within the same digest cycle - componentScope.ref = 'same'; - $rootScope.name = 'same'; - $rootScope.$apply(); + it('should complain if assigning to undefined', inject(function() { + compile('
'); + $rootScope.$apply(); + expect(componentScope.ref).toBeUndefined(); - //change origin back to its previous value - $rootScope.name = 'aaa'; - $rootScope.$apply(); + componentScope.ref = 'ignore me'; + expect(function() { $rootScope.$apply(); }). + toThrowMinErr('$compile', 'nonassign', 'Expression \'undefined\' in attribute \'ref\' used with directive \'myComponent\' is non-assignable!'); + expect(componentScope.ref).toBeUndefined(); - expect($rootScope.name).toBe('aaa'); - expect(componentScope.ref).toBe('aaa'); - })); + $rootScope.$$phase = null; // reset since the exception was rethrown which prevented phase clearing + $rootScope.$apply(); + expect(componentScope.ref).toBeUndefined(); + })); - it('should complain on non assignable changes', inject(function() { - compile('
'); - $rootScope.name = 'world'; - $rootScope.$apply(); - expect(componentScope.ref).toBe('hello world'); + // regression + it('should stabilize model', inject(function() { + compile('
'); - componentScope.ref = 'ignore me'; - expect(function() { $rootScope.$apply(); }). - toThrowMinErr('$compile', 'nonassign', 'Expression \'\'hello \' + name\' in attribute \'ref\' used with directive \'myComponent\' is non-assignable!'); - expect(componentScope.ref).toBe('hello world'); - // reset since the exception was rethrown which prevented phase clearing - $rootScope.$$phase = null; + var lastRefValueInParent; + $rootScope.$watch('name', function(ref) { + lastRefValueInParent = ref; + }); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.ref).toBe('hello misko'); - })); + $rootScope.name = 'aaa'; + $rootScope.$apply(); - it('should complain if assigning to undefined', inject(function() { - compile('
'); - $rootScope.$apply(); - expect(componentScope.ref).toBeUndefined(); + componentScope.reference = 'new'; + $rootScope.$apply(); - componentScope.ref = 'ignore me'; - expect(function() { $rootScope.$apply(); }). - toThrowMinErr('$compile', 'nonassign', 'Expression \'undefined\' in attribute \'ref\' used with directive \'myComponent\' is non-assignable!'); - expect(componentScope.ref).toBeUndefined(); + expect(lastRefValueInParent).toBe('new'); + })); - $rootScope.$$phase = null; // reset since the exception was rethrown which prevented phase clearing - $rootScope.$apply(); - expect(componentScope.ref).toBeUndefined(); - })); + describe('literal objects', function() { + it('should copy parent changes', inject(function() { + compile('
'); - // regression - it('should stabilize model', inject(function() { - compile('
'); + $rootScope.name = 'a'; + $rootScope.$apply(); + expect(componentScope.reference).toEqual({name: 'a'}); - var lastRefValueInParent; - $rootScope.$watch('name', function(ref) { - lastRefValueInParent = ref; - }); + $rootScope.name = 'b'; + $rootScope.$apply(); + expect(componentScope.reference).toEqual({name: 'b'}); + })); - $rootScope.name = 'aaa'; - $rootScope.$apply(); + it('should not change the component when parent does not change', inject(function() { + compile('
'); - componentScope.reference = 'new'; - $rootScope.$apply(); + $rootScope.name = 'a'; + $rootScope.$apply(); + var lastComponentValue = componentScope.reference; + $rootScope.$apply(); + expect(componentScope.reference).toBe(lastComponentValue); + })); - expect(lastRefValueInParent).toBe('new'); - })); + it('should complain when the component changes', inject(function() { + compile('
'); - describe('literal objects', function() { - it('should copy parent changes', inject(function() { - compile('
'); + $rootScope.name = 'a'; + $rootScope.$apply(); + componentScope.reference = {name: 'b'}; + expect(function() { + $rootScope.$apply(); + }).toThrowMinErr('$compile', 'nonassign', 'Expression \'{name: name}\' in attribute \'reference\' used with directive \'myComponent\' is non-assignable!'); - $rootScope.name = 'a'; - $rootScope.$apply(); - expect(componentScope.reference).toEqual({name: 'a'}); + })); - $rootScope.name = 'b'; - $rootScope.$apply(); - expect(componentScope.reference).toEqual({name: 'b'}); - })); + it('should work for primitive literals', inject(function() { + test('1', 1); + test('null', null); + test('undefined', undefined); + test('\'someString\'', 'someString'); + test('true', true); - it('should not change the component when parent does not change', inject(function() { - compile('
'); + function test(literalString, literalValue) { + compile('
'); - $rootScope.name = 'a'; - $rootScope.$apply(); - var lastComponentValue = componentScope.reference; - $rootScope.$apply(); - expect(componentScope.reference).toBe(lastComponentValue); - })); + $rootScope.$apply(); + expect(componentScope.reference).toBe(literalValue); + dealoc(element); + } + })); - it('should complain when the component changes', inject(function() { - compile('
'); + }); - $rootScope.name = 'a'; - $rootScope.$apply(); - componentScope.reference = {name: 'b'}; - expect(function() { - $rootScope.$apply(); - }).toThrowMinErr('$compile', 'nonassign', 'Expression \'{name: name}\' in attribute \'reference\' used with directive \'myComponent\' is non-assignable!'); + }); - })); - it('should work for primitive literals', inject(function() { - test('1', 1); - test('null', null); - test('undefined', undefined); - test('\'someString\'', 'someString'); - test('true', true); + describe('optional object reference', function() { + it('should update local when origin changes', inject(function() { + compile('
'); + expect(componentScope.optRef).toBeUndefined(); + expect(componentScope.optRefAlias).toBe(componentScope.optRef); + expect(componentScope.$optRefAlias).toBe(componentScope.optRef); - function test(literalString, literalValue) { - compile('
'); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + expect(componentScope.$optrefAlias).toBe($rootScope.name); - $rootScope.$apply(); - expect(componentScope.reference).toBe(literalValue); - dealoc(element); - } - })); + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + expect(componentScope.$optrefAlias).toBe($rootScope.name); + })); - }); + it('should not throw exception when reference does not exist', inject(function() { + compile('
'); - }); + expect(componentScope.optref).toBeUndefined(); + expect(componentScope.optrefAlias).toBeUndefined(); + expect(componentScope.$optrefAlias).toBeUndefined(); + expect(componentScope.optreference).toBeUndefined(); + })); + }); - describe('optional object reference', function() { - it('should update local when origin changes', inject(function() { - compile('
'); - expect(componentScope.optRef).toBeUndefined(); - expect(componentScope.optRefAlias).toBe(componentScope.optRef); - expect(componentScope.$optRefAlias).toBe(componentScope.optRef); + describe('collection object reference', function() { + it('should update isolate scope when origin scope changes', inject(function() { + $rootScope.collection = [{ + name: 'Gabriel', + value: 18 + }, { + name: 'Tony', + value: 91 + }]; + $rootScope.query = ''; + $rootScope.$apply(); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.optref).toBe($rootScope.name); - expect(componentScope.optrefAlias).toBe($rootScope.name); - expect(componentScope.$optrefAlias).toBe($rootScope.name); + compile('
'); - $rootScope.name = {}; - $rootScope.$apply(); - expect(componentScope.optref).toBe($rootScope.name); - expect(componentScope.optrefAlias).toBe($rootScope.name); - expect(componentScope.$optrefAlias).toBe($rootScope.name); - })); + expect(componentScope.colref).toEqual($rootScope.collection); + expect(componentScope.colrefAlias).toEqual(componentScope.colref); + expect(componentScope.$colrefAlias).toEqual(componentScope.colref); - it('should not throw exception when reference does not exist', inject(function() { - compile('
'); + $rootScope.query = 'Gab'; + $rootScope.$apply(); - expect(componentScope.optref).toBeUndefined(); - expect(componentScope.optrefAlias).toBeUndefined(); - expect(componentScope.$optrefAlias).toBeUndefined(); - expect(componentScope.optreference).toBeUndefined(); - })); - }); + expect(componentScope.colref).toEqual([$rootScope.collection[0]]); + expect(componentScope.colrefAlias).toEqual([$rootScope.collection[0]]); + expect(componentScope.$colrefAlias).toEqual([$rootScope.collection[0]]); + })); + it('should update origin scope when isolate scope changes', inject(function() { + $rootScope.collection = [{ + name: 'Gabriel', + value: 18 + }, { + name: 'Tony', + value: 91 + }]; - describe('collection object reference', function() { - it('should update isolate scope when origin scope changes', inject(function() { - $rootScope.collection = [{ - name: 'Gabriel', - value: 18 - }, { - name: 'Tony', - value: 91 - }]; - $rootScope.query = ''; - $rootScope.$apply(); + compile('
'); - compile('
'); + var newItem = { + name: 'Pablo', + value: 10 + }; + componentScope.colref.push(newItem); + componentScope.$apply(); - expect(componentScope.colref).toEqual($rootScope.collection); - expect(componentScope.colrefAlias).toEqual(componentScope.colref); - expect(componentScope.$colrefAlias).toEqual(componentScope.colref); + expect($rootScope.collection[2]).toEqual(newItem); + })); + }); - $rootScope.query = 'Gab'; - $rootScope.$apply(); - expect(componentScope.colref).toEqual([$rootScope.collection[0]]); - expect(componentScope.colrefAlias).toEqual([$rootScope.collection[0]]); - expect(componentScope.$colrefAlias).toEqual([$rootScope.collection[0]]); - })); + describe('one-way binding', function() { + it('should update isolate when the identity of origin changes', inject(function() { + compile('
'); - it('should update origin scope when isolate scope changes', inject(function() { - $rootScope.collection = [{ - name: 'Gabriel', - value: 18 - }, { - name: 'Tony', - value: 91 - }]; + expect(componentScope.owRef).toBeUndefined(); + expect(componentScope.owRefAlias).toBe(componentScope.owRef); + expect(componentScope.$owRefAlias).toBe(componentScope.owRef); - compile('
'); + $rootScope.obj = {value: 'initial'}; + $rootScope.$apply(); - var newItem = { - name: 'Pablo', - value: 10 - }; - componentScope.colref.push(newItem); - componentScope.$apply(); + expect($rootScope.obj).toEqual({value: 'initial'}); + expect(componentScope.owRef).toEqual({value: 'initial'}); + expect(componentScope.owRefAlias).toBe(componentScope.owRef); + expect(componentScope.$owRefAlias).toBe(componentScope.owRef); - expect($rootScope.collection[2]).toEqual(newItem); - })); - }); + // This changes in both scopes because of reference + $rootScope.obj.value = 'origin1'; + $rootScope.$apply(); + expect(componentScope.owRef.value).toBe('origin1'); + expect(componentScope.owRefAlias.value).toBe('origin1'); + expect(componentScope.$owRefAlias.value).toBe('origin1'); + componentScope.owRef = {value: 'isolate1'}; + componentScope.$apply(); + expect($rootScope.obj.value).toBe('origin1'); - describe('one-way binding', function() { - it('should update isolate when the identity of origin changes', inject(function() { - compile('
'); + // Change does not propagate because object identity hasn't changed + $rootScope.obj.value = 'origin2'; + $rootScope.$apply(); + expect(componentScope.owRef.value).toBe('isolate1'); + expect(componentScope.owRefAlias.value).toBe('origin2'); + expect(componentScope.$owRefAlias.value).toBe('origin2'); - expect(componentScope.owRef).toBeUndefined(); - expect(componentScope.owRefAlias).toBe(componentScope.owRef); - expect(componentScope.$owRefAlias).toBe(componentScope.owRef); + // Change does propagate because object identity changes + $rootScope.obj = {value: 'origin3'}; + $rootScope.$apply(); + expect(componentScope.owRef.value).toBe('origin3'); + expect(componentScope.owRef).toBe($rootScope.obj); + expect(componentScope.owRefAlias).toBe($rootScope.obj); + expect(componentScope.$owRefAlias).toBe($rootScope.obj); + })); - $rootScope.obj = {value: 'initial'}; - $rootScope.$apply(); + it('should update isolate when both change', inject(function() { + compile('
'); - expect($rootScope.obj).toEqual({value: 'initial'}); - expect(componentScope.owRef).toEqual({value: 'initial'}); - expect(componentScope.owRefAlias).toBe(componentScope.owRef); - expect(componentScope.$owRefAlias).toBe(componentScope.owRef); + $rootScope.name = {mark:123}; + componentScope.owRef = 'misko'; - // This changes in both scopes because of reference - $rootScope.obj.value = 'origin1'; - $rootScope.$apply(); - expect(componentScope.owRef.value).toBe('origin1'); - expect(componentScope.owRefAlias.value).toBe('origin1'); - expect(componentScope.$owRefAlias.value).toBe('origin1'); + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); + expect(componentScope.$owRefAlias).toBe($rootScope.name); - componentScope.owRef = {value: 'isolate1'}; - componentScope.$apply(); - expect($rootScope.obj.value).toBe('origin1'); + $rootScope.name = 'igor'; + componentScope.owRef = {}; + $rootScope.$apply(); + expect($rootScope.name).toEqual('igor'); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); + expect(componentScope.$owRefAlias).toBe($rootScope.name); + })); - // Change does not propagate because object identity hasn't changed - $rootScope.obj.value = 'origin2'; - $rootScope.$apply(); - expect(componentScope.owRef.value).toBe('isolate1'); - expect(componentScope.owRefAlias.value).toBe('origin2'); - expect(componentScope.$owRefAlias.value).toBe('origin2'); + describe('initialization', function() { + var component, log; - // Change does propagate because object identity changes - $rootScope.obj = {value: 'origin3'}; - $rootScope.$apply(); - expect(componentScope.owRef.value).toBe('origin3'); - expect(componentScope.owRef).toBe($rootScope.obj); - expect(componentScope.owRefAlias).toBe($rootScope.obj); - expect(componentScope.$owRefAlias).toBe($rootScope.obj); - })); + beforeEach(function() { + log = []; + angular.module('owComponentTest', []) + .component('owComponent', { + bindings: { input: '<' }, + controller: function() { + component = this; + this.input = 'constructor'; + log.push('constructor'); - it('should update isolate when both change', inject(function() { - compile('
'); + this.$onInit = function() { + this.input = '$onInit'; + log.push('$onInit'); + }; - $rootScope.name = {mark:123}; - componentScope.owRef = 'misko'; + this.$onChanges = function(changes) { + if (changes.input) { + log.push(['$onChanges', copy(changes.input)]); + } + }; + } + }); + }); - $rootScope.$apply(); - expect($rootScope.name).toEqual({mark:123}); - expect(componentScope.owRef).toBe($rootScope.name); - expect(componentScope.owRefAlias).toBe($rootScope.name); - expect(componentScope.$owRefAlias).toBe($rootScope.name); + it('should not update isolate again after $onInit if outer has not changed', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = 'outer'; + compile(''); - $rootScope.name = 'igor'; - componentScope.owRef = {}; - $rootScope.$apply(); - expect($rootScope.name).toEqual('igor'); - expect(componentScope.owRef).toBe($rootScope.name); - expect(componentScope.owRefAlias).toBe($rootScope.name); - expect(componentScope.$owRefAlias).toBe($rootScope.name); - })); + expect($rootScope.name).toEqual('outer'); + expect(component.input).toEqual('$onInit'); - describe('initialization', function() { - var component, log; - - beforeEach(function() { - log = []; - angular.module('owComponentTest', []) - .component('owComponent', { - bindings: { input: '<' }, - controller: function() { - component = this; - this.input = 'constructor'; - log.push('constructor'); - - this.$onInit = function() { - this.input = '$onInit'; - log.push('$onInit'); - }; + $rootScope.$digest(); - this.$onChanges = function(changes) { - if (changes.input) { - log.push(['$onChanges', copy(changes.input)]); - } - }; - } - }); - }); + expect($rootScope.name).toEqual('outer'); + expect(component.input).toEqual('$onInit'); - it('should not update isolate again after $onInit if outer has not changed', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer'; - compile(''); + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: 'outer' })], + '$onInit' + ]); + }); + }); - expect($rootScope.name).toEqual('outer'); - expect(component.input).toEqual('$onInit'); + it('should not update isolate again after $onInit if outer object reference has not changed', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = ['outer']; + compile(''); - $rootScope.$digest(); + expect($rootScope.name).toEqual(['outer']); + expect(component.input).toEqual('$onInit'); - expect($rootScope.name).toEqual('outer'); - expect(component.input).toEqual('$onInit'); + $rootScope.name[0] = 'inner'; + $rootScope.$digest(); - expect(log).toEqual([ - 'constructor', - ['$onChanges', jasmine.objectContaining({ currentValue: 'outer' })], - '$onInit' - ]); - }); - }); + expect($rootScope.name).toEqual(['inner']); + expect(component.input).toEqual('$onInit'); - it('should not update isolate again after $onInit if outer object reference has not changed', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = ['outer']; - compile(''); + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], + '$onInit' + ]); + }); + }); - expect($rootScope.name).toEqual(['outer']); - expect(component.input).toEqual('$onInit'); + it('should update isolate again after $onInit if outer object reference changes even if equal', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = ['outer']; + compile(''); - $rootScope.name[0] = 'inner'; - $rootScope.$digest(); + expect($rootScope.name).toEqual(['outer']); + expect(component.input).toEqual('$onInit'); - expect($rootScope.name).toEqual(['inner']); - expect(component.input).toEqual('$onInit'); + $rootScope.name = ['outer']; + $rootScope.$digest(); - expect(log).toEqual([ - 'constructor', - ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], - '$onInit' - ]); - }); - }); + expect($rootScope.name).toEqual(['outer']); + expect(component.input).toEqual(['outer']); - it('should update isolate again after $onInit if outer object reference changes even if equal', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = ['outer']; - compile(''); + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], + '$onInit', + ['$onChanges', jasmine.objectContaining({ previousValue: ['outer'], currentValue: ['outer'] })] + ]); + }); + }); - expect($rootScope.name).toEqual(['outer']); - expect(component.input).toEqual('$onInit'); + it('should not update isolate again after $onInit if outer is a literal', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = 'outer'; + compile(''); - $rootScope.name = ['outer']; - $rootScope.$digest(); + expect(component.input).toEqual('$onInit'); - expect($rootScope.name).toEqual(['outer']); - expect(component.input).toEqual(['outer']); + // No outer change + $rootScope.$apply('name = "outer"'); + expect(component.input).toEqual('$onInit'); - expect(log).toEqual([ - 'constructor', - ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], - '$onInit', - ['$onChanges', jasmine.objectContaining({ previousValue: ['outer'], currentValue: ['outer'] })] - ]); - }); - }); + // Outer change + $rootScope.$apply('name = "re-outer"'); + expect(component.input).toEqual(['re-outer']); - it('should not update isolate again after $onInit if outer is a literal', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer'; - compile(''); - - expect(component.input).toEqual('$onInit'); - - // No outer change - $rootScope.$apply('name = "outer"'); - expect(component.input).toEqual('$onInit'); - - // Outer change - $rootScope.$apply('name = "re-outer"'); - expect(component.input).toEqual(['re-outer']); - - expect(log).toEqual([ - 'constructor', - [ - '$onChanges', - jasmine.objectContaining({currentValue: ['outer']}) - ], - '$onInit', - [ - '$onChanges', - jasmine.objectContaining({previousValue: ['outer'], currentValue: ['re-outer']}) - ] - ]); - }); - }); + expect(log).toEqual([ + 'constructor', + [ + '$onChanges', + jasmine.objectContaining({currentValue: ['outer']}) + ], + '$onInit', + [ + '$onChanges', + jasmine.objectContaining({previousValue: ['outer'], currentValue: ['re-outer']}) + ] + ]); + }); + }); - it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer1'; - compile(''); - - expect(component.input).toEqual('$onInit'); - $rootScope.$apply('name = "outer2"'); - - expect($rootScope.name).toEqual('outer2'); - expect(component.input).toEqual('outer2'); - expect(log).toEqual([ - 'constructor', - ['$onChanges', jasmine.objectContaining({ currentValue: 'outer1' })], - '$onInit', - ['$onChanges', jasmine.objectContaining({ currentValue: 'outer2', previousValue: 'outer1' })] - ]); - }); - }); + it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = 'outer1'; + compile(''); - it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { - angular.module('owComponentTest') - .directive('changeInput', function() { - return function(scope, elem, attrs) { - scope.name = 'outer2'; - }; - }); - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer1'; - compile(''); + expect(component.input).toEqual('$onInit'); + $rootScope.$apply('name = "outer2"'); - expect(component.input).toEqual('$onInit'); - $rootScope.$digest(); + expect($rootScope.name).toEqual('outer2'); + expect(component.input).toEqual('outer2'); + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: 'outer1' })], + '$onInit', + ['$onChanges', jasmine.objectContaining({ currentValue: 'outer2', previousValue: 'outer1' })] + ]); + }); + }); - expect($rootScope.name).toEqual('outer2'); - expect(component.input).toEqual('outer2'); - expect(log).toEqual([ - 'constructor', - ['$onChanges', jasmine.objectContaining({ currentValue: 'outer1' })], - '$onInit', - ['$onChanges', jasmine.objectContaining({ currentValue: 'outer2', previousValue: 'outer1' })] - ]); - }); + it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { + angular.module('owComponentTest') + .directive('changeInput', function() { + return function(scope, elem, attrs) { + scope.name = 'outer2'; + }; }); - }); + module('owComponentTest'); + inject(function() { + $rootScope.name = 'outer1'; + compile(''); - it('should not break when isolate and origin both change to the same value', inject(function() { - $rootScope.name = 'aaa'; - compile('
'); + expect(component.input).toEqual('$onInit'); + $rootScope.$digest(); - //change both sides to the same item within the same digest cycle - componentScope.owRef = 'same'; - $rootScope.name = 'same'; - $rootScope.$apply(); + expect($rootScope.name).toEqual('outer2'); + expect(component.input).toEqual('outer2'); + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: 'outer1' })], + '$onInit', + ['$onChanges', jasmine.objectContaining({ currentValue: 'outer2', previousValue: 'outer1' })] + ]); + }); + }); + }); - //change origin back to its previous value - $rootScope.name = 'aaa'; - $rootScope.$apply(); + it('should not break when isolate and origin both change to the same value', inject(function() { + $rootScope.name = 'aaa'; + compile('
'); - expect($rootScope.name).toBe('aaa'); - expect(componentScope.owRef).toBe('aaa'); - })); + //change both sides to the same item within the same digest cycle + componentScope.owRef = 'same'; + $rootScope.name = 'same'; + $rootScope.$apply(); + //change origin back to its previous value + $rootScope.name = 'aaa'; + $rootScope.$apply(); - it('should not update origin when identity of isolate changes', inject(function() { - $rootScope.name = {mark:123}; - compile('
'); + expect($rootScope.name).toBe('aaa'); + expect(componentScope.owRef).toBe('aaa'); + })); - expect($rootScope.name).toEqual({mark:123}); - expect(componentScope.owRef).toBe($rootScope.name); - expect(componentScope.owRefAlias).toBe($rootScope.name); - expect(componentScope.$owRefAlias).toBe($rootScope.name); - componentScope.owRef = 'martin'; - $rootScope.$apply(); - expect($rootScope.name).toEqual({mark: 123}); - expect(componentScope.owRef).toBe('martin'); - expect(componentScope.owRefAlias).toEqual({mark: 123}); - expect(componentScope.$owRefAlias).toEqual({mark: 123}); - })); + it('should not update origin when identity of isolate changes', inject(function() { + $rootScope.name = {mark:123}; + compile('
'); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); + expect(componentScope.$owRefAlias).toBe($rootScope.name); - it('should update origin when property of isolate object reference changes', inject(function() { - $rootScope.obj = {mark:123}; - compile('
'); + componentScope.owRef = 'martin'; + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark: 123}); + expect(componentScope.owRef).toBe('martin'); + expect(componentScope.owRefAlias).toEqual({mark: 123}); + expect(componentScope.$owRefAlias).toEqual({mark: 123}); + })); - expect($rootScope.obj).toEqual({mark:123}); - expect(componentScope.owRef).toBe($rootScope.obj); - componentScope.owRef.mark = 789; - $rootScope.$apply(); - expect($rootScope.obj).toEqual({mark: 789}); - expect(componentScope.owRef).toBe($rootScope.obj); - })); + it('should update origin when property of isolate object reference changes', inject(function() { + $rootScope.obj = {mark:123}; + compile('
'); + expect($rootScope.obj).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.obj); - it('should not throw on non assignable expressions in the parent', inject(function() { - compile('
'); + componentScope.owRef.mark = 789; + $rootScope.$apply(); + expect($rootScope.obj).toEqual({mark: 789}); + expect(componentScope.owRef).toBe($rootScope.obj); + })); - $rootScope.name = 'world'; - $rootScope.$apply(); - expect(componentScope.owRef).toBe('hello world'); - componentScope.owRef = 'ignore me'; - expect(componentScope.owRef).toBe('ignore me'); - expect($rootScope.name).toBe('world'); + it('should not throw on non assignable expressions in the parent', inject(function() { + compile('
'); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.owRef).toBe('hello misko'); - })); + $rootScope.name = 'world'; + $rootScope.$apply(); + expect(componentScope.owRef).toBe('hello world'); + componentScope.owRef = 'ignore me'; + expect(componentScope.owRef).toBe('ignore me'); + expect($rootScope.name).toBe('world'); - it('should not throw when assigning to undefined', inject(function() { - compile('
'); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.owRef).toBe('hello misko'); + })); - expect(componentScope.owRef).toBeUndefined(); - componentScope.owRef = 'ignore me'; - expect(componentScope.owRef).toBe('ignore me'); + it('should not throw when assigning to undefined', inject(function() { + compile('
'); - $rootScope.$apply(); - expect(componentScope.owRef).toBe('ignore me'); - })); + expect(componentScope.owRef).toBeUndefined(); + componentScope.owRef = 'ignore me'; + expect(componentScope.owRef).toBe('ignore me'); - it('should update isolate scope when "<"-bound NaN changes', inject(function() { - $rootScope.num = NaN; - compile('
'); + $rootScope.$apply(); + expect(componentScope.owRef).toBe('ignore me'); + })); - var isolateScope = element.isolateScope(); - expect(isolateScope.owRef).toBeNaN(); - $rootScope.num = 64; - $rootScope.$apply(); - expect(isolateScope.owRef).toBe(64); - })); + it('should update isolate scope when "<"-bound NaN changes', inject(function() { + $rootScope.num = NaN; + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.owRef).toBeNaN(); - describe('literal objects', function() { - it('should copy parent changes', inject(function() { - compile('
'); + $rootScope.num = 64; + $rootScope.$apply(); + expect(isolateScope.owRef).toBe(64); + })); - $rootScope.name = 'a'; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'a'}); - $rootScope.name = 'b'; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'b'}); - })); + describe('literal objects', function() { + it('should copy parent changes', inject(function() { + compile('
'); + $rootScope.name = 'a'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'a'}); - it('should not change the isolated scope when origin does not change', inject(function() { - compile('
'); + $rootScope.name = 'b'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'b'}); + })); - $rootScope.name = 'a'; - $rootScope.$apply(); - var lastComponentValue = componentScope.owRef; - $rootScope.$apply(); - expect(componentScope.owRef).toBe(lastComponentValue); - })); + it('should not change the isolated scope when origin does not change', inject(function() { + compile('
'); - it('should watch input values to array literals', inject(function() { - $rootScope.name = 'georgios'; - $rootScope.obj = {name: 'pete'}; - compile('
'); + $rootScope.name = 'a'; + $rootScope.$apply(); + var lastComponentValue = componentScope.owRef; + $rootScope.$apply(); + expect(componentScope.owRef).toBe(lastComponentValue); + })); - expect(componentScope.owRef).toEqual([{name: 'georgios'}, {name: 'pete'}]); - $rootScope.name = 'lucas'; - $rootScope.obj = {name: 'martin'}; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual([{name: 'lucas'}, {name: 'martin'}]); - })); + it('should watch input values to array literals', inject(function() { + $rootScope.name = 'georgios'; + $rootScope.obj = {name: 'pete'}; + compile('
'); + expect(componentScope.owRef).toEqual([{name: 'georgios'}, {name: 'pete'}]); - it('should watch input values object literals', inject(function() { - $rootScope.name = 'georgios'; - $rootScope.obj = {name: 'pete'}; - compile('
'); + $rootScope.name = 'lucas'; + $rootScope.obj = {name: 'martin'}; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual([{name: 'lucas'}, {name: 'martin'}]); + })); - expect(componentScope.owRef).toEqual({name: 'georgios', item: {name: 'pete'}}); - $rootScope.name = 'lucas'; - $rootScope.obj = {name: 'martin'}; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'lucas', item: {name: 'martin'}}); - })); + it('should watch input values object literals', inject(function() { + $rootScope.name = 'georgios'; + $rootScope.obj = {name: 'pete'}; + compile('
'); + expect(componentScope.owRef).toEqual({name: 'georgios', item: {name: 'pete'}}); - it('should not complain when the isolated scope changes', inject(function() { - compile('
'); + $rootScope.name = 'lucas'; + $rootScope.obj = {name: 'martin'}; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'lucas', item: {name: 'martin'}}); + })); - $rootScope.name = 'a'; - $rootScope.$apply(); - componentScope.owRef = {name: 'b'}; - componentScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'b'}); - expect($rootScope.name).toBe('a'); + it('should not complain when the isolated scope changes', inject(function() { + compile('
'); - $rootScope.name = 'c'; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'c'}); - })); + $rootScope.name = 'a'; + $rootScope.$apply(); + componentScope.owRef = {name: 'b'}; + componentScope.$apply(); - it('should work for primitive literals', inject(function() { - test('1', 1); - test('null', null); - test('undefined', undefined); - test('\'someString\'', 'someString'); - test('true', true); + expect(componentScope.owRef).toEqual({name: 'b'}); + expect($rootScope.name).toBe('a'); - function test(literalString, literalValue) { - compile('
'); + $rootScope.name = 'c'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'c'}); + })); - expect(componentScope.owRef).toBe(literalValue); - dealoc(element); - } - })); + it('should work for primitive literals', inject(function() { + test('1', 1); + test('null', null); + test('undefined', undefined); + test('\'someString\'', 'someString'); + test('true', true); - describe('optional one-way binding', function() { - it('should update local when origin changes', inject(function() { - compile('
'); + function test(literalString, literalValue) { + compile('
'); - expect(componentScope.owOptref).toBeUndefined(); - expect(componentScope.owOptrefAlias).toBe(componentScope.owOptref); - expect(componentScope.$owOptrefAlias).toBe(componentScope.owOptref); + expect(componentScope.owRef).toBe(literalValue); + dealoc(element); + } + })); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.owOptref).toBe($rootScope.name); - expect(componentScope.owOptrefAlias).toBe($rootScope.name); - expect(componentScope.$owOptrefAlias).toBe($rootScope.name); + describe('optional one-way binding', function() { + it('should update local when origin changes', inject(function() { + compile('
'); - $rootScope.name = {}; - $rootScope.$apply(); - expect(componentScope.owOptref).toBe($rootScope.name); - expect(componentScope.owOptrefAlias).toBe($rootScope.name); - expect(componentScope.$owOptrefAlias).toBe($rootScope.name); - })); + expect(componentScope.owOptref).toBeUndefined(); + expect(componentScope.owOptrefAlias).toBe(componentScope.owOptref); + expect(componentScope.$owOptrefAlias).toBe(componentScope.owOptref); - it('should not throw exception when reference does not exist', inject(function() { - compile('
'); - - expect(componentScope.owOptref).toBeUndefined(); - expect(componentScope.owOptrefAlias).toBeUndefined(); - expect(componentScope.$owOptrefAlias).toBeUndefined(); - })); - }); - }); - }); - - describe('executable expression', function() { - it('should allow expression execution with locals', inject(function() { - compile('
'); - $rootScope.count = 2; + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.owOptref).toBe($rootScope.name); + expect(componentScope.owOptrefAlias).toBe($rootScope.name); + expect(componentScope.$owOptrefAlias).toBe($rootScope.name); - expect(typeof componentScope.expr).toBe('function'); - expect(typeof componentScope.exprAlias).toBe('function'); - expect(typeof componentScope.$exprAlias).toBe('function'); + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.owOptref).toBe($rootScope.name); + expect(componentScope.owOptrefAlias).toBe($rootScope.name); + expect(componentScope.$owOptrefAlias).toBe($rootScope.name); + })); - expect(componentScope.expr({offset: 1})).toEqual(3); - expect($rootScope.count).toEqual(3); + it('should not throw exception when reference does not exist', inject(function() { + compile('
'); - expect(componentScope.exprAlias({offset: 10})).toEqual(13); - expect(componentScope.$exprAlias({offset: 10})).toEqual(23); - expect($rootScope.count).toEqual(23); + expect(componentScope.owOptref).toBeUndefined(); + expect(componentScope.owOptrefAlias).toBeUndefined(); + expect(componentScope.$owOptrefAlias).toBeUndefined(); })); }); + }); + }); - it('should throw on unknown definition', inject(function() { - expect(function() { - compile('
'); - }).toThrowMinErr('$compile', 'iscp', 'Invalid isolate scope definition for directive \'badDeclaration\'. Definition: {... attr: \'xxx\' ...}'); - })); + describe('executable expression', function() { + it('should allow expression execution with locals', inject(function() { + compile('
'); + $rootScope.count = 2; - it('should expose a $$isolateBindings property onto the scope', inject(function() { - compile('
'); - - expect(typeof componentScope.$$isolateBindings).toBe('object'); - - expect(componentScope.$$isolateBindings.attr.mode).toBe('@'); - expect(componentScope.$$isolateBindings.attr.attrName).toBe('attr'); - expect(componentScope.$$isolateBindings.attrAlias.attrName).toBe('attr'); - expect(componentScope.$$isolateBindings.$attrAlias.attrName).toBe('$attr$'); - expect(componentScope.$$isolateBindings.ref.mode).toBe('='); - expect(componentScope.$$isolateBindings.ref.attrName).toBe('ref'); - expect(componentScope.$$isolateBindings.refAlias.attrName).toBe('ref'); - expect(componentScope.$$isolateBindings.$refAlias.attrName).toBe('$ref$'); - expect(componentScope.$$isolateBindings.reference.mode).toBe('='); - expect(componentScope.$$isolateBindings.reference.attrName).toBe('reference'); - expect(componentScope.$$isolateBindings.owRef.mode).toBe('<'); - expect(componentScope.$$isolateBindings.owRef.attrName).toBe('owRef'); - expect(componentScope.$$isolateBindings.owRefAlias.attrName).toBe('owRef'); - expect(componentScope.$$isolateBindings.$owRefAlias.attrName).toBe('$owRef$'); - expect(componentScope.$$isolateBindings.expr.mode).toBe('&'); - expect(componentScope.$$isolateBindings.expr.attrName).toBe('expr'); - expect(componentScope.$$isolateBindings.exprAlias.attrName).toBe('expr'); - expect(componentScope.$$isolateBindings.$exprAlias.attrName).toBe('$expr$'); - - var firstComponentScope = componentScope, - first$$isolateBindings = componentScope.$$isolateBindings; + expect(typeof componentScope.expr).toBe('function'); + expect(typeof componentScope.exprAlias).toBe('function'); + expect(typeof componentScope.$exprAlias).toBe('function'); - dealoc(element); - compile('
'); - expect(componentScope).not.toBe(firstComponentScope); - expect(componentScope.$$isolateBindings).toBe(first$$isolateBindings); - })); + expect(componentScope.expr({offset: 1})).toEqual(3); + expect($rootScope.count).toEqual(3); + expect(componentScope.exprAlias({offset: 10})).toEqual(13); + expect(componentScope.$exprAlias({offset: 10})).toEqual(23); + expect($rootScope.count).toEqual(23); + })); + }); - it('should expose isolate scope variables on controller with controllerAs when bindToController is true (template)', function() { - var controllerCalled = false; - module(function($compileProvider) { - $compileProvider.directive('fooDir', valueFn({ - template: '

isolate

', - scope: { - 'data': '=dirData', - 'oneway': '
')($rootScope); - expect(controllerCalled).toBe(true); - }); - }); + it('should throw on unknown definition', inject(function() { + expect(function() { + compile('
'); + }).toThrowMinErr('$compile', 'iscp', 'Invalid isolate scope definition for directive \'badDeclaration\'. Definition: {... attr: \'xxx\' ...}'); + })); + it('should expose a $$isolateBindings property onto the scope', inject(function() { + compile('
'); + + expect(typeof componentScope.$$isolateBindings).toBe('object'); + + expect(componentScope.$$isolateBindings.attr.mode).toBe('@'); + expect(componentScope.$$isolateBindings.attr.attrName).toBe('attr'); + expect(componentScope.$$isolateBindings.attrAlias.attrName).toBe('attr'); + expect(componentScope.$$isolateBindings.$attrAlias.attrName).toBe('$attr$'); + expect(componentScope.$$isolateBindings.ref.mode).toBe('='); + expect(componentScope.$$isolateBindings.ref.attrName).toBe('ref'); + expect(componentScope.$$isolateBindings.refAlias.attrName).toBe('ref'); + expect(componentScope.$$isolateBindings.$refAlias.attrName).toBe('$ref$'); + expect(componentScope.$$isolateBindings.reference.mode).toBe('='); + expect(componentScope.$$isolateBindings.reference.attrName).toBe('reference'); + expect(componentScope.$$isolateBindings.owRef.mode).toBe('<'); + expect(componentScope.$$isolateBindings.owRef.attrName).toBe('owRef'); + expect(componentScope.$$isolateBindings.owRefAlias.attrName).toBe('owRef'); + expect(componentScope.$$isolateBindings.$owRefAlias.attrName).toBe('$owRef$'); + expect(componentScope.$$isolateBindings.expr.mode).toBe('&'); + expect(componentScope.$$isolateBindings.expr.attrName).toBe('expr'); + expect(componentScope.$$isolateBindings.exprAlias.attrName).toBe('expr'); + expect(componentScope.$$isolateBindings.$exprAlias.attrName).toBe('$expr$'); + + var firstComponentScope = componentScope, + first$$isolateBindings = componentScope.$$isolateBindings; + + dealoc(element); + compile('
'); + expect(componentScope).not.toBe(firstComponentScope); + expect(componentScope.$$isolateBindings).toBe(first$$isolateBindings); + })); - it('should not pre-assign bound properties to the controller if `preAssignBindingsEnabled` is disabled', function() { - var controllerCalled = false, onInitCalled = false; - module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(false); - $compileProvider.directive('fooDir', valueFn({ - template: '

isolate

', - scope: { - 'data': '=dirData', - 'oneway': '
')($rootScope); - expect(controllerCalled).toBe(true); - expect(onInitCalled).toBe(true); - }); - }); - it('should pre-assign bound properties to the controller if `preAssignBindingsEnabled` is enabled', function() { - var controllerCalled = false, onInitCalled = false; - module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(true); - $compileProvider.directive('fooDir', valueFn({ - template: '

isolate

', - scope: { - 'data': '=dirData', - 'oneway': 'isolate

', + scope: { + 'data': '=dirData', + 'oneway': '
')($rootScope); - expect(controllerCalled).toBe(true); - expect(onInitCalled).toBe(true); - }); - }); - - it('should eventually expose isolate scope variables on ES6 class controller with controllerAs when bindToController is true', function() { - if (!/chrome/i.test(window.navigator.userAgent)) return; - var controllerCalled = false; - // eslint-disable-next-line no-eval - var Controller = eval( - 'class Foo {\n' + - ' constructor($scope) {}\n' + - ' $onInit() { this.check(); }\n' + - ' check() {\n' + - ' expect(this.data).toEqualData({\n' + - ' \'foo\': \'bar\',\n' + - ' \'baz\': \'biz\'\n' + - ' });\n' + - ' expect(this.oneway).toEqualData({\n' + - ' \'foo\': \'bar\',\n' + - ' \'baz\': \'biz\'\n' + - ' });\n' + - ' expect(this.str).toBe(\'Hello, world!\');\n' + - ' expect(this.fn()).toBe(\'called!\');\n' + - ' controllerCalled = true;\n' + - ' }\n' + - '}'); - spyOn(Controller.prototype, '$onInit').and.callThrough(); + controllerCalled = true; + }, + controllerAs: 'test', + bindToController: true + })); + }); + inject(function($compile, $rootScope) { + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + expect(controllerCalled).toBe(true); + }); + }); - module(function($compileProvider) { - $compileProvider.directive('fooDir', valueFn({ - template: '

isolate

', - scope: { - 'data': '=dirData', - 'oneway': 'isolate

', + scope: { + 'data': '=dirData', + 'oneway': '
')($rootScope); - expect(Controller.prototype.$onInit).toHaveBeenCalled(); - expect(controllerCalled).toBe(true); - }); - }); + }, + controllerAs: 'test', + bindToController: true + })); + }); + inject(function($compile, $rootScope) { + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + expect(controllerCalled).toBe(true); + expect(onInitCalled).toBe(true); + }); + }); + it('should eventually expose isolate scope variables on ES6 class controller with controllerAs when bindToController is true', function() { + if (!/chrome/i.test(window.navigator.userAgent)) return; + var controllerCalled = false; + // eslint-disable-next-line no-eval + var Controller = eval( + 'class Foo {\n' + + ' constructor($scope) {}\n' + + ' $onInit() {\n' + + ' expect(this.data).toEqualData({\n' + + ' \'foo\': \'bar\',\n' + + ' \'baz\': \'biz\'\n' + + ' });\n' + + ' expect(this.oneway).toEqualData({\n' + + ' \'foo\': \'bar\',\n' + + ' \'baz\': \'biz\'\n' + + ' });\n' + + ' expect(this.str).toBe(\'Hello, world!\');\n' + + ' expect(this.fn()).toBe(\'called!\');\n' + + ' controllerCalled = true;\n' + + ' }\n' + + '}'); + spyOn(Controller.prototype, '$onInit').and.callThrough(); - it('should update @-bindings on controller when bindToController and attribute change observed', function() { - module(function($compileProvider) { - $compileProvider.directive('atBinding', valueFn({ - template: '

{{At.text}}

', - scope: { - text: '@atBinding' - }, - controller: function($scope) {}, - bindToController: true, - controllerAs: 'At' - })); - }); + module(function($compileProvider) { + $compileProvider.directive('fooDir', valueFn({ + template: '

isolate

', + scope: { + 'data': '=dirData', + 'oneway': '
')($rootScope); + expect(Controller.prototype.$onInit).toHaveBeenCalled(); + expect(controllerCalled).toBe(true); + }); + }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - var p = element.find('p'); - $rootScope.$digest(); - expect(p.text()).toBe('Test: '); - $rootScope.text = 'Kittens'; - $rootScope.$digest(); - expect(p.text()).toBe('Test: Kittens'); - }); - }); + it('should update @-bindings on controller when bindToController and attribute change observed', function() { + module(function($compileProvider) { + $compileProvider.directive('atBinding', valueFn({ + template: '

{{At.text}}

', + scope: { + text: '@atBinding' + }, + controller: function($scope) {}, + bindToController: true, + controllerAs: 'At' + })); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + var p = element.find('p'); + $rootScope.$digest(); + expect(p.text()).toBe('Test: '); - it('should expose isolate scope variables on controller with controllerAs when bindToController is true (templateUrl)', function() { - var controllerCalled = false; - module(function($compileProvider) { - $compileProvider.directive('fooDir', valueFn({ - templateUrl: 'test.html', - scope: { - 'data': '=dirData', - 'oneway': 'isolate

'); - $rootScope.fn = valueFn('called!'); - $rootScope.whom = 'world'; - $rootScope.remoteData = { - 'foo': 'bar', - 'baz': 'biz' + $rootScope.text = 'Kittens'; + $rootScope.$digest(); + expect(p.text()).toBe('Test: Kittens'); + }); + }); + + + it('should expose isolate scope variables on controller with controllerAs when bindToController is true (templateUrl)', function() { + var controllerCalled = false; + module(function($compileProvider) { + $compileProvider.directive('fooDir', valueFn({ + templateUrl: 'test.html', + scope: { + 'data': '=dirData', + 'oneway': '
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); - }); - }); + controllerCalled = true; + }, + controllerAs: 'test', + bindToController: true + })); + }); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('test.html', '

isolate

'); + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); + }); + }); - it('should throw noctrl when missing controller', function() { - module(function($compileProvider) { - $compileProvider.directive('noCtrl', valueFn({ - templateUrl: 'test.html', - scope: { - 'data': '=dirData', - 'oneway': '')($rootScope); - }).toThrowMinErr('$compile', 'noctrl', - 'Cannot bind to controller without directive \'noCtrl\'s controller.'); - }); - }); + it('should throw noctrl when missing controller', function() { + module(function($compileProvider) { + $compileProvider.directive('noCtrl', valueFn({ + templateUrl: 'test.html', + scope: { + 'data': '=dirData', + 'oneway': '')($rootScope); + }).toThrowMinErr('$compile', 'noctrl', + 'Cannot bind to controller without directive \'noCtrl\'s controller.'); + }); + }); - it('should throw badrestrict on first compilation when restrict is invalid', function() { - module(function($compileProvider, $exceptionHandlerProvider) { - $compileProvider.directive('invalidRestrictBadString', valueFn({restrict: '"'})); - $compileProvider.directive('invalidRestrictTrue', valueFn({restrict: true})); - $compileProvider.directive('invalidRestrictObject', valueFn({restrict: {}})); - $compileProvider.directive('invalidRestrictNumber', valueFn({restrict: 42})); + it('should throw badrestrict on first compilation when restrict is invalid', function() { + module(function($compileProvider, $exceptionHandlerProvider) { + $compileProvider.directive('invalidRestrictBadString', valueFn({restrict: '"'})); + $compileProvider.directive('invalidRestrictTrue', valueFn({restrict: true})); + $compileProvider.directive('invalidRestrictObject', valueFn({restrict: {}})); + $compileProvider.directive('invalidRestrictNumber', valueFn({restrict: 42})); - // We need to test with the exceptionHandler not rethrowing... - $exceptionHandlerProvider.mode('log'); - }); + // We need to test with the exceptionHandler not rethrowing... + $exceptionHandlerProvider.mode('log'); + }); - inject(function($exceptionHandler, $compile, $rootScope) { - $compile('
')($rootScope); - expect($exceptionHandler.errors.length).toBe(1); - expect($exceptionHandler.errors[0]).toMatch(/\$compile.*badrestrict.*'true'/); + inject(function($exceptionHandler, $compile, $rootScope) { + $compile('
')($rootScope); + expect($exceptionHandler.errors.length).toBe(1); + expect($exceptionHandler.errors[0]).toMatch(/\$compile.*badrestrict.*'true'/); - $compile('
')($rootScope); - $compile('
')($rootScope); - expect($exceptionHandler.errors.length).toBe(2); - expect($exceptionHandler.errors[1]).toMatch(/\$compile.*badrestrict.*'"'/); + $compile('
')($rootScope); + $compile('
')($rootScope); + expect($exceptionHandler.errors.length).toBe(2); + expect($exceptionHandler.errors[1]).toMatch(/\$compile.*badrestrict.*'"'/); - $compile('
')($rootScope); - expect($exceptionHandler.errors.length).toBe(3); - expect($exceptionHandler.errors[2]).toMatch(/\$compile.*badrestrict.*'{}'/); + $compile('
')($rootScope); + expect($exceptionHandler.errors.length).toBe(3); + expect($exceptionHandler.errors[2]).toMatch(/\$compile.*badrestrict.*'{}'/); + + $compile('
')($rootScope); + expect($exceptionHandler.errors.length).toBe(4); + expect($exceptionHandler.errors[3]).toMatch(/\$compile.*badrestrict.*'42'/); + }); + }); - $compile('
')($rootScope); - expect($exceptionHandler.errors.length).toBe(4); - expect($exceptionHandler.errors[3]).toMatch(/\$compile.*badrestrict.*'42'/); - }); - }); + describe('should bind to controller via object notation', function() { + var controllerOptions = [{ + description: 'no controller identifier', + controller: 'myCtrl' + }, { + description: '"Ctrl as ident" syntax', + controller: 'myCtrl as myCtrl' + }, { + description: 'controllerAs setting', + controller: 'myCtrl', + controllerAs: 'myCtrl' + }], - describe('should bind to controller via object notation', function() { - var controllerOptions = [{ - description: 'no controller identifier', - controller: 'myCtrl' - }, { - description: '"Ctrl as ident" syntax', - controller: 'myCtrl as myCtrl' - }, { - description: 'controllerAs setting', - controller: 'myCtrl', - controllerAs: 'myCtrl' - }], + scopeOptions = [{ + description: 'isolate scope', + scope: {} + }, { + description: 'new scope', + scope: true + }, { + description: 'no scope', + scope: false + }], - scopeOptions = [{ - description: 'isolate scope', - scope: {} - }, { - description: 'new scope', - scope: true - }, { - description: 'no scope', - scope: false - }], - - templateOptions = [{ - description: 'inline template', - template: '

template

' - }, { - description: 'templateUrl setting', - templateUrl: 'test.html' - }, { - description: 'no template' - }]; - - forEach(controllerOptions, function(controllerOption) { - forEach(scopeOptions, function(scopeOption) { - forEach(templateOptions, function(templateOption) { - - var description = [], - ddo = { - bindToController: { - 'data': '=dirData', - 'oneway': 'template

' + }, { + description: 'templateUrl setting', + templateUrl: 'test.html' + }, { + description: 'no template' + }]; + + forEach(controllerOptions, function(controllerOption) { + forEach(scopeOptions, function(scopeOption) { + forEach(templateOptions, function(templateOption) { + + var description = [], + ddo = { + bindToController: { + 'data': '=dirData', + 'oneway': 'template

'); - $rootScope.fn = valueFn('called!'); - $rootScope.whom = 'world'; - $rootScope.remoteData = { + expect(this.oneway).toEqualData({ 'foo': 'bar', 'baz': 'biz' - }; - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); - if (ddo.controllerAs || ddo.controller.indexOf(' as ') !== -1) { - if (ddo.scope) { - expect($rootScope.myCtrl).toBeUndefined(); - } else { - // The controller identifier was added to the containing scope. - expect($rootScope.myCtrl).toBeDefined(); - } - } - }); + }); + expect(this.str).toBe('Hello, world!'); + expect(this.fn()).toBe('called!'); + }; + controllerCalled = true; }); - + $compileProvider.directive('fooDir', valueFn(ddo)); }); - }); - }); - - }); - - - it('should bind to multiple directives controllers via object notation (no scope)', function() { - var controller1Called = false; - var controller2Called = false; - module(function($compileProvider, $controllerProvider) { - $compileProvider.directive('foo', valueFn({ - bindToController: { - 'data': '=fooData', - 'oneway': 'template

'); + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' }; - controller2Called = true; - if (preAssignBindingsEnabled) { - this.check(); - } else { - this.$onInit = this.check; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); + if (ddo.controllerAs || ddo.controller.indexOf(' as ') !== -1) { + if (ddo.scope) { + expect($rootScope.myCtrl).toBeUndefined(); + } else { + // The controller identifier was added to the containing scope. + expect($rootScope.myCtrl).toBeDefined(); + } } - } - })); - }); - inject(function($compile, $rootScope) { - $rootScope.fn = valueFn('called!'); - $rootScope.string = 'world'; - $rootScope.data = {'foo': 'bar','baz': 'biz'}; - $rootScope.fn2 = valueFn('second called!'); - $rootScope.string2 = 'second world'; - $rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'}; - element = $compile( - '
' + - '
')($rootScope); - $rootScope.$digest(); - expect(controller1Called).toBe(true); - expect(controller2Called).toBe(true); - }); - }); - + }); + }); - it('should bind to multiple directives controllers via object notation (new iso scope)', function() { - var controller1Called = false; - var controller2Called = false; - module(function($compileProvider, $controllerProvider) { - $compileProvider.directive('foo', valueFn({ - bindToController: { - 'data': '=fooData', - 'oneway': ' ' + - '
')($rootScope); - $rootScope.$digest(); - expect(controller1Called).toBe(true); - expect(controller2Called).toBe(true); }); }); + }); + }); - it('should bind to multiple directives controllers via object notation (new scope)', function() { - var controller1Called = false; - var controller2Called = false; - module(function($compileProvider, $controllerProvider) { - $compileProvider.directive('foo', valueFn({ - bindToController: { - 'data': '=fooData', - 'oneway': ' ' + - '
')($rootScope); - $rootScope.$digest(); - expect(controller1Called).toBe(true); - expect(controller2Called).toBe(true); - }); - }); - - - it('should evaluate against the correct scope, when using `bindToController` (new scope)', - function() { - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register({ - 'ParentCtrl': function() { - this.value1 = 'parent1'; - this.value2 = 'parent2'; - this.value3 = function() { return 'parent3'; }; - this.value4 = 'parent4'; - }, - 'ChildCtrl': function() { - this.value1 = 'child1'; - this.value2 = 'child2'; - this.value3 = function() { return 'child3'; }; - this.value4 = 'child4'; - } - }); - - $compileProvider.directive('child', valueFn({ - scope: true, - controller: 'ChildCtrl as ctrl', - bindToController: { - fromParent1: '@', - fromParent2: '=', - fromParent3: '&', - fromParent4: '<' - }, - template: '' - })); - }); - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '' + - '' + - '
')($rootScope); - $rootScope.$digest(); + it('should bind to multiple directives controllers via object notation (no scope)', function() { + var controller1Called = false; + var controller2Called = false; + module(function($compileProvider, $controllerProvider) { + $compileProvider.directive('foo', valueFn({ + bindToController: { + 'data': '=fooData', + 'oneway': ' ' + + '
')($rootScope); + $rootScope.$digest(); + expect(controller1Called).toBe(true); + expect(controller2Called).toBe(true); + }); + }); - var parentCtrl = element.controller('ngController'); - var childCtrl = element.find('child').controller('child'); - expect(childCtrl.fromParent1).toBe(parentCtrl.value1); - expect(childCtrl.fromParent1).not.toBe(childCtrl.value1); - expect(childCtrl.fromParent2).toBe(parentCtrl.value2); - expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); - expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); - expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); - expect(childCtrl.fromParent4).toBe(parentCtrl.value4); - expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); + it('should bind to multiple directives controllers via object notation (new iso scope)', function() { + var controller1Called = false; + var controller2Called = false; + module(function($compileProvider, $controllerProvider) { + $compileProvider.directive('foo', valueFn({ + bindToController: { + 'data': '=fooData', + 'oneway': ' ' + + '
')($rootScope); + $rootScope.$digest(); + expect(controller1Called).toBe(true); + expect(controller2Called).toBe(true); + }); + }); - childCtrl.fromParent2 = 'modified'; - $rootScope.$digest(); - expect(parentCtrl.value2).toBe('modified'); - expect(childCtrl.value2).toBe('child2'); - }); + it('should bind to multiple directives controllers via object notation (new scope)', function() { + var controller1Called = false; + var controller2Called = false; + module(function($compileProvider, $controllerProvider) { + $compileProvider.directive('foo', valueFn({ + bindToController: { + 'data': '=fooData', + 'oneway': ' ' + + '
')($rootScope); + $rootScope.$digest(); + expect(controller1Called).toBe(true); + expect(controller2Called).toBe(true); + }); + }); - it('should evaluate against the correct scope, when using `bindToController` (new iso scope)', - function() { - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register({ - 'ParentCtrl': function() { - this.value1 = 'parent1'; - this.value2 = 'parent2'; - this.value3 = function() { return 'parent3'; }; - this.value4 = 'parent4'; - }, - 'ChildCtrl': function() { - this.value1 = 'child1'; - this.value2 = 'child2'; - this.value3 = function() { return 'child3'; }; - this.value4 = 'child4'; - } - }); + it('should evaluate against the correct scope, when using `bindToController` (new scope)', + function() { + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register({ + 'ParentCtrl': function() { + this.value1 = 'parent1'; + this.value2 = 'parent2'; + this.value3 = function() { return 'parent3'; }; + this.value4 = 'parent4'; + }, + 'ChildCtrl': function() { + this.value1 = 'child1'; + this.value2 = 'child2'; + this.value3 = function() { return 'child3'; }; + this.value4 = 'child4'; + } + }); - $compileProvider.directive('child', valueFn({ - scope: {}, - controller: 'ChildCtrl as ctrl', - bindToController: { - fromParent1: '@', - fromParent2: '=', - fromParent3: '&', - fromParent4: '<' - }, - template: '' - })); - }); + $compileProvider.directive('child', valueFn({ + scope: true, + controller: 'ChildCtrl as ctrl', + bindToController: { + fromParent1: '@', + fromParent2: '=', + fromParent3: '&', + fromParent4: '<' + }, + template: '' + })); + }); - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '' + - '' + - '
')($rootScope); - $rootScope.$digest(); + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); - var parentCtrl = element.controller('ngController'); - var childCtrl = element.find('child').controller('child'); + var parentCtrl = element.controller('ngController'); + var childCtrl = element.find('child').controller('child'); - expect(childCtrl.fromParent1).toBe(parentCtrl.value1); - expect(childCtrl.fromParent1).not.toBe(childCtrl.value1); - expect(childCtrl.fromParent2).toBe(parentCtrl.value2); - expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); - expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); - expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); - expect(childCtrl.fromParent4).toBe(parentCtrl.value4); - expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); + expect(childCtrl.fromParent1).toBe(parentCtrl.value1); + expect(childCtrl.fromParent1).not.toBe(childCtrl.value1); + expect(childCtrl.fromParent2).toBe(parentCtrl.value2); + expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); + expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); + expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); + expect(childCtrl.fromParent4).toBe(parentCtrl.value4); + expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); - childCtrl.fromParent2 = 'modified'; - $rootScope.$digest(); + childCtrl.fromParent2 = 'modified'; + $rootScope.$digest(); - expect(parentCtrl.value2).toBe('modified'); - expect(childCtrl.value2).toBe('child2'); - }); - } - ); + expect(parentCtrl.value2).toBe('modified'); + expect(childCtrl.value2).toBe('child2'); + }); + } + ); - it('should put controller in scope when controller identifier present but not using controllerAs', function() { - var controllerCalled = false; - var myCtrl; - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() { - controllerCalled = true; - myCtrl = this; - }); - $compileProvider.directive('fooDir', valueFn({ - templateUrl: 'test.html', - bindToController: {}, - scope: true, - controller: 'myCtrl as theCtrl' - })); - }); - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('test.html', '

isolate

'); - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); - var childScope = element.children().scope(); - expect(childScope).not.toBe($rootScope); - expect(childScope.theCtrl).toBe(myCtrl); + it('should evaluate against the correct scope, when using `bindToController` (new iso scope)', + function() { + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register({ + 'ParentCtrl': function() { + this.value1 = 'parent1'; + this.value2 = 'parent2'; + this.value3 = function() { return 'parent3'; }; + this.value4 = 'parent4'; + }, + 'ChildCtrl': function() { + this.value1 = 'child1'; + this.value2 = 'child2'; + this.value3 = function() { return 'child3'; }; + this.value4 = 'child4'; + } }); + + $compileProvider.directive('child', valueFn({ + scope: {}, + controller: 'ChildCtrl as ctrl', + bindToController: { + fromParent1: '@', + fromParent2: '=', + fromParent3: '&', + fromParent4: '<' + }, + template: '' + })); }); + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); - it('should re-install controllerAs and bindings for returned value from controller (new scope)', function() { - var controllerCalled = false; - var myCtrl; + var parentCtrl = element.controller('ngController'); + var childCtrl = element.find('child').controller('child'); - function MyCtrl() { - } - MyCtrl.prototype.test = function() { - expect(this.data).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.oneway).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.str).toBe('Hello, world!'); - expect(this.fn()).toBe('called!'); - }; + expect(childCtrl.fromParent1).toBe(parentCtrl.value1); + expect(childCtrl.fromParent1).not.toBe(childCtrl.value1); + expect(childCtrl.fromParent2).toBe(parentCtrl.value2); + expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); + expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); + expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); + expect(childCtrl.fromParent4).toBe(parentCtrl.value4); + expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() { - controllerCalled = true; - myCtrl = this; - return new MyCtrl(); - }); - $compileProvider.directive('fooDir', valueFn({ - templateUrl: 'test.html', - bindToController: { - 'data': '=dirData', - 'oneway': 'isolate

'); - $rootScope.fn = valueFn('called!'); - $rootScope.whom = 'world'; - $rootScope.remoteData = { - 'foo': 'bar', - 'baz': 'biz' - }; - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); - var childScope = element.children().scope(); - expect(childScope).not.toBe($rootScope); - expect(childScope.theCtrl).not.toBe(myCtrl); - expect(childScope.theCtrl.constructor).toBe(MyCtrl); - childScope.theCtrl.test(); - }); + childCtrl.fromParent2 = 'modified'; + $rootScope.$digest(); + + expect(parentCtrl.value2).toBe('modified'); + expect(childCtrl.value2).toBe('child2'); }); + } + ); - it('should re-install controllerAs and bindings for returned value from controller (isolate scope)', function() { - var controllerCalled = false; - var myCtrl; + it('should put controller in scope when controller identifier present but not using controllerAs', function() { + var controllerCalled = false; + var myCtrl; + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register('myCtrl', function() { + controllerCalled = true; + myCtrl = this; + }); + $compileProvider.directive('fooDir', valueFn({ + templateUrl: 'test.html', + bindToController: {}, + scope: true, + controller: 'myCtrl as theCtrl' + })); + }); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('test.html', '

isolate

'); + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); + var childScope = element.children().scope(); + expect(childScope).not.toBe($rootScope); + expect(childScope.theCtrl).toBe(myCtrl); + }); + }); - function MyCtrl() { - } - MyCtrl.prototype.test = function() { - expect(this.data).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.oneway).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.str).toBe('Hello, world!'); - expect(this.fn()).toBe('called!'); - }; - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() { - controllerCalled = true; - myCtrl = this; - return new MyCtrl(); - }); - $compileProvider.directive('fooDir', valueFn({ - templateUrl: 'test.html', - bindToController: true, - scope: { - 'data': '=dirData', - 'oneway': 'isolate

'); - $rootScope.fn = valueFn('called!'); - $rootScope.whom = 'world'; - $rootScope.remoteData = { - 'foo': 'bar', - 'baz': 'biz' - }; - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); - var childScope = element.children().scope(); - expect(childScope).not.toBe($rootScope); - expect(childScope.theCtrl).not.toBe(myCtrl); - expect(childScope.theCtrl.constructor).toBe(MyCtrl); - childScope.theCtrl.test(); - }); + it('should re-install controllerAs and bindings for returned value from controller (new scope)', function() { + var controllerCalled = false; + var myCtrl; + + function MyCtrl() { + } + MyCtrl.prototype.test = function() { + expect(this.data).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' + }); + expect(this.oneway).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' }); + expect(this.str).toBe('Hello, world!'); + expect(this.fn()).toBe('called!'); + }; - describe('should not overwrite @-bound property each digest when not present', function() { - it('when creating new scope', function() { - module(function($compileProvider) { - $compileProvider.directive('testDir', valueFn({ - scope: true, - bindToController: { - prop: '@' - }, - controller: function() { - var self = this; - this.initProp = function() { - this.prop = this.prop || 'default'; - }; - if (preAssignBindingsEnabled) { - this.initProp(); - } else { - this.$onInit = this.initProp; - } - this.getProp = function() { - return self.prop; - }; - }, - controllerAs: 'ctrl', - template: '

' - })); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - var scope = element.scope(); - expect(scope.ctrl.getProp()).toBe('default'); + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register('myCtrl', function() { + controllerCalled = true; + myCtrl = this; + return new MyCtrl(); + }); + $compileProvider.directive('fooDir', valueFn({ + templateUrl: 'test.html', + bindToController: { + 'data': '=dirData', + 'oneway': 'isolate

'); + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); + var childScope = element.children().scope(); + expect(childScope).not.toBe($rootScope); + expect(childScope.theCtrl).not.toBe(myCtrl); + expect(childScope.theCtrl.constructor).toBe(MyCtrl); + childScope.theCtrl.test(); + }); + }); - $rootScope.$digest(); - expect(scope.ctrl.getProp()).toBe('default'); - }); - }); - it('when creating isolate scope', function() { - module(function($compileProvider) { - $compileProvider.directive('testDir', valueFn({ - scope: {}, - bindToController: { - prop: '@' - }, - controller: function() { - var self = this; - this.initProp = function() { - this.prop = this.prop || 'default'; - }; - this.getProp = function() { - return self.prop; - }; - if (preAssignBindingsEnabled) { - this.initProp(); - } else { - this.$onInit = this.initProp; - } - }, - controllerAs: 'ctrl', - template: '

' - })); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - var scope = element.isolateScope(); - expect(scope.ctrl.getProp()).toBe('default'); + it('should re-install controllerAs and bindings for returned value from controller (isolate scope)', function() { + var controllerCalled = false; + var myCtrl; - $rootScope.$digest(); - expect(scope.ctrl.getProp()).toBe('default'); - }); - }); + function MyCtrl() { + } + MyCtrl.prototype.test = function() { + expect(this.data).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' + }); + expect(this.oneway).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' }); + expect(this.str).toBe('Hello, world!'); + expect(this.fn()).toBe('called!'); + }; + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register('myCtrl', function() { + controllerCalled = true; + myCtrl = this; + return new MyCtrl(); + }); + $compileProvider.directive('fooDir', valueFn({ + templateUrl: 'test.html', + bindToController: true, + scope: { + 'data': '=dirData', + 'oneway': 'isolate

'); + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); + var childScope = element.children().scope(); + expect(childScope).not.toBe($rootScope); + expect(childScope.theCtrl).not.toBe(myCtrl); + expect(childScope.theCtrl.constructor).toBe(MyCtrl); + childScope.theCtrl.test(); + }); + }); - describe('require', function() { - - it('should get required controller', function() { - module(function() { - directive('main', function(log) { - return { - priority: 2, - controller: function() { - this.name = 'main'; - }, - link: function(scope, element, attrs, controller) { - log(controller.name); - } - }; - }); - directive('dep', function(log) { - return { - priority: 1, - require: 'main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller.name); - } + describe('should not overwrite @-bound property each digest when not present', function() { + it('when creating new scope', function() { + module(function($compileProvider) { + $compileProvider.directive('testDir', valueFn({ + scope: true, + bindToController: { + prop: '@' + }, + controller: function() { + var self = this; + this.$onInit = function() { + this.prop = this.prop || 'default'; }; - }); - directive('other', function(log) { - return { - link: function(scope, element, attrs, controller) { - log(!!controller); // should be false - } + this.getProp = function() { + return self.prop; }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('false; dep:main; main'); - }); + }, + controllerAs: 'ctrl', + template: '

' + })); }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + var scope = element.scope(); + expect(scope.ctrl.getProp()).toBe('default'); + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('default'); + }); + }); - it('should respect explicit return value from controller', function() { - var expectedController; - module(function() { - directive('logControllerProp', function(log) { - return { - controller: function($scope) { - this.foo = 'baz'; // value should not be used. - expectedController = {foo: 'bar'}; - return expectedController; - }, - link: function(scope, element, attrs, controller) { - expect(expectedController).toBeDefined(); - expect(controller).toBe(expectedController); - expect(controller.foo).toBe('bar'); - log('done'); - } + it('when creating isolate scope', function() { + module(function($compileProvider) { + $compileProvider.directive('testDir', valueFn({ + scope: {}, + bindToController: { + prop: '@' + }, + controller: function() { + var self = this; + this.$onInit = function() { + this.prop = this.prop || 'default'; }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('')($rootScope); - expect(log).toEqual('done'); - expect(element.data('$logControllerPropController')).toBe(expectedController); - }); + this.getProp = function() { + return self.prop; + }; + }, + controllerAs: 'ctrl', + template: '

' + })); }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + var scope = element.isolateScope(); + expect(scope.ctrl.getProp()).toBe('default'); + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('default'); + }); + }); + }); - it('should get explicit return value of required parent controller', function() { - var expectedController; - module(function() { - directive('nested', function(log) { - return { - require: '^^?nested', - controller: function() { - if (!expectedController) expectedController = {foo: 'bar'}; - return expectedController; - }, - link: function(scope, element, attrs, controller) { - if (element.parent().length) { - expect(expectedController).toBeDefined(); - expect(controller).toBe(expectedController); - expect(controller.foo).toBe('bar'); - log('done'); - } - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('done'); - expect(element.data('$nestedController')).toBe(expectedController); - }); + }); + + describe('require', function() { + + it('should get required controller', function() { + module(function() { + directive('main', function(log) { + return { + priority: 2, + controller: function() { + this.name = 'main'; + }, + link: function(scope, element, attrs, controller) { + log(controller.name); + } + }; }); + directive('dep', function(log) { + return { + priority: 1, + require: 'main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; + }); + directive('other', function(log) { + return { + link: function(scope, element, attrs, controller) { + log(!!controller); // should be false + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('false; dep:main; main'); + }); + }); - it('should respect explicit controller return value when using controllerAs', function() { - module(function() { - directive('main', function() { - return { - templateUrl: 'main.html', - scope: {}, - controller: function() { - this.name = 'lucas'; - return {name: 'george'}; - }, - controllerAs: 'mainCtrl' - }; - }); - }); - inject(function($templateCache, $compile, $rootScope) { - $templateCache.put('main.html', 'template:{{mainCtrl.name}}'); - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toBe('template:george'); - }); + it('should respect explicit return value from controller', function() { + var expectedController; + module(function() { + directive('logControllerProp', function(log) { + return { + controller: function($scope) { + this.foo = 'baz'; // value should not be used. + expectedController = {foo: 'bar'}; + return expectedController; + }, + link: function(scope, element, attrs, controller) { + expect(expectedController).toBeDefined(); + expect(controller).toBe(expectedController); + expect(controller.foo).toBe('bar'); + log('done'); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual('done'); + expect(element.data('$logControllerPropController')).toBe(expectedController); + }); + }); - it('transcluded children should receive explicit return value of parent controller', function() { - var expectedController; - module(function() { - directive('nester', valueFn({ - transclude: true, - controller: function($transclude) { - this.foo = 'baz'; - expectedController = {transclude:$transclude, foo: 'bar'}; - return expectedController; - }, - link: function(scope, el, attr, ctrl) { - ctrl.transclude(cloneAttach); - function cloneAttach(clone) { - el.append(clone); - } + it('should get explicit return value of required parent controller', function() { + var expectedController; + module(function() { + directive('nested', function(log) { + return { + require: '^^?nested', + controller: function() { + if (!expectedController) expectedController = {foo: 'bar'}; + return expectedController; + }, + link: function(scope, element, attrs, controller) { + if (element.parent().length) { + expect(expectedController).toBeDefined(); + expect(controller).toBe(expectedController); + expect(controller.foo).toBe('bar'); + log('done'); } - })); - directive('nested', function(log) { - return { - require: '^^nester', - link: function(scope, element, attrs, controller) { - expect(controller).toBeDefined(); - expect(controller).toBe(expectedController); - log('done'); - } - }; - }); - }); - inject(function(log, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(log.toString()).toBe('done'); - expect(element.data('$nesterController')).toBe(expectedController); - }); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('done'); + expect(element.data('$nestedController')).toBe(expectedController); + }); + }); - it('explicit controller return values are ignored if they are primitives', function() { - module(function() { - directive('logControllerProp', function(log) { - return { - controller: function($scope) { - this.foo = 'baz'; // value *will* be used. - return 'bar'; - }, - link: function(scope, element, attrs, controller) { - log(controller.foo); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('')($rootScope); - expect(log).toEqual('baz'); - expect(element.data('$logControllerPropController').foo).toEqual('baz'); - }); + it('should respect explicit controller return value when using controllerAs', function() { + module(function() { + directive('main', function() { + return { + templateUrl: 'main.html', + scope: {}, + controller: function() { + this.name = 'lucas'; + return {name: 'george'}; + }, + controllerAs: 'mainCtrl' + }; }); + }); + inject(function($templateCache, $compile, $rootScope) { + $templateCache.put('main.html', 'template:{{mainCtrl.name}}'); + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('template:george'); + }); + }); - it('should correctly assign controller return values for multiple directives', function() { - var directiveController, otherDirectiveController; - module(function() { + it('transcluded children should receive explicit return value of parent controller', function() { + var expectedController; + module(function() { + directive('nester', valueFn({ + transclude: true, + controller: function($transclude) { + this.foo = 'baz'; + expectedController = {transclude:$transclude, foo: 'bar'}; + return expectedController; + }, + link: function(scope, el, attr, ctrl) { + ctrl.transclude(cloneAttach); + function cloneAttach(clone) { + el.append(clone); + } + } + })); + directive('nested', function(log) { + return { + require: '^^nester', + link: function(scope, element, attrs, controller) { + expect(controller).toBeDefined(); + expect(controller).toBe(expectedController); + log('done'); + } + }; + }); + }); + inject(function(log, $compile) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(log.toString()).toBe('done'); + expect(element.data('$nesterController')).toBe(expectedController); + }); + }); - directive('myDirective', function(log) { - return { - scope: true, - controller: function($scope) { - directiveController = { - foo: 'bar' - }; - return directiveController; - } + + it('explicit controller return values are ignored if they are primitives', function() { + module(function() { + directive('logControllerProp', function(log) { + return { + controller: function($scope) { + this.foo = 'baz'; // value *will* be used. + return 'bar'; + }, + link: function(scope, element, attrs, controller) { + log(controller.foo); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual('baz'); + expect(element.data('$logControllerPropController').foo).toEqual('baz'); + }); + }); + + + it('should correctly assign controller return values for multiple directives', function() { + var directiveController, otherDirectiveController; + module(function() { + + directive('myDirective', function(log) { + return { + scope: true, + controller: function($scope) { + directiveController = { + foo: 'bar' }; - }); + return directiveController; + } + }; + }); - directive('myOtherDirective', function(log) { - return { - controller: function($scope) { - otherDirectiveController = { - baz: 'luh' - }; - return otherDirectiveController; - } + directive('myOtherDirective', function(log) { + return { + controller: function($scope) { + otherDirectiveController = { + baz: 'luh' }; - }); + return otherDirectiveController; + } + }; + }); - }); + }); - inject(function(log, $compile, $rootScope) { - element = $compile('')($rootScope); - expect(element.data('$myDirectiveController')).toBe(directiveController); - expect(element.data('$myOtherDirectiveController')).toBe(otherDirectiveController); - }); + inject(function(log, $compile, $rootScope) { + element = $compile('')($rootScope); + expect(element.data('$myDirectiveController')).toBe(directiveController); + expect(element.data('$myOtherDirectiveController')).toBe(otherDirectiveController); + }); + }); + + + it('should get required parent controller', function() { + module(function() { + directive('nested', function(log) { + return { + require: '^^?nested', + controller: function($scope) {}, + link: function(scope, element, attrs, controller) { + log(!!controller); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('true; false'); + }); + }); - it('should get required parent controller', function() { - module(function() { - directive('nested', function(log) { - return { - require: '^^?nested', - controller: function($scope) {}, - link: function(scope, element, attrs, controller) { - log(!!controller); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('true; false'); - }); + it('should get required parent controller when the question mark precedes the ^^', function() { + module(function() { + directive('nested', function(log) { + return { + require: '?^^nested', + controller: function($scope) {}, + link: function(scope, element, attrs, controller) { + log(!!controller); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('true; false'); + }); + }); - it('should get required parent controller when the question mark precedes the ^^', function() { - module(function() { - directive('nested', function(log) { - return { - require: '?^^nested', - controller: function($scope) {}, - link: function(scope, element, attrs, controller) { - log(!!controller); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('true; false'); - }); + it('should throw if required parent is not found', function() { + module(function() { + directive('nested', function() { + return { + require: '^^nested', + controller: function($scope) {}, + link: function(scope, element, attrs, controller) {} + }; }); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile('
')($rootScope); + }).toThrowMinErr('$compile', 'ctreq', 'Controller \'nested\', required by directive \'nested\', can\'t be found!'); + }); + }); - it('should throw if required parent is not found', function() { - module(function() { - directive('nested', function() { - return { - require: '^^nested', - controller: function($scope) {}, - link: function(scope, element, attrs, controller) {} - }; - }); - }); - inject(function($compile, $rootScope) { - expect(function() { - element = $compile('
')($rootScope); - }).toThrowMinErr('$compile', 'ctreq', 'Controller \'nested\', required by directive \'nested\', can\'t be found!'); - }); + it('should get required controller via linkingFn (template)', function() { + module(function() { + directive('dirA', function() { + return { + controller: function() { + this.name = 'dirA'; + } + }; + }); + directive('dirB', function(log) { + return { + require: 'dirA', + template: '

dirB

', + link: function(scope, element, attrs, dirAController) { + log('dirAController.name: ' + dirAController.name); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dirAController.name: dirA'); + }); + }); - it('should get required controller via linkingFn (template)', function() { - module(function() { - directive('dirA', function() { - return { - controller: function() { - this.name = 'dirA'; - } - }; - }); - directive('dirB', function(log) { - return { - require: 'dirA', - template: '

dirB

', - link: function(scope, element, attrs, dirAController) { - log('dirAController.name: ' + dirAController.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dirAController.name: dirA'); - }); + it('should get required controller via linkingFn (templateUrl)', function() { + module(function() { + directive('dirA', function() { + return { + controller: function() { + this.name = 'dirA'; + } + }; }); + directive('dirB', function(log) { + return { + require: 'dirA', + templateUrl: 'dirB.html', + link: function(scope, element, attrs, dirAController) { + log('dirAController.name: ' + dirAController.name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope, $templateCache) { + $templateCache.put('dirB.html', '

dirB

'); + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(log).toEqual('dirAController.name: dirA'); + }); + }); + it('should bind the required controllers to the directive controller, if provided as an object and bindToController is truthy', function() { + var parentController, siblingController; - it('should get required controller via linkingFn (templateUrl)', function() { - module(function() { - directive('dirA', function() { - return { - controller: function() { - this.name = 'dirA'; - } - }; - }); - directive('dirB', function(log) { - return { - require: 'dirA', - templateUrl: 'dirB.html', - link: function(scope, element, attrs, dirAController) { - log('dirAController.name: ' + dirAController.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope, $templateCache) { - $templateCache.put('dirB.html', '

dirB

'); - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(log).toEqual('dirAController.name: dirA'); - }); + function ParentController() { this.name = 'Parent'; } + function SiblingController() { this.name = 'Sibling'; } + function MeController() { this.name = 'Me'; } + MeController.prototype.$onInit = function() { + parentController = this.container; + siblingController = this.friend; + }; + spyOn(MeController.prototype, '$onInit').and.callThrough(); + + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + bindToController: true, + controller: MeController, + controllerAs: '$ctrl' + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; + }); + + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(parentController).toEqual(jasmine.any(ParentController)); + expect(siblingController).toEqual(jasmine.any(SiblingController)); + }); + }); + + it('should use the key if the name of a required controller is omitted', function() { + function ParentController() { this.name = 'Parent'; } + function ParentOptController() { this.name = 'ParentOpt'; } + function ParentOrSiblingController() { this.name = 'ParentOrSibling'; } + function ParentOrSiblingOptController() { this.name = 'ParentOrSiblingOpt'; } + function SiblingController() { this.name = 'Sibling'; } + function SiblingOptController() { this.name = 'SiblingOpt'; } + + angular.module('my', []) + .component('me', { + require: { + parent: '^^', + parentOpt: '?^^', + parentOrSibling1: '^', + parentOrSiblingOpt1: '?^', + parentOrSibling2: '^', + parentOrSiblingOpt2: '?^', + sibling: '', + siblingOpt: '?' + } + }) + .directive('parent', function() { + return {controller: ParentController}; + }) + .directive('parentOpt', function() { + return {controller: ParentOptController}; + }) + .directive('parentOrSibling1', function() { + return {controller: ParentOrSiblingController}; + }) + .directive('parentOrSiblingOpt1', function() { + return {controller: ParentOrSiblingOptController}; + }) + .directive('parentOrSibling2', function() { + return {controller: ParentOrSiblingController}; + }) + .directive('parentOrSiblingOpt2', function() { + return {controller: ParentOrSiblingOptController}; + }) + .directive('sibling', function() { + return {controller: SiblingController}; + }) + .directive('siblingOpt', function() { + return {controller: SiblingOptController}; }); - it('should bind the required controllers to the directive controller, if provided as an object and bindToController is truthy', function() { - var parentController, siblingController; + module('my'); + inject(function($compile, $rootScope) { + var template = + '
' + + // With optional + '' + + '' + + '' + + // Without optional + '' + + '' + + '' + + '
'; + element = $compile(template)($rootScope); + + var ctrl1 = element.find('me').eq(0).controller('me'); + expect(ctrl1.parent).toEqual(jasmine.any(ParentController)); + expect(ctrl1.parentOpt).toEqual(jasmine.any(ParentOptController)); + expect(ctrl1.parentOrSibling1).toEqual(jasmine.any(ParentOrSiblingController)); + expect(ctrl1.parentOrSiblingOpt1).toEqual(jasmine.any(ParentOrSiblingOptController)); + expect(ctrl1.parentOrSibling2).toEqual(jasmine.any(ParentOrSiblingController)); + expect(ctrl1.parentOrSiblingOpt2).toEqual(jasmine.any(ParentOrSiblingOptController)); + expect(ctrl1.sibling).toEqual(jasmine.any(SiblingController)); + expect(ctrl1.siblingOpt).toEqual(jasmine.any(SiblingOptController)); + + var ctrl2 = element.find('me').eq(1).controller('me'); + expect(ctrl2.parent).toEqual(jasmine.any(ParentController)); + expect(ctrl2.parentOpt).toBe(null); + expect(ctrl2.parentOrSibling1).toEqual(jasmine.any(ParentOrSiblingController)); + expect(ctrl2.parentOrSiblingOpt1).toBe(null); + expect(ctrl2.parentOrSibling2).toEqual(jasmine.any(ParentOrSiblingController)); + expect(ctrl2.parentOrSiblingOpt2).toBe(null); + expect(ctrl2.sibling).toEqual(jasmine.any(SiblingController)); + expect(ctrl2.siblingOpt).toBe(null); + }); + }); - function ParentController() { this.name = 'Parent'; } - function SiblingController() { this.name = 'Sibling'; } - function MeController() { this.name = 'Me'; } - MeController.prototype.$onInit = function() { - parentController = this.container; - siblingController = this.friend; + + it('should not bind required controllers if bindToController is falsy', function() { + var parentController, siblingController; + + function ParentController() { this.name = 'Parent'; } + function SiblingController() { this.name = 'Sibling'; } + function MeController() { this.name = 'Me'; } + MeController.prototype.$onInit = function() { + parentController = this.container; + siblingController = this.friend; + }; + spyOn(MeController.prototype, '$onInit').and.callThrough(); + + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + controller: MeController }; - spyOn(MeController.prototype, '$onInit').and.callThrough(); + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; + }); - angular.module('my', []) - .directive('me', function() { - return { - restrict: 'E', - scope: {}, - require: { container: '^parent', friend: 'sibling' }, - bindToController: true, - controller: MeController, - controllerAs: '$ctrl' - }; - }) - .directive('parent', function() { - return { - restrict: 'E', - scope: {}, - controller: ParentController - }; - }) - .directive('sibling', function() { - return { - controller: SiblingController - }; - }); + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(parentController).toBeUndefined(); + expect(siblingController).toBeUndefined(); + }); + }); - module('my'); - inject(function($compile, $rootScope, meDirective) { - element = $compile('')($rootScope); - expect(MeController.prototype.$onInit).toHaveBeenCalled(); - expect(parentController).toEqual(jasmine.any(ParentController)); - expect(siblingController).toEqual(jasmine.any(SiblingController)); - }); - }); - - it('should use the key if the name of a required controller is omitted', function() { - function ParentController() { this.name = 'Parent'; } - function ParentOptController() { this.name = 'ParentOpt'; } - function ParentOrSiblingController() { this.name = 'ParentOrSibling'; } - function ParentOrSiblingOptController() { this.name = 'ParentOrSiblingOpt'; } - function SiblingController() { this.name = 'Sibling'; } - function SiblingOptController() { this.name = 'SiblingOpt'; } - - angular.module('my', []) - .component('me', { - require: { - parent: '^^', - parentOpt: '?^^', - parentOrSibling1: '^', - parentOrSiblingOpt1: '?^', - parentOrSibling2: '^', - parentOrSiblingOpt2: '?^', - sibling: '', - siblingOpt: '?' - } - }) - .directive('parent', function() { - return {controller: ParentController}; - }) - .directive('parentOpt', function() { - return {controller: ParentOptController}; - }) - .directive('parentOrSibling1', function() { - return {controller: ParentOrSiblingController}; - }) - .directive('parentOrSiblingOpt1', function() { - return {controller: ParentOrSiblingOptController}; - }) - .directive('parentOrSibling2', function() { - return {controller: ParentOrSiblingController}; - }) - .directive('parentOrSiblingOpt2', function() { - return {controller: ParentOrSiblingOptController}; - }) - .directive('sibling', function() { - return {controller: SiblingController}; - }) - .directive('siblingOpt', function() { - return {controller: SiblingOptController}; - }); + it('should bind required controllers to controller that has an explicit constructor return value', function() { + var parentController, siblingController, meController; - module('my'); - inject(function($compile, $rootScope) { - var template = - '
' + - // With optional - '' + - '' + - '' + - // Without optional - '' + - '' + - '' + - '
'; - element = $compile(template)($rootScope); - - var ctrl1 = element.find('me').eq(0).controller('me'); - expect(ctrl1.parent).toEqual(jasmine.any(ParentController)); - expect(ctrl1.parentOpt).toEqual(jasmine.any(ParentOptController)); - expect(ctrl1.parentOrSibling1).toEqual(jasmine.any(ParentOrSiblingController)); - expect(ctrl1.parentOrSiblingOpt1).toEqual(jasmine.any(ParentOrSiblingOptController)); - expect(ctrl1.parentOrSibling2).toEqual(jasmine.any(ParentOrSiblingController)); - expect(ctrl1.parentOrSiblingOpt2).toEqual(jasmine.any(ParentOrSiblingOptController)); - expect(ctrl1.sibling).toEqual(jasmine.any(SiblingController)); - expect(ctrl1.siblingOpt).toEqual(jasmine.any(SiblingOptController)); - - var ctrl2 = element.find('me').eq(1).controller('me'); - expect(ctrl2.parent).toEqual(jasmine.any(ParentController)); - expect(ctrl2.parentOpt).toBe(null); - expect(ctrl2.parentOrSibling1).toEqual(jasmine.any(ParentOrSiblingController)); - expect(ctrl2.parentOrSiblingOpt1).toBe(null); - expect(ctrl2.parentOrSibling2).toEqual(jasmine.any(ParentOrSiblingController)); - expect(ctrl2.parentOrSiblingOpt2).toBe(null); - expect(ctrl2.sibling).toEqual(jasmine.any(SiblingController)); - expect(ctrl2.siblingOpt).toBe(null); - }); - }); - - - it('should not bind required controllers if bindToController is falsy', function() { - var parentController, siblingController; - - function ParentController() { this.name = 'Parent'; } - function SiblingController() { this.name = 'Sibling'; } - function MeController() { this.name = 'Me'; } - MeController.prototype.$onInit = function() { + function ParentController() { this.name = 'Parent'; } + function SiblingController() { this.name = 'Sibling'; } + function MeController() { + meController = { + name: 'Me', + $onInit: function() { parentController = this.container; siblingController = this.friend; - }; - spyOn(MeController.prototype, '$onInit').and.callThrough(); - - angular.module('my', []) - .directive('me', function() { - return { - restrict: 'E', - scope: {}, - require: { container: '^parent', friend: 'sibling' }, - controller: MeController - }; - }) - .directive('parent', function() { - return { - restrict: 'E', - scope: {}, - controller: ParentController - }; - }) - .directive('sibling', function() { - return { - controller: SiblingController - }; - }); + } + }; + spyOn(meController, '$onInit').and.callThrough(); + return meController; + } - module('my'); - inject(function($compile, $rootScope, meDirective) { - element = $compile('')($rootScope); - expect(MeController.prototype.$onInit).toHaveBeenCalled(); - expect(parentController).toBeUndefined(); - expect(siblingController).toBeUndefined(); - }); + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + bindToController: true, + controller: MeController, + controllerAs: '$ctrl' + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; }); - it('should bind required controllers to controller that has an explicit constructor return value', function() { - var parentController, siblingController, meController; + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(meController.$onInit).toHaveBeenCalled(); + expect(parentController).toEqual(jasmine.any(ParentController)); + expect(siblingController).toEqual(jasmine.any(SiblingController)); + }); + }); - function ParentController() { this.name = 'Parent'; } - function SiblingController() { this.name = 'Sibling'; } - function MeController() { - meController = { - name: 'Me', - $onInit: function() { - parentController = this.container; - siblingController = this.friend; - } - }; - spyOn(meController, '$onInit').and.callThrough(); - return meController; - } - angular.module('my', []) - .directive('me', function() { - return { - restrict: 'E', - scope: {}, - require: { container: '^parent', friend: 'sibling' }, - bindToController: true, - controller: MeController, - controllerAs: '$ctrl' - }; - }) - .directive('parent', function() { - return { - restrict: 'E', - scope: {}, - controller: ParentController - }; - }) - .directive('sibling', function() { - return { - controller: SiblingController - }; - }); + it('should bind required controllers to controllers that return an explicit constructor return value', function() { + var parentController, containerController, siblingController, friendController, meController; - module('my'); - inject(function($compile, $rootScope, meDirective) { - element = $compile('')($rootScope); - expect(meController.$onInit).toHaveBeenCalled(); - expect(parentController).toEqual(jasmine.any(ParentController)); - expect(siblingController).toEqual(jasmine.any(SiblingController)); - }); + function MeController() { + this.name = 'Me'; + this.$onInit = function() { + containerController = this.container; + friendController = this.friend; + }; + } + function ParentController() { + parentController = { name: 'Parent' }; + return parentController; + } + function SiblingController() { + siblingController = { name: 'Sibling' }; + return siblingController; + } + + angular.module('my', []) + .directive('me', function() { + return { + priority: 1, // make sure it is run before sibling to test this case correctly + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + bindToController: true, + controller: MeController, + controllerAs: '$ctrl' + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; }); + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(containerController).toEqual(parentController); + expect(friendController).toEqual(siblingController); + }); + }); - it('should bind required controllers to controllers that return an explicit constructor return value', function() { - var parentController, containerController, siblingController, friendController, meController; + it('should require controller of an isolate directive from a non-isolate directive on the ' + + 'same element', function() { + var IsolateController = function() {}; + var isolateDirControllerInNonIsolateDirective; - function MeController() { - this.name = 'Me'; - this.$onInit = function() { - containerController = this.container; - friendController = this.friend; - }; - } - function ParentController() { - parentController = { name: 'Parent' }; - return parentController; - } - function SiblingController() { - siblingController = { name: 'Sibling' }; - return siblingController; - } + module(function() { + directive('isolate', function() { + return { + scope: {}, + controller: IsolateController + }; + }); + directive('nonIsolate', function() { + return { + require: 'isolate', + link: function(_, __, ___, isolateDirController) { + isolateDirControllerInNonIsolateDirective = isolateDirController; + } + }; + }); + }); - angular.module('my', []) - .directive('me', function() { - return { - priority: 1, // make sure it is run before sibling to test this case correctly - restrict: 'E', - scope: {}, - require: { container: '^parent', friend: 'sibling' }, - bindToController: true, - controller: MeController, - controllerAs: '$ctrl' - }; - }) - .directive('parent', function() { - return { - restrict: 'E', - scope: {}, - controller: ParentController - }; - }) - .directive('sibling', function() { - return { - controller: SiblingController - }; - }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); - module('my'); - inject(function($compile, $rootScope, meDirective) { - element = $compile('')($rootScope); - expect(containerController).toEqual(parentController); - expect(friendController).toEqual(siblingController); - }); + expect(isolateDirControllerInNonIsolateDirective).toBeDefined(); + expect(isolateDirControllerInNonIsolateDirective instanceof IsolateController).toBe(true); + }); + }); + + + it('should give the isolate scope to the controller of another replaced directives in the template', function() { + module(function() { + directive('testDirective', function() { + return { + replace: true, + restrict: 'E', + scope: {}, + template: '' + }; }); + }); - it('should require controller of an isolate directive from a non-isolate directive on the ' + - 'same element', function() { - var IsolateController = function() {}; - var isolateDirControllerInNonIsolateDirective; + inject(function($rootScope) { + compile('
'); - module(function() { - directive('isolate', function() { - return { - scope: {}, - controller: IsolateController - }; - }); - directive('nonIsolate', function() { - return { - require: 'isolate', - link: function(_, __, ___, isolateDirController) { - isolateDirControllerInNonIsolateDirective = isolateDirController; - } - }; - }); - }); + element = element.children().eq(0); + expect(element[0].checked).toBe(false); + element.isolateScope().model = true; + $rootScope.$digest(); + expect(element[0].checked).toBe(true); + }); + }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - expect(isolateDirControllerInNonIsolateDirective).toBeDefined(); - expect(isolateDirControllerInNonIsolateDirective instanceof IsolateController).toBe(true); - }); + it('should share isolate scope with replaced directives (template)', function() { + var normalScope; + var isolateScope; + + module(function() { + directive('isolate', function() { + return { + replace: true, + scope: {}, + template: '{{name}}', + link: function(s) { + isolateScope = s; + } + }; + }); + directive('nonIsolate', function() { + return { + link: function(s) { + normalScope = s; + } + }; }); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); - it('should give the isolate scope to the controller of another replaced directives in the template', function() { - module(function() { - directive('testDirective', function() { - return { - replace: true, - restrict: 'E', - scope: {}, - template: '' - }; - }); - }); + expect(normalScope).toBe($rootScope); + expect(normalScope.name).toEqual(undefined); + expect(isolateScope.name).toEqual('WORKS'); + $rootScope.$digest(); + expect(element.text()).toEqual('WORKS'); + }); + }); - inject(function($rootScope) { - compile('
'); - element = element.children().eq(0); - expect(element[0].checked).toBe(false); - element.isolateScope().model = true; - $rootScope.$digest(); - expect(element[0].checked).toBe(true); - }); + it('should share isolate scope with replaced directives (templateUrl)', function() { + var normalScope; + var isolateScope; + + module(function() { + directive('isolate', function() { + return { + replace: true, + scope: {}, + templateUrl: 'main.html', + link: function(s) { + isolateScope = s; + } + }; + }); + directive('nonIsolate', function() { + return { + link: function(s) { + normalScope = s; + } + }; }); + }); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('main.html', '{{name}}'); + element = $compile('
')($rootScope); + $rootScope.$apply(); - it('should share isolate scope with replaced directives (template)', function() { - var normalScope; - var isolateScope; + expect(normalScope).toBe($rootScope); + expect(normalScope.name).toEqual(undefined); + expect(isolateScope.name).toEqual('WORKS'); + expect(element.text()).toEqual('WORKS'); + }); + }); - module(function() { - directive('isolate', function() { - return { - replace: true, - scope: {}, - template: '{{name}}', - link: function(s) { - isolateScope = s; - } - }; - }); - directive('nonIsolate', function() { - return { - link: function(s) { - normalScope = s; - } - }; - }); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); + it('should not get confused about where to use isolate scope when a replaced directive is used multiple times', + function() { - expect(normalScope).toBe($rootScope); - expect(normalScope.name).toEqual(undefined); - expect(isolateScope.name).toEqual('WORKS'); - $rootScope.$digest(); - expect(element.text()).toEqual('WORKS'); - }); + module(function() { + directive('isolate', function() { + return { + replace: true, + scope: {}, + template: '' + }; + }); + directive('scopeTester', function(log) { + return { + link: function($scope, $element) { + log($element.attr('scope-tester') + '=' + ($scope.$root === $scope ? 'non-isolate' : 'isolate')); + } + }; }); + }); + inject(function($compile, $rootScope, log) { + element = $compile('
' + + '
' + + '' + + '
')($rootScope); - it('should share isolate scope with replaced directives (templateUrl)', function() { - var normalScope; - var isolateScope; + $rootScope.$digest(); + expect(log).toEqual('inside=isolate; ' + + 'outside replaced=non-isolate; ' + // outside + 'outside replaced=isolate; ' + // replaced + 'sibling=non-isolate'); + }); + }); - module(function() { - directive('isolate', function() { - return { - replace: true, - scope: {}, - templateUrl: 'main.html', - link: function(s) { - isolateScope = s; - } - }; - }); - directive('nonIsolate', function() { - return { - link: function(s) { - normalScope = s; - } - }; - }); - }); - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('main.html', '{{name}}'); - element = $compile('
')($rootScope); - $rootScope.$apply(); + it('should require controller of a non-isolate directive from an isolate directive on the ' + + 'same element', function() { + var NonIsolateController = function() {}; + var nonIsolateDirControllerInIsolateDirective; - expect(normalScope).toBe($rootScope); - expect(normalScope.name).toEqual(undefined); - expect(isolateScope.name).toEqual('WORKS'); - expect(element.text()).toEqual('WORKS'); - }); + module(function() { + directive('isolate', function() { + return { + scope: {}, + require: 'nonIsolate', + link: function(_, __, ___, nonIsolateDirController) { + nonIsolateDirControllerInIsolateDirective = nonIsolateDirController; + } + }; }); + directive('nonIsolate', function() { + return { + controller: NonIsolateController + }; + }); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); - it('should not get confused about where to use isolate scope when a replaced directive is used multiple times', - function() { - - module(function() { - directive('isolate', function() { - return { - replace: true, - scope: {}, - template: '' - }; - }); - directive('scopeTester', function(log) { - return { - link: function($scope, $element) { - log($element.attr('scope-tester') + '=' + ($scope.$root === $scope ? 'non-isolate' : 'isolate')); - } - }; - }); - }); + expect(nonIsolateDirControllerInIsolateDirective).toBeDefined(); + expect(nonIsolateDirControllerInIsolateDirective instanceof NonIsolateController).toBe(true); + }); + }); - inject(function($compile, $rootScope, log) { - element = $compile('
' + - '
' + - '' + - '
')($rootScope); - $rootScope.$digest(); - expect(log).toEqual('inside=isolate; ' + - 'outside replaced=non-isolate; ' + // outside - 'outside replaced=isolate; ' + // replaced - 'sibling=non-isolate'); - }); + it('should support controllerAs', function() { + module(function() { + directive('main', function() { + return { + templateUrl: 'main.html', + transclude: true, + scope: {}, + controller: function() { + this.name = 'lucas'; + }, + controllerAs: 'mainCtrl' + }; }); + }); + inject(function($templateCache, $compile, $rootScope) { + $templateCache.put('main.html', 'template:{{mainCtrl.name}}
'); + element = $compile('
transclude:{{mainCtrl.name}}
')($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('template:lucas transclude:'); + }); + }); - it('should require controller of a non-isolate directive from an isolate directive on the ' + - 'same element', function() { - var NonIsolateController = function() {}; - var nonIsolateDirControllerInIsolateDirective; + it('should support controller alias', function() { + module(function($controllerProvider) { + $controllerProvider.register('MainCtrl', function() { + this.name = 'lucas'; + }); + directive('main', function() { + return { + templateUrl: 'main.html', + scope: {}, + controller: 'MainCtrl as mainCtrl' + }; + }); + }); + inject(function($templateCache, $compile, $rootScope) { + $templateCache.put('main.html', '{{mainCtrl.name}}'); + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('lucas'); + }); + }); - module(function() { - directive('isolate', function() { - return { - scope: {}, - require: 'nonIsolate', - link: function(_, __, ___, nonIsolateDirController) { - nonIsolateDirControllerInIsolateDirective = nonIsolateDirController; - } - }; - }); - directive('nonIsolate', function() { - return { - controller: NonIsolateController - }; - }); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - expect(nonIsolateDirControllerInIsolateDirective).toBeDefined(); - expect(nonIsolateDirControllerInIsolateDirective instanceof NonIsolateController).toBe(true); - }); + it('should require controller on parent element',function() { + module(function() { + directive('main', function(log) { + return { + controller: function() { + this.name = 'main'; + } + }; + }); + directive('dep', function(log) { + return { + require: '^main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:main'); + }); + }); - it('should support controllerAs', function() { - module(function() { - directive('main', function() { - return { - templateUrl: 'main.html', - transclude: true, - scope: {}, - controller: function() { - this.name = 'lucas'; - }, - controllerAs: 'mainCtrl' - }; - }); - }); - inject(function($templateCache, $compile, $rootScope) { - $templateCache.put('main.html', 'template:{{mainCtrl.name}}
'); - element = $compile('
transclude:{{mainCtrl.name}}
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toBe('template:lucas transclude:'); - }); + it('should throw an error if required controller can\'t be found',function() { + module(function() { + directive('dep', function(log) { + return { + require: '^main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + expect(function() { + $compile('
')($rootScope); + }).toThrowMinErr('$compile', 'ctreq', 'Controller \'main\', required by directive \'dep\', can\'t be found!'); + }); + }); - it('should support controller alias', function() { - module(function($controllerProvider) { - $controllerProvider.register('MainCtrl', function() { - this.name = 'lucas'; - }); - directive('main', function() { - return { - templateUrl: 'main.html', - scope: {}, - controller: 'MainCtrl as mainCtrl' - }; - }); - }); - inject(function($templateCache, $compile, $rootScope) { - $templateCache.put('main.html', '{{mainCtrl.name}}'); - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toBe('lucas'); - }); + it('should pass null if required controller can\'t be found and is optional',function() { + module(function() { + directive('dep', function(log) { + return { + require: '?^main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + $compile('
')($rootScope); + expect(log).toEqual('dep:null'); + }); + }); + it('should pass null if required controller can\'t be found and is optional with the question mark on the right',function() { + module(function() { + directive('dep', function(log) { + return { + require: '^?main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + $compile('
')($rootScope); + expect(log).toEqual('dep:null'); + }); + }); + - it('should require controller on parent element',function() { - module(function() { - directive('main', function(log) { - return { - controller: function() { - this.name = 'main'; - } - }; - }); - directive('dep', function(log) { - return { - require: '^main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:main'); - }); + it('should have optional controller on current element', function() { + module(function() { + directive('dep', function(log) { + return { + require: '?main', + link: function(scope, element, attrs, controller) { + log('dep:' + !!controller); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:false'); + }); + }); - it('should throw an error if required controller can\'t be found',function() { - module(function() { - directive('dep', function(log) { - return { - require: '^main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - expect(function() { - $compile('
')($rootScope); - }).toThrowMinErr('$compile', 'ctreq', 'Controller \'main\', required by directive \'dep\', can\'t be found!'); - }); + it('should support multiple controllers', function() { + module(function() { + directive('c1', valueFn({ + controller: function() { this.name = 'c1'; } + })); + directive('c2', valueFn({ + controller: function() { this.name = 'c2'; } + })); + directive('dep', function(log) { + return { + require: ['^c1', '^c2'], + link: function(scope, element, attrs, controller) { + log('dep:' + controller[0].name + '-' + controller[1].name); + } + }; }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:c1-c2'); + }); + }); + it('should support multiple controllers as an object hash', function() { + module(function() { + directive('c1', valueFn({ + controller: function() { this.name = 'c1'; } + })); + directive('c2', valueFn({ + controller: function() { this.name = 'c2'; } + })); + directive('dep', function(log) { + return { + require: { myC1: '^c1', myC2: '^c2' }, + link: function(scope, element, attrs, controllers) { + log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:c1-c2'); + }); + }); - it('should pass null if required controller can\'t be found and is optional',function() { - module(function() { - directive('dep', function(log) { - return { - require: '?^main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - $compile('
')($rootScope); - expect(log).toEqual('dep:null'); + it('should support omitting the name of the required controller if it is the same as the key', + function() { + module(function() { + directive('myC1', valueFn({ + controller: function() { this.name = 'c1'; } + })); + directive('myC2', valueFn({ + controller: function() { this.name = 'c2'; } + })); + directive('dep', function(log) { + return { + require: { myC1: '^', myC2: '^' }, + link: function(scope, element, attrs, controllers) { + log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name); + } + }; }); }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:c1-c2'); + }); + } + ); + it('should instantiate the controller just once when template/templateUrl', function() { + var syncCtrlSpy = jasmine.createSpy('sync controller'), + asyncCtrlSpy = jasmine.createSpy('async controller'); - it('should pass null if required controller can\'t be found and is optional with the question mark on the right',function() { - module(function() { - directive('dep', function(log) { - return { - require: '^?main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - $compile('
')($rootScope); - expect(log).toEqual('dep:null'); - }); - }); + module(function() { + directive('myDirectiveSync', valueFn({ + template: '
Hello!
', + controller: syncCtrlSpy + })); + directive('myDirectiveAsync', valueFn({ + templateUrl: 'myDirectiveAsync.html', + controller: asyncCtrlSpy, + compile: function() { + return function() { + }; + } + })); + }); + inject(function($templateCache, $compile, $rootScope) { + expect(syncCtrlSpy).not.toHaveBeenCalled(); + expect(asyncCtrlSpy).not.toHaveBeenCalled(); - it('should have optional controller on current element', function() { - module(function() { - directive('dep', function(log) { - return { - require: '?main', - link: function(scope, element, attrs, controller) { - log('dep:' + !!controller); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:false'); - }); - }); + $templateCache.put('myDirectiveAsync.html', '
Hello!
'); + element = $compile('
' + + '' + + '' + + '
')($rootScope); + expect(syncCtrlSpy).not.toHaveBeenCalled(); + expect(asyncCtrlSpy).not.toHaveBeenCalled(); + $rootScope.$apply(); - it('should support multiple controllers', function() { - module(function() { - directive('c1', valueFn({ - controller: function() { this.name = 'c1'; } - })); - directive('c2', valueFn({ - controller: function() { this.name = 'c2'; } - })); - directive('dep', function(log) { - return { - require: ['^c1', '^c2'], - link: function(scope, element, attrs, controller) { - log('dep:' + controller[0].name + '-' + controller[1].name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:c1-c2'); - }); - }); + //expect(syncCtrlSpy).toHaveBeenCalledOnce(); + expect(asyncCtrlSpy).toHaveBeenCalledOnce(); + }); + }); - it('should support multiple controllers as an object hash', function() { - module(function() { - directive('c1', valueFn({ - controller: function() { this.name = 'c1'; } - })); - directive('c2', valueFn({ - controller: function() { this.name = 'c2'; } - })); - directive('dep', function(log) { - return { - require: { myC1: '^c1', myC2: '^c2' }, - link: function(scope, element, attrs, controllers) { - log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:c1-c2'); - }); - }); - it('should support omitting the name of the required controller if it is the same as the key', - function() { - module(function() { - directive('myC1', valueFn({ - controller: function() { this.name = 'c1'; } - })); - directive('myC2', valueFn({ - controller: function() { this.name = 'c2'; } - })); - directive('dep', function(log) { - return { - require: { myC1: '^', myC2: '^' }, - link: function(scope, element, attrs, controllers) { - log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:c1-c2'); - }); - } - ); - it('should instantiate the controller just once when template/templateUrl', function() { - var syncCtrlSpy = jasmine.createSpy('sync controller'), - asyncCtrlSpy = jasmine.createSpy('async controller'); + it('should instantiate controllers in the parent->child order when transclusion, templateUrl and replacement ' + + 'are in the mix', function() { + // When a child controller is in the transclusion that replaces the parent element that has a directive with + // a controller, we should ensure that we first instantiate the parent and only then stuff that comes from the + // transclusion. + // + // The transclusion moves the child controller onto the same element as parent controller so both controllers are + // on the same level. - module(function() { - directive('myDirectiveSync', valueFn({ - template: '
Hello!
', - controller: syncCtrlSpy - })); - directive('myDirectiveAsync', valueFn({ - templateUrl: 'myDirectiveAsync.html', - controller: asyncCtrlSpy, - compile: function() { - return function() { - }; - } - })); - }); + module(function() { + directive('parentDirective', function() { + return { + transclude: true, + replace: true, + templateUrl: 'parentDirective.html', + controller: function(log) { log('parentController'); } + }; + }); + directive('childDirective', function() { + return { + require: '^parentDirective', + templateUrl: 'childDirective.html', + controller: function(log) { log('childController'); } + }; + }); + }); - inject(function($templateCache, $compile, $rootScope) { - expect(syncCtrlSpy).not.toHaveBeenCalled(); - expect(asyncCtrlSpy).not.toHaveBeenCalled(); + inject(function($templateCache, log, $compile, $rootScope) { + $templateCache.put('parentDirective.html', '
parentTemplateText;
'); + $templateCache.put('childDirective.html', 'childTemplateText;'); - $templateCache.put('myDirectiveAsync.html', '
Hello!
'); - element = $compile('
' + - '' + - '' + - '
')($rootScope); - expect(syncCtrlSpy).not.toHaveBeenCalled(); - expect(asyncCtrlSpy).not.toHaveBeenCalled(); + element = $compile('
childContentText;
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('parentController; childController'); + expect(element.text()).toBe('childTemplateText;childContentText;'); + }); + }); - $rootScope.$apply(); - //expect(syncCtrlSpy).toHaveBeenCalledOnce(); - expect(asyncCtrlSpy).toHaveBeenCalledOnce(); - }); - }); + it('should instantiate the controller after the isolate scope bindings are initialized (with template)', function() { + module(function() { + var Ctrl = function($scope, log) { + log('myFoo=' + $scope.myFoo); + }; + directive('myDirective', function() { + return { + scope: { + myFoo: '=' + }, + template: '

Hello

', + controller: Ctrl + }; + }); + }); + inject(function($templateCache, $compile, $rootScope, log) { + $rootScope.foo = 'bar'; - it('should instantiate controllers in the parent->child order when transclusion, templateUrl and replacement ' + - 'are in the mix', function() { - // When a child controller is in the transclusion that replaces the parent element that has a directive with - // a controller, we should ensure that we first instantiate the parent and only then stuff that comes from the - // transclusion. - // - // The transclusion moves the child controller onto the same element as parent controller so both controllers are - // on the same level. + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('myFoo=bar'); + }); + }); - module(function() { - directive('parentDirective', function() { - return { - transclude: true, - replace: true, - templateUrl: 'parentDirective.html', - controller: function(log) { log('parentController'); } - }; - }); - directive('childDirective', function() { - return { - require: '^parentDirective', - templateUrl: 'childDirective.html', - controller: function(log) { log('childController'); } - }; - }); - }); - inject(function($templateCache, log, $compile, $rootScope) { - $templateCache.put('parentDirective.html', '
parentTemplateText;
'); - $templateCache.put('childDirective.html', 'childTemplateText;'); + it('should instantiate the controller after the isolate scope bindings are initialized (with templateUrl)', function() { + module(function() { + var Ctrl = function($scope, log) { + log('myFoo=' + $scope.myFoo); + }; - element = $compile('
childContentText;
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('parentController; childController'); - expect(element.text()).toBe('childTemplateText;childContentText;'); - }); + directive('myDirective', function() { + return { + scope: { + myFoo: '=' + }, + templateUrl: 'hello.html', + controller: Ctrl + }; }); + }); + inject(function($templateCache, $compile, $rootScope, log) { + $templateCache.put('hello.html', '

Hello

'); + $rootScope.foo = 'bar'; - it('should instantiate the controller after the isolate scope bindings are initialized (with template)', function() { - module(function() { - var Ctrl = function($scope, log) { - log('myFoo=' + $scope.myFoo); - }; + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('myFoo=bar'); + }); + }); - directive('myDirective', function() { - return { - scope: { - myFoo: '=' - }, - template: '

Hello

', - controller: Ctrl - }; - }); - }); - inject(function($templateCache, $compile, $rootScope, log) { - $rootScope.foo = 'bar'; + it('should instantiate controllers in the parent->child->baby order when nested transclusion, templateUrl and ' + + 'replacement are in the mix', function() { + // similar to the test above, except that we have one more layer of nesting and nested transclusion - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('myFoo=bar'); - }); + module(function() { + directive('parentDirective', function() { + return { + transclude: true, + replace: true, + templateUrl: 'parentDirective.html', + controller: function(log) { log('parentController'); } + }; + }); + directive('childDirective', function() { + return { + require: '^parentDirective', + transclude: true, + replace: true, + templateUrl: 'childDirective.html', + controller: function(log) { log('childController'); } + }; + }); + directive('babyDirective', function() { + return { + require: '^childDirective', + templateUrl: 'babyDirective.html', + controller: function(log) { log('babyController'); } + }; }); + }); + inject(function($templateCache, log, $compile, $rootScope) { + $templateCache.put('parentDirective.html', '
parentTemplateText;
'); + $templateCache.put('childDirective.html', 'childTemplateText;'); + $templateCache.put('babyDirective.html', 'babyTemplateText;'); - it('should instantiate the controller after the isolate scope bindings are initialized (with templateUrl)', function() { - module(function() { - var Ctrl = function($scope, log) { - log('myFoo=' + $scope.myFoo); - }; + element = $compile('
' + + '
' + + 'childContentText;' + + '
babyContent;
' + + '
' + + '
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('parentController; childController; babyController'); + expect(element.text()).toBe('childContentText;babyTemplateText;'); + }); + }); - directive('myDirective', function() { + + it('should allow controller usage in pre-link directive functions with templateUrl', function() { + module(function() { + var Ctrl = function(log) { + log('instance'); + }; + + directive('myDirective', function() { + return { + scope: true, + templateUrl: 'hello.html', + controller: Ctrl, + compile: function() { return { - scope: { - myFoo: '=' - }, - templateUrl: 'hello.html', - controller: Ctrl + pre: function(scope, template, attr, ctrl) {}, + post: function() {} }; - }); - }); + } + }; + }); + }); - inject(function($templateCache, $compile, $rootScope, log) { - $templateCache.put('hello.html', '

Hello

'); - $rootScope.foo = 'bar'; + inject(function($templateCache, $compile, $rootScope, log) { + $templateCache.put('hello.html', '

Hello

'); - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('myFoo=bar'); - }); - }); + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('instance'); + expect(element.text()).toBe('Hello'); + }); + }); - it('should instantiate controllers in the parent->child->baby order when nested transclusion, templateUrl and ' + - 'replacement are in the mix', function() { - // similar to the test above, except that we have one more layer of nesting and nested transclusion - module(function() { - directive('parentDirective', function() { - return { - transclude: true, - replace: true, - templateUrl: 'parentDirective.html', - controller: function(log) { log('parentController'); } - }; - }); - directive('childDirective', function() { - return { - require: '^parentDirective', - transclude: true, - replace: true, - templateUrl: 'childDirective.html', - controller: function(log) { log('childController'); } - }; - }); - directive('babyDirective', function() { + it('should allow controller usage in pre-link directive functions with a template', function() { + module(function() { + var Ctrl = function(log) { + log('instance'); + }; + + directive('myDirective', function() { + return { + scope: true, + template: '

Hello

', + controller: Ctrl, + compile: function() { return { - require: '^childDirective', - templateUrl: 'babyDirective.html', - controller: function(log) { log('babyController'); } + pre: function(scope, template, attr, ctrl) {}, + post: function() {} }; - }); - }); + } + }; + }); + }); - inject(function($templateCache, log, $compile, $rootScope) { - $templateCache.put('parentDirective.html', '
parentTemplateText;
'); - $templateCache.put('childDirective.html', 'childTemplateText;'); - $templateCache.put('babyDirective.html', 'babyTemplateText;'); + inject(function($templateCache, $compile, $rootScope, log) { + element = $compile('
')($rootScope); + $rootScope.$apply(); - element = $compile('
' + - '
' + - 'childContentText;' + - '
babyContent;
' + - '
' + - '
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('parentController; childController; babyController'); - expect(element.text()).toBe('childContentText;babyTemplateText;'); - }); - }); + expect(log).toEqual('instance'); + expect(element.text()).toBe('Hello'); + }); + }); - it('should allow controller usage in pre-link directive functions with templateUrl', function() { - module(function() { - var Ctrl = function(log) { - log('instance'); - }; + it('should throw ctreq with correct directive name, regardless of order', function() { + module(function($compileProvider) { + $compileProvider.directive('aDir', valueFn({ + restrict: 'E', + require: 'ngModel', + link: noop + })); + }); + inject(function($compile, $rootScope) { + expect(function() { + // a-dir will cause a ctreq error to be thrown. Previously, the error would reference + // the last directive in the chain (which in this case would be ngClick), based on + // priority and alphabetical ordering. This test verifies that the ordering does not + // affect which directive is referenced in the minErr message. + element = $compile('')($rootScope); + }).toThrowMinErr('$compile', 'ctreq', + 'Controller \'ngModel\', required by directive \'aDir\', can\'t be found!'); + }); + }); + }); - directive('myDirective', function() { - return { - scope: true, - templateUrl: 'hello.html', - controller: Ctrl, - compile: function() { - return { - pre: function(scope, template, attr, ctrl) {}, - post: function() {} - }; - } - }; - }); - }); - inject(function($templateCache, $compile, $rootScope, log) { - $templateCache.put('hello.html', '

Hello

'); + describe('transclude', function() { - element = $compile('
')($rootScope); - $rootScope.$apply(); + describe('content transclusion', function() { - expect(log).toEqual('instance'); - expect(element.text()).toBe('Hello'); + it('should support transclude directive', function() { + module(function() { + directive('trans', function() { + return { + transclude: 'content', + replace: true, + scope: {}, + link: function(scope) { + scope.x = 'iso'; + }, + template: '
  • W:{{x}}-{{$parent.$id}}-{{$id}};
' + }; }); }); + inject(function(log, $rootScope, $compile) { + element = $compile('
T:{{x}}-{{$parent.$id}}-{{$id}};
')($rootScope); + $rootScope.x = 'root'; + $rootScope.$apply(); + expect(element.text()).toEqual('W:iso-1-2;T:root-2-3;'); + expect(jqLite(jqLite(element.find('li')[1]).contents()[0]).text()).toEqual('T:root-2-3'); + expect(jqLite(element.find('span')[0]).text()).toEqual(';'); + }); + }); - it('should allow controller usage in pre-link directive functions with a template', function() { - module(function() { - var Ctrl = function(log) { - log('instance'); - }; - - directive('myDirective', function() { - return { - scope: true, - template: '

Hello

', - controller: Ctrl, - compile: function() { - return { - pre: function(scope, template, attr, ctrl) {}, - post: function() {} - }; - } - }; - }); - }); + it('should transclude transcluded content', function() { + module(function() { + directive('book', valueFn({ + transclude: 'content', + template: '
book-
(
)
' + })); + directive('chapter', valueFn({ + transclude: 'content', + templateUrl: 'chapter.html' + })); + directive('section', valueFn({ + transclude: 'content', + template: '
section-!
!
' + })); + return function($httpBackend) { + $httpBackend. + expect('GET', 'chapter.html'). + respond('
chapter-
[
]
'); + }; + }); + inject(function(log, $rootScope, $compile, $httpBackend) { + element = $compile('
paragraph
')($rootScope); + $rootScope.$apply(); - inject(function($templateCache, $compile, $rootScope, log) { - element = $compile('
')($rootScope); - $rootScope.$apply(); + expect(element.text()).toEqual('book-'); - expect(log).toEqual('instance'); - expect(element.text()).toBe('Hello'); - }); + $httpBackend.flush(); + $rootScope.$apply(); + expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); }); + }); - it('should throw ctreq with correct directive name, regardless of order', function() { - module(function($compileProvider) { - $compileProvider.directive('aDir', valueFn({ - restrict: 'E', - require: 'ngModel', - link: noop - })); - }); - inject(function($compile, $rootScope) { - expect(function() { - // a-dir will cause a ctreq error to be thrown. Previously, the error would reference - // the last directive in the chain (which in this case would be ngClick), based on - // priority and alphabetical ordering. This test verifies that the ordering does not - // affect which directive is referenced in the minErr message. - element = $compile('')($rootScope); - }).toThrowMinErr('$compile', 'ctreq', - 'Controller \'ngModel\', required by directive \'aDir\', can\'t be found!'); - }); + it('should not merge text elements from transcluded content', function() { + module(function() { + directive('foo', valueFn({ + transclude: 'content', + template: '
This is before {{before}}.
', + link: function(scope, element, attr, ctrls, $transclude) { + var futureParent = element.children().eq(0); + $transclude(function(clone) { + futureParent.append(clone); + }, futureParent); + }, + scope: true + })); + }); + inject(function($rootScope, $compile) { + element = $compile('
This is after {{after}}
')($rootScope); + $rootScope.before = 'BEFORE'; + $rootScope.after = 'AFTER'; + $rootScope.$apply(); + expect(element.text()).toEqual('This is before BEFORE. This is after AFTER'); + + $rootScope.before = 'Not-Before'; + $rootScope.after = 'AfTeR'; + $rootScope.$$childHead.before = 'BeFoRe'; + $rootScope.$$childHead.after = 'Not-After'; + $rootScope.$apply(); + expect(element.text()).toEqual('This is before BeFoRe. This is after AfTeR'); }); }); - describe('transclude', function() { + it('should only allow one content transclusion per element', function() { + module(function() { + directive('first', valueFn({ + transclude: true + })); + directive('second', valueFn({ + transclude: true + })); + }); + inject(function($compile) { + expect(function() { + $compile('
'); + }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
{{x}}
', + link: function(scope, element, attr, ctrl) { + scope.x = 'iso'; + } + })); + directive('trans', valueFn({ + transclude: 'content', + link: function(scope, element, attr, ctrl, $transclude) { + $transclude(function(clone) { + element.append(clone); + }); + } + })); + }); + inject(function($rootScope, $compile) { + element = $compile('')($rootScope); + $rootScope.x = 'root'; + $rootScope.$apply(); + expect(element.text()).toEqual('iso'); + }); + }); - describe('content transclusion', function() { - it('should support transclude directive', function() { - module(function() { - directive('trans', function() { - return { - transclude: 'content', - replace: true, - scope: {}, - link: function(scope) { - scope.x = 'iso'; - }, - template: '
  • W:{{x}}-{{$parent.$id}}-{{$id}};
' - }; + //see issue https://github.com/angular/angular.js/issues/12936 + it('should use the proper scope when it is on the root element of a replaced directive template with child scope', function() { + module(function() { + directive('child', valueFn({ + scope: true, + replace: true, + template: '
{{x}}
', + link: function(scope, element, attr, ctrl) { + scope.x = 'child'; + } + })); + directive('trans', valueFn({ + transclude: 'content', + link: function(scope, element, attr, ctrl, $transclude) { + $transclude(function(clone) { + element.append(clone); }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
T:{{x}}-{{$parent.$id}}-{{$id}};
')($rootScope); - $rootScope.x = 'root'; - $rootScope.$apply(); - expect(element.text()).toEqual('W:iso-1-2;T:root-2-3;'); - expect(jqLite(jqLite(element.find('li')[1]).contents()[0]).text()).toEqual('T:root-2-3'); - expect(jqLite(element.find('span')[0]).text()).toEqual(';'); - }); - }); + } + })); + }); + inject(function($rootScope, $compile) { + element = $compile('')($rootScope); + $rootScope.x = 'root'; + $rootScope.$apply(); + expect(element.text()).toEqual('child'); + }); + }); + it('should throw if a transcluded node is transcluded again', function() { + module(function() { + directive('trans', valueFn({ + transclude: true, + link: function(scope, element, attr, ctrl, $transclude) { + $transclude(); + $transclude(); + } + })); + }); + inject(function($rootScope, $compile) { + expect(function() { + $compile('')($rootScope); + }).toThrowMinErr('$compile', 'multilink', 'This element has already been linked.'); + }); + }); - it('should transclude transcluded content', function() { - module(function() { - directive('book', valueFn({ - transclude: 'content', - template: '
book-
(
)
' - })); - directive('chapter', valueFn({ - transclude: 'content', - templateUrl: 'chapter.html' - })); - directive('section', valueFn({ - transclude: 'content', - template: '
section-!
!
' - })); - return function($httpBackend) { - $httpBackend. - expect('GET', 'chapter.html'). - respond('
chapter-
[
]
'); - }; - }); - inject(function(log, $rootScope, $compile, $httpBackend) { - element = $compile('
paragraph
')($rootScope); - $rootScope.$apply(); + 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; + } - expect(element.text()).toEqual('book-'); - $httpBackend.flush(); - $rootScope.$apply(); - expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); - }); - }); + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }); + inject(function($compile, $rootScope) { + var cacheSize = jqLiteCacheSize(); - it('should not merge text elements from transcluded content', function() { - module(function() { - directive('foo', valueFn({ - transclude: 'content', - template: '
This is before {{before}}.
', - link: function(scope, element, attr, ctrls, $transclude) { - var futureParent = element.children().eq(0); - $transclude(function(clone) { - futureParent.append(clone); - }, futureParent); - }, - scope: true - })); - }); - inject(function($rootScope, $compile) { - element = $compile('
This is after {{after}}
')($rootScope); - $rootScope.before = 'BEFORE'; - $rootScope.after = 'AFTER'; - $rootScope.$apply(); - expect(element.text()).toEqual('This is before BEFORE. This is after AFTER'); - - $rootScope.before = 'Not-Before'; - $rootScope.after = 'AfTeR'; - $rootScope.$$childHead.before = 'BeFoRe'; - $rootScope.$$childHead.after = 'Not-After'; - $rootScope.$apply(); - expect(element.text()).toEqual('This is before BeFoRe. This is after AfTeR'); - }); - }); + element = $compile('
{{x}}
')($rootScope); + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + $rootScope.$apply('xs = [0,1]'); + expect(jqLiteCacheSize()).toEqual(cacheSize + 2); - it('should only allow one content transclusion per element', function() { - module(function() { - directive('first', valueFn({ - transclude: true - })); - directive('second', valueFn({ - transclude: true - })); - }); - inject(function($compile) { - expect(function() { - $compile('
'); - }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
{{x}}
', - link: function(scope, element, attr, ctrl) { - scope.x = 'iso'; - } - })); - directive('trans', valueFn({ - transclude: 'content', - link: function(scope, element, attr, ctrl, $transclude) { - $transclude(function(clone) { - element.append(clone); - }); - } - })); - }); - inject(function($rootScope, $compile) { - element = $compile('')($rootScope); - $rootScope.x = 'root'; - $rootScope.$apply(); - expect(element.text()).toEqual('iso'); - }); - }); + $rootScope.$apply('xs = []'); + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + element.remove(); + expect(jqLiteCacheSize()).toEqual(cacheSize + 0); + }); + }); - //see issue https://github.com/angular/angular.js/issues/12936 - it('should use the proper scope when it is on the root element of a replaced directive template with child scope', function() { - module(function() { - directive('child', valueFn({ - scope: true, - replace: true, - template: '
{{x}}
', - link: function(scope, element, attr, ctrl) { - scope.x = 'child'; - } - })); - directive('trans', valueFn({ - transclude: 'content', - link: function(scope, element, attr, ctrl, $transclude) { - $transclude(function(clone) { - element.append(clone); - }); - } - })); - }); - inject(function($rootScope, $compile) { - element = $compile('')($rootScope); - $rootScope.x = 'root'; - $rootScope.$apply(); - expect(element.text()).toEqual('child'); - }); - }); - it('should throw if a transcluded node is transcluded again', function() { - module(function() { - directive('trans', valueFn({ - transclude: true, - link: function(scope, element, attr, ctrl, $transclude) { - $transclude(); - $transclude(); - } - })); - }); - inject(function($rootScope, $compile) { - expect(function() { - $compile('')($rootScope); - }).toThrowMinErr('$compile', 'multilink', 'This element has already been linked.'); - }); - }); + 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; + } - 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(false); + }); - module(function($compileProvider) { - $compileProvider.debugInfoEnabled(true); - }); + inject(function($compile, $rootScope) { + var cacheSize = jqLiteCacheSize(); - inject(function($compile, $rootScope) { - var cacheSize = jqLiteCacheSize(); + element = $compile('
{{x}}
')($rootScope); + expect(jqLiteCacheSize()).toEqual(cacheSize); - element = $compile('
{{x}}
')($rootScope); - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + $rootScope.$apply('xs = [0,1]'); + expect(jqLiteCacheSize()).toEqual(cacheSize); - $rootScope.$apply('xs = [0,1]'); - expect(jqLiteCacheSize()).toEqual(cacheSize + 2); + $rootScope.$apply('xs = [0]'); + expect(jqLiteCacheSize()).toEqual(cacheSize); - $rootScope.$apply('xs = [0]'); - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + $rootScope.$apply('xs = []'); + expect(jqLiteCacheSize()).toEqual(cacheSize); - $rootScope.$apply('xs = []'); - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + element.remove(); + expect(jqLiteCacheSize()).toEqual(cacheSize); + }); + }); - element.remove(); - expect(jqLiteCacheSize()).toEqual(cacheSize + 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; + } - 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(true); + }); + inject(function($compile, $rootScope) { + var cacheSize = jqLiteCacheSize(); + element = $compile('
{{x}}
')($rootScope); - module(function($compileProvider) { - $compileProvider.debugInfoEnabled(false); - }); + $rootScope.$apply('xs = [0,1]'); + // At this point we have a bunch of comment placeholders but no real transcluded elements + // So the cache only contains the root element's data + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - inject(function($compile, $rootScope) { - var cacheSize = jqLiteCacheSize(); + $rootScope.$apply('val = true'); + // Now we have two concrete transcluded elements plus some comments so two more cache items + expect(jqLiteCacheSize()).toEqual(cacheSize + 3); - element = $compile('
{{x}}
')($rootScope); - expect(jqLiteCacheSize()).toEqual(cacheSize); + $rootScope.$apply('val = false'); + // Once again we only have comments so no transcluded elements and the cache is back to just + // the root element + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - $rootScope.$apply('xs = [0,1]'); - expect(jqLiteCacheSize()).toEqual(cacheSize); + element.remove(); + // Now we've even removed the root element along with its cache + expect(jqLiteCacheSize()).toEqual(cacheSize + 0); + }); + }); - $rootScope.$apply('xs = [0]'); - expect(jqLiteCacheSize()).toEqual(cacheSize); + it('should not leak when continuing the compilation of elements on a scope that was destroyed', function() { + if (jQuery) { + // jQuery 2.x doesn't expose the cache storage. + return; + } - $rootScope.$apply('xs = []'); - expect(jqLiteCacheSize()).toEqual(cacheSize); + var linkFn = jasmine.createSpy('linkFn'); - element.remove(); - expect(jqLiteCacheSize()).toEqual(cacheSize); + module(function($controllerProvider, $compileProvider) { + $controllerProvider.register('Leak', function($scope, $timeout) { + $scope.code = 'red'; + $timeout(function() { + $scope.code = 'blue'; }); }); + $compileProvider.directive('isolateRed', function() { + return { + restrict: 'A', + scope: {}, + template: '
' + }; + }); + $compileProvider.directive('red', function() { + return { + restrict: 'A', + templateUrl: 'red.html', + scope: {}, + link: linkFn + }; + }); + }); + inject(function($compile, $rootScope, $httpBackend, $timeout, $templateCache) { + var cacheSize = jqLiteCacheSize(); + $httpBackend.whenGET('red.html').respond('

red.html

'); + var template = $compile( + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'); + element = template($rootScope, noop); + $rootScope.$digest(); + $timeout.flush(); + $httpBackend.flush(); + expect(linkFn).not.toHaveBeenCalled(); + expect(jqLiteCacheSize()).toEqual(cacheSize + 2); - 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; - } + $templateCache.removeAll(); + var destroyedScope = $rootScope.$new(); + destroyedScope.$destroy(); + var clone = template(destroyedScope, noop); + $rootScope.$digest(); + $timeout.flush(); + expect(linkFn).not.toHaveBeenCalled(); + clone.remove(); + }); + }); - module(function($compileProvider) { - $compileProvider.debugInfoEnabled(true); - }); + if (jQuery) { + describe('cleaning up after a replaced element', function() { + var $compile, xs; + beforeEach(inject(function(_$compile_) { + $compile = _$compile_; + xs = [0, 1]; + })); - inject(function($compile, $rootScope) { - var cacheSize = jqLiteCacheSize(); - element = $compile('
{{x}}
')($rootScope); - - $rootScope.$apply('xs = [0,1]'); - // At this point we have a bunch of comment placeholders but no real transcluded elements - // So the cache only contains the root element's data - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - - $rootScope.$apply('val = true'); - // Now we have two concrete transcluded elements plus some comments so two more cache items - expect(jqLiteCacheSize()).toEqual(cacheSize + 3); - - $rootScope.$apply('val = false'); - // Once again we only have comments so no transcluded elements and the cache is back to just - // the root element - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - - element.remove(); - // Now we've even removed the root element along with its cache - expect(jqLiteCacheSize()).toEqual(cacheSize + 0); - }); - }); + function testCleanup() { + var privateData, firstRepeatedElem; - it('should not leak when continuing the compilation of elements on a scope that was destroyed', function() { - if (jQuery) { - // jQuery 2.x doesn't expose the cache storage. - return; - } + element = $compile('
{{x}}
')($rootScope); - var linkFn = jasmine.createSpy('linkFn'); + $rootScope.$apply('xs = [' + xs + ']'); + firstRepeatedElem = element.children('.ng-scope').eq(0); - module(function($controllerProvider, $compileProvider) { - $controllerProvider.register('Leak', function($scope, $timeout) { - $scope.code = 'red'; - $timeout(function() { - $scope.code = 'blue'; - }); - }); - $compileProvider.directive('isolateRed', function() { - return { - restrict: 'A', - scope: {}, - template: '
' - }; - }); - $compileProvider.directive('red', function() { - return { - restrict: 'A', - templateUrl: 'red.html', - scope: {}, - link: linkFn - }; - }); - }); + expect(firstRepeatedElem.data('$scope')).toBeDefined(); + privateData = jQuery._data(firstRepeatedElem[0]); + expect(privateData.events).toBeDefined(); + expect(privateData.events.click).toBeDefined(); + expect(privateData.events.click[0]).toBeDefined(); - inject(function($compile, $rootScope, $httpBackend, $timeout, $templateCache) { - var cacheSize = jqLiteCacheSize(); - $httpBackend.whenGET('red.html').respond('

red.html

'); - var template = $compile( - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
'); - element = template($rootScope, noop); - $rootScope.$digest(); - $timeout.flush(); - $httpBackend.flush(); - expect(linkFn).not.toHaveBeenCalled(); - expect(jqLiteCacheSize()).toEqual(cacheSize + 2); + //Ensure the AngularJS $destroy event is still sent + var destroyCount = 0; + element.find('div').on('$destroy', function() { destroyCount++; }); - $templateCache.removeAll(); - var destroyedScope = $rootScope.$new(); - destroyedScope.$destroy(); - var clone = template(destroyedScope, noop); - $rootScope.$digest(); - $timeout.flush(); - expect(linkFn).not.toHaveBeenCalled(); - clone.remove(); - }); - }); + $rootScope.$apply('xs = null'); - if (jQuery) { - describe('cleaning up after a replaced element', function() { - var $compile, xs; - beforeEach(inject(function(_$compile_) { - $compile = _$compile_; - xs = [0, 1]; - })); + expect(destroyCount).toBe(2); + expect(firstRepeatedElem.data('$scope')).not.toBeDefined(); + privateData = jQuery._data(firstRepeatedElem[0]); + expect(privateData && privateData.events).not.toBeDefined(); + } - function testCleanup() { - var privateData, firstRepeatedElem; + it('should work without external libraries (except jQuery)', testCleanup); + + it('should work with another library patching jQuery.cleanData after AngularJS', function() { + var cleanedCount = 0; + var currentCleanData = jQuery.cleanData; + jQuery.cleanData = function(elems) { + cleanedCount += elems.length; + // Don't return the output and explicitly pass only the first parameter + // so that we're sure we're not relying on either of them. jQuery UI patch + // behaves in this way. + currentCleanData(elems); + }; - element = $compile('
{{x}}
')($rootScope); + testCleanup(); - $rootScope.$apply('xs = [' + xs + ']'); - firstRepeatedElem = element.children('.ng-scope').eq(0); + // The ng-repeat template is removed/cleaned (the +1) + // and each clone of the ng-repeat template is also removed (xs.length) + expect(cleanedCount).toBe(xs.length + 1); - expect(firstRepeatedElem.data('$scope')).toBeDefined(); - privateData = jQuery._data(firstRepeatedElem[0]); - expect(privateData.events).toBeDefined(); - expect(privateData.events.click).toBeDefined(); - expect(privateData.events.click[0]).toBeDefined(); + // Restore the previous jQuery.cleanData. + jQuery.cleanData = currentCleanData; + }); + }); + } - //Ensure the AngularJS $destroy event is still sent - var destroyCount = 0; - element.find('div').on('$destroy', function() { destroyCount++; }); - $rootScope.$apply('xs = null'); + it('should add a $$transcluded property onto the transcluded scope', function() { + module(function() { + directive('trans', function() { + return { + transclude: true, + replace: true, + scope: true, + template: '
I:{{$$transcluded}}
' + }; + }); + }); + inject(function($rootScope, $compile) { + element = $compile('
T:{{$$transcluded}}
')($rootScope); + $rootScope.$apply(); + expect(jqLite(element.find('span')[0]).text()).toEqual('I:'); + expect(jqLite(element.find('span')[1]).text()).toEqual('T:true'); + }); + }); - expect(destroyCount).toBe(2); - expect(firstRepeatedElem.data('$scope')).not.toBeDefined(); - privateData = jQuery._data(firstRepeatedElem[0]); - expect(privateData && privateData.events).not.toBeDefined(); - } - it('should work without external libraries (except jQuery)', testCleanup); - - it('should work with another library patching jQuery.cleanData after AngularJS', function() { - var cleanedCount = 0; - var currentCleanData = jQuery.cleanData; - jQuery.cleanData = function(elems) { - cleanedCount += elems.length; - // Don't return the output and explicitly pass only the first parameter - // so that we're sure we're not relying on either of them. jQuery UI patch - // behaves in this way. - currentCleanData(elems); - }; + it('should clear contents of the ng-transclude element before appending transcluded content' + + ' if transcluded content exists', function() { + module(function() { + directive('trans', function() { + return { + transclude: true, + template: '
old stuff!
' + }; + }); + }); + inject(function($rootScope, $compile) { + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
unicorn!
'); + }); + }); - testCleanup(); + it('should NOT clear contents of the ng-transclude element before appending transcluded content' + + ' if transcluded content does NOT exist', function() { + module(function() { + directive('trans', function() { + return { + transclude: true, + template: '
old stuff!
' + }; + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
old stuff!
'); + }); + }); - // The ng-repeat template is removed/cleaned (the +1) - // and each clone of the ng-repeat template is also removed (xs.length) - expect(cleanedCount).toBe(xs.length + 1); - // Restore the previous jQuery.cleanData. - jQuery.cleanData = currentCleanData; - }); - }); - } + it('should clear the fallback content from the element during compile and before linking', function() { + module(function() { + directive('trans', function() { + return { + transclude: true, + template: '
fallback content
' + }; + }); + }); + inject(function(log, $rootScope, $compile) { + element = jqLite('
'); + var linkfn = $compile(element); + expect(element.html()).toEqual('
'); + linkfn($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
fallback content
'); + }); + }); - it('should add a $$transcluded property onto the transcluded scope', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - replace: true, - scope: true, - template: '
I:{{$$transcluded}}
' - }; - }); - }); - inject(function($rootScope, $compile) { - element = $compile('
T:{{$$transcluded}}
')($rootScope); - $rootScope.$apply(); - expect(jqLite(element.find('span')[0]).text()).toEqual('I:'); - expect(jqLite(element.find('span')[1]).text()).toEqual('T:true'); - }); + it('should allow cloning of the fallback via ngRepeat', function() { + module(function() { + directive('trans', function() { + return { + transclude: true, + template: '
{{i}}
' + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(element.text()).toEqual('012'); + }); + }); - it('should clear contents of the ng-transclude element before appending transcluded content' + - ' if transcluded content exists', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
old stuff!
' - }; - }); - }); - inject(function($rootScope, $compile) { - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
unicorn!
'); - }); - }); + it('should not link the fallback content if transcluded content is provided', function() { + var linkSpy = jasmine.createSpy('postlink'); - it('should NOT clear contents of the ng-transclude element before appending transcluded content' + - ' if transcluded content does NOT exist', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
old stuff!
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
old stuff!
'); - }); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; }); - - it('should clear the fallback content from the element during compile and before linking', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
fallback content
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = jqLite('
'); - var linkfn = $compile(element); - expect(element.html()).toEqual('
'); - linkfn($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
fallback content
'); - }); + directive('trans', function() { + return { + transclude: true, + template: '
' + }; }); + }); + inject(function($rootScope, $compile) { + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
unicorn!
'); + expect(linkSpy).not.toHaveBeenCalled(); + }); + }); + it('should compile and link the fallback content if no transcluded content is provided', function() { + var linkSpy = jasmine.createSpy('postlink'); - it('should allow cloning of the fallback via ngRepeat', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
{{i}}
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toEqual('012'); - }); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; }); + directive('trans', function() { + return { + transclude: true, + template: '
' + }; + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
old stuff!
'); + expect(linkSpy).toHaveBeenCalled(); + }); + }); - it('should not link the fallback content if transcluded content is provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + it('should compile and link the fallback content if only whitespace transcluded content is provided', function() { + var linkSpy = jasmine.createSpy('postlink'); - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; - }); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); - directive('trans', function() { - return { - transclude: true, - template: '
' - }; - }); - }); - inject(function($rootScope, $compile) { - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
unicorn!
'); - expect(linkSpy).not.toHaveBeenCalled(); - }); + directive('trans', function() { + return { + transclude: true, + template: '
' + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
\n \n
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
old stuff!
'); + expect(linkSpy).toHaveBeenCalled(); + }); + }); - it('should compile and link the fallback content if no transcluded content is provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + it('should not link the fallback content if only whitespace and comments are provided as transclude content', function() { + var linkSpy = jasmine.createSpy('postlink'); - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; - }); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); - directive('trans', function() { - return { - transclude: true, - template: '
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
old stuff!
'); - expect(linkSpy).toHaveBeenCalled(); - }); + directive('trans', function() { + return { + transclude: true, + template: '
' + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
\n \n
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
\n \n
'); + expect(linkSpy).not.toHaveBeenCalled(); + }); + }); - it('should compile and link the fallback content if only whitespace transcluded content is provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + it('should compile and link the fallback content if an optional transclusion slot is not provided', function() { + var linkSpy = jasmine.createSpy('postlink'); - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; - }); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); - directive('trans', function() { - return { - transclude: true, - template: '
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
\n \n
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
old stuff!
'); - expect(linkSpy).toHaveBeenCalled(); - }); + directive('trans', function() { + return { + transclude: { optionalSlot: '?optional'}, + template: '
' + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
old stuff!
'); + expect(linkSpy).toHaveBeenCalled(); + }); + }); - it('should not link the fallback content if only whitespace and comments are provided as transclude content', function() { - var linkSpy = jasmine.createSpy('postlink'); + it('should cope if there is neither transcluded content nor fallback content', function() { + module(function() { + directive('trans', function() { + return { + transclude: true, + template: '
' + }; + }); + }); + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
'); + }); + }); - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; - }); + it('should throw on an ng-transclude element inside no transclusion directive', function() { + inject(function($rootScope, $compile) { + var error; - directive('trans', function() { - return { - transclude: true, - template: '
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
\n \n
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
\n \n
'); - expect(linkSpy).not.toHaveBeenCalled(); - }); - }); + try { + $compile('
')($rootScope); + } catch (e) { + error = e; + } - it('should compile and link the fallback content if an optional transclusion slot is not provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + expect(error).toEqualMinErr('ngTransclude', 'orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element:
' - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
old stuff!
'); - expect(linkSpy).toHaveBeenCalled(); - }); - }); + it('should not pass transclusion into a template directive when the directive didn\'t request transclusion', function() { - it('should cope if there is neither transcluded content nor fallback content', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
' - }; - }); - }); - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
'); - }); - }); + module(function($compileProvider) { - it('should throw on an ng-transclude element inside no transclusion directive', function() { - inject(function($rootScope, $compile) { - var error; + $compileProvider.directive('transFoo', valueFn({ + template: '
' + + '
' + + '
this one should get replaced with content
' + + '
' + + '
', + transclude: true - try { - $compile('
')($rootScope); - } catch (e) { - error = e; - } + })); - expect(error).toEqualMinErr('ngTransclude', 'orphan', - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found. ' + - 'Element:
' + + // This ng-transclude is invalid. It should throw an error. + '
' + + '
', + transclude: false + })); + }); - it('should not pass transclusion into a template directive when the directive didn\'t request transclusion', function() { + inject(function($compile, $rootScope) { + expect(function() { + $compile('
content
')($rootScope); + }).toThrowMinErr('ngTransclude', 'orphan', + 'Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:
'); + }); + }); - module(function($compileProvider) { - $compileProvider.directive('transFoo', valueFn({ - template: '
' + - '
' + - '
this one should get replaced with content
' + - '
' + - '
', - transclude: true + it('should not pass transclusion into a templateUrl directive', function() { - })); + module(function($compileProvider) { - $compileProvider.directive('noTransBar', valueFn({ - template: '
' + - // This ng-transclude is invalid. It should throw an error. - '
' + - '
', - transclude: false + $compileProvider.directive('transFoo', valueFn({ + template: '
' + + '
' + + '
this one should get replaced with content
' + + '
' + + '
', + transclude: true + })); - })); - }); + $compileProvider.directive('noTransBar', valueFn({ + templateUrl: 'noTransBar.html', + transclude: false + })); + }); - inject(function($compile, $rootScope) { - expect(function() { - $compile('
content
')($rootScope); - }).toThrowMinErr('ngTransclude', 'orphan', - 'Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:
'); - }); - }); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('noTransBar.html', + '
' + + // This ng-transclude is invalid. It should throw an error. + '
' + + '
'); + expect(function() { + element = $compile('
content
')($rootScope); + $rootScope.$digest(); + }).toThrowMinErr('ngTransclude', 'orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element:
'); + }); + }); - it('should not pass transclusion into a templateUrl directive', function() { - module(function($compileProvider) { + it('should expose transcludeFn in compile fn even for templateUrl', function() { + module(function() { + directive('transInCompile', valueFn({ + transclude: true, + // template: '
whatever
', + templateUrl: 'foo.html', + compile: function(_, __, transclude) { + return function(scope, element) { + transclude(scope, function(clone, scope) { + element.html(''); + element.append(clone); + }); + }; + } + })); + }); - $compileProvider.directive('transFoo', valueFn({ - template: '
' + - '
' + - '
this one should get replaced with content
' + - '
' + - '
', - transclude: true - })); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('foo.html', '
whatever
'); - $compileProvider.directive('noTransBar', valueFn({ - templateUrl: 'noTransBar.html', - transclude: false - })); - }); + compile('
transcluded content
'); + $rootScope.$apply(); - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('noTransBar.html', - '
' + - // This ng-transclude is invalid. It should throw an error. - '
' + - '
'); + expect(trim(element.text())).toBe('transcluded content'); + }); + }); - expect(function() { - element = $compile('
content
')($rootScope); - $rootScope.$digest(); - }).toThrowMinErr('ngTransclude', 'orphan', - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found. ' + - 'Element:
'); - }); + + it('should make the result of a transclusion available to the parent directive in post-linking phase' + + '(template)', function() { + module(function() { + directive('trans', function(log) { + return { + transclude: true, + template: '
', + link: { + pre: function($scope, $element) { + log('pre(' + $element.text() + ')'); + }, + post: function($scope, $element) { + log('post(' + $element.text() + ')'); + } + } + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('pre(); post(unicorn!)'); + }); + }); - it('should expose transcludeFn in compile fn even for templateUrl', function() { - module(function() { - directive('transInCompile', valueFn({ - transclude: true, - // template: '
whatever
', - templateUrl: 'foo.html', - compile: function(_, __, transclude) { - return function(scope, element) { - transclude(scope, function(clone, scope) { - element.html(''); - element.append(clone); - }); - }; + it('should make the result of a transclusion available to the parent directive in post-linking phase' + + '(templateUrl)', function() { + // when compiling an async directive the transclusion is always processed before the directive + // this is different compared to sync directive. delaying the transclusion makes little sense. + + module(function() { + directive('trans', function(log) { + return { + transclude: true, + templateUrl: 'trans.html', + link: { + pre: function($scope, $element) { + log('pre(' + $element.text() + ')'); + }, + post: function($scope, $element) { + log('post(' + $element.text() + ')'); } - })); - }); + } + }; + }); + }); + inject(function(log, $rootScope, $compile, $templateCache) { + $templateCache.put('trans.html', '
'); - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('foo.html', '
whatever
'); + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('pre(); post(unicorn!)'); + }); + }); - compile('
transcluded content
'); - $rootScope.$apply(); - expect(trim(element.text())).toBe('transcluded content'); - }); + it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' + + '(template)', function() { + module(function() { + directive('replacedTrans', function(log) { + return { + transclude: true, + replace: true, + template: '
', + link: { + pre: function($scope, $element) { + log('pre(' + $element.text() + ')'); + }, + post: function($scope, $element) { + log('post(' + $element.text() + ')'); + } + } + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('pre(); post(unicorn!)'); + }); + }); - it('should make the result of a transclusion available to the parent directive in post-linking phase' + - '(template)', function() { - module(function() { - directive('trans', function(log) { - return { - transclude: true, - template: '
', - link: { - pre: function($scope, $element) { - log('pre(' + $element.text() + ')'); - }, - post: function($scope, $element) { - log('post(' + $element.text() + ')'); - } - } - }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('pre(); post(unicorn!)'); - }); + it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' + + ' (templateUrl)', function() { + module(function() { + directive('replacedTrans', function(log) { + return { + transclude: true, + replace: true, + templateUrl: 'trans.html', + link: { + pre: function($scope, $element) { + log('pre(' + $element.text() + ')'); + }, + post: function($scope, $element) { + log('post(' + $element.text() + ')'); + } + } + }; }); + }); + inject(function(log, $rootScope, $compile, $templateCache) { + $templateCache.put('trans.html', '
'); + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('pre(); post(unicorn!)'); + }); + }); - it('should make the result of a transclusion available to the parent directive in post-linking phase' + - '(templateUrl)', function() { - // when compiling an async directive the transclusion is always processed before the directive - // this is different compared to sync directive. delaying the transclusion makes little sense. + it('should copy the directive controller to all clones', function() { + var transcludeCtrl, cloneCount = 2; + module(function() { + directive('transclude', valueFn({ + transclude: 'content', + controller: function($transclude) { + transcludeCtrl = this; + }, + link: function(scope, el, attr, ctrl, $transclude) { + var i; + for (i = 0; i < cloneCount; i++) { + $transclude(cloneAttach); + } - module(function() { - directive('trans', function(log) { - return { - transclude: true, - templateUrl: 'trans.html', - link: { - pre: function($scope, $element) { - log('pre(' + $element.text() + ')'); - }, - post: function($scope, $element) { - log('post(' + $element.text() + ')'); - } - } - }; - }); - }); - inject(function(log, $rootScope, $compile, $templateCache) { - $templateCache.put('trans.html', '
'); + function cloneAttach(clone) { + el.append(clone); + } + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + var children = element.children(), i; + expect(transcludeCtrl).toBeDefined(); - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('pre(); post(unicorn!)'); - }); - }); + expect(element.data('$transcludeController')).toBe(transcludeCtrl); + for (i = 0; i < cloneCount; i++) { + expect(children.eq(i).data('$transcludeController')).toBeUndefined(); + } + }); + }); + it('should provide the $transclude controller local as 5th argument to the pre and post-link function', function() { + var ctrlTransclude, preLinkTransclude, postLinkTransclude; + module(function() { + directive('transclude', valueFn({ + transclude: 'content', + controller: function($transclude) { + ctrlTransclude = $transclude; + }, + compile: function() { + return { + pre: function(scope, el, attr, ctrl, $transclude) { + preLinkTransclude = $transclude; + }, + post: function(scope, el, attr, ctrl, $transclude) { + postLinkTransclude = $transclude; + } + }; + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + expect(ctrlTransclude).toBeDefined(); + expect(ctrlTransclude).toBe(preLinkTransclude); + expect(ctrlTransclude).toBe(postLinkTransclude); + }); + }); - it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' + - '(template)', function() { - module(function() { - directive('replacedTrans', function(log) { - return { - transclude: true, - replace: true, - template: '
', - link: { - pre: function($scope, $element) { - log('pre(' + $element.text() + ')'); - }, - post: function($scope, $element) { - log('post(' + $element.text() + ')'); - } - } - }; + it('should allow an optional scope argument in $transclude', function() { + var capturedChildCtrl; + module(function() { + directive('transclude', valueFn({ + transclude: 'content', + link: function(scope, element, attr, ctrl, $transclude) { + $transclude(scope, function(clone) { + element.append(clone); }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('pre(); post(unicorn!)'); - }); - }); + } + })); + }); + inject(function($compile) { + element = $compile('
{{$id}}
')($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('' + $rootScope.$id); + }); + }); - it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' + - ' (templateUrl)', function() { - module(function() { - directive('replacedTrans', function(log) { - return { - transclude: true, - replace: true, - templateUrl: 'trans.html', - link: { - pre: function($scope, $element) { - log('pre(' + $element.text() + ')'); - }, - post: function($scope, $element) { - log('post(' + $element.text() + ')'); - } - } - }; + it('should expose the directive controller to transcluded children', function() { + var capturedChildCtrl; + module(function() { + directive('transclude', valueFn({ + transclude: 'content', + controller: function() { + }, + link: function(scope, element, attr, ctrl, $transclude) { + $transclude(function(clone) { + element.append(clone); }); - }); - inject(function(log, $rootScope, $compile, $templateCache) { - $templateCache.put('trans.html', '
'); + } + })); + directive('child', valueFn({ + require: '^transclude', + link: function(scope, element, attr, ctrl) { + capturedChildCtrl = ctrl; + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + expect(capturedChildCtrl).toBeTruthy(); + }); + }); - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('pre(); post(unicorn!)'); - }); - }); - it('should copy the directive controller to all clones', function() { - var transcludeCtrl, cloneCount = 2; - module(function() { - directive('transclude', valueFn({ - transclude: 'content', - controller: function($transclude) { - transcludeCtrl = this; - }, - link: function(scope, el, attr, ctrl, $transclude) { - var i; - for (i = 0; i < cloneCount; i++) { - $transclude(cloneAttach); - } + // See issue https://github.com/angular/angular.js/issues/14924 + it('should not process top-level transcluded text nodes merged into their sibling', + function() { + module(function() { + directive('transclude', valueFn({ + template: '', + transclude: true, + scope: {} + })); + }); - function cloneAttach(clone) { - el.append(clone); - } - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - var children = element.children(), i; - expect(transcludeCtrl).toBeDefined(); + inject(function($compile) { + element = jqLite('
'); + element[0].appendChild(document.createTextNode('1{{ value }}')); + element[0].appendChild(document.createTextNode('2{{ value }}')); + element[0].appendChild(document.createTextNode('3{{ value }}')); - expect(element.data('$transcludeController')).toBe(transcludeCtrl); - for (i = 0; i < cloneCount; i++) { - expect(children.eq(i).data('$transcludeController')).toBeUndefined(); - } - }); - }); + var initialWatcherCount = $rootScope.$countWatchers(); + $compile(element)($rootScope); + $rootScope.$apply('value = 0'); + var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount; - it('should provide the $transclude controller local as 5th argument to the pre and post-link function', function() { - var ctrlTransclude, preLinkTransclude, postLinkTransclude; - module(function() { - directive('transclude', valueFn({ - transclude: 'content', - controller: function($transclude) { - ctrlTransclude = $transclude; - }, - compile: function() { - return { - pre: function(scope, el, attr, ctrl, $transclude) { - preLinkTransclude = $transclude; - }, - post: function(scope, el, attr, ctrl, $transclude) { - postLinkTransclude = $transclude; - } - }; - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - expect(ctrlTransclude).toBeDefined(); - expect(ctrlTransclude).toBe(preLinkTransclude); - expect(ctrlTransclude).toBe(postLinkTransclude); - }); + expect(element.text()).toBe('102030'); + expect(newWatcherCount).toBe(3); }); + } + ); - it('should allow an optional scope argument in $transclude', function() { - var capturedChildCtrl; - module(function() { - directive('transclude', valueFn({ - transclude: 'content', - link: function(scope, element, attr, ctrl, $transclude) { - $transclude(scope, function(clone) { - element.append(clone); - }); - } - })); - }); - inject(function($compile) { - element = $compile('
{{$id}}
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toBe('' + $rootScope.$id); - }); - }); + // see issue https://github.com/angular/angular.js/issues/9413 + describe('passing a parent bound transclude function to the link ' + + 'function returned from `$compile`', function() { - it('should expose the directive controller to transcluded children', function() { - var capturedChildCtrl; - module(function() { - directive('transclude', valueFn({ - transclude: 'content', - controller: function() { - }, - link: function(scope, element, attr, ctrl, $transclude) { - $transclude(function(clone) { - element.append(clone); + beforeEach(module(function() { + directive('lazyCompile', function($compile) { + return { + compile: function(tElement, tAttrs) { + var content = tElement.contents(); + tElement.empty(); + return function(scope, element, attrs, ctrls, transcludeFn) { + element.append(content); + $compile(content)(scope, undefined, { + parentBoundTranscludeFn: transcludeFn }); - } - })); - directive('child', valueFn({ - require: '^transclude', - link: function(scope, element, attr, ctrl) { - capturedChildCtrl = ctrl; - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - expect(capturedChildCtrl).toBeTruthy(); - }); + }; + } + }; }); + directive('toggle', valueFn({ + scope: {t: '=toggle'}, + transclude: true, + template: '
' + })); + })); + it('should preserve the bound scope', function() { - // See issue https://github.com/angular/angular.js/issues/14924 - it('should not process top-level transcluded text nodes merged into their sibling', - function() { - module(function() { - directive('transclude', valueFn({ - template: '', - transclude: true, - scope: {} - })); - }); - - inject(function($compile) { - element = jqLite('
'); - element[0].appendChild(document.createTextNode('1{{ value }}')); - element[0].appendChild(document.createTextNode('2{{ value }}')); - element[0].appendChild(document.createTextNode('3{{ value }}')); - - var initialWatcherCount = $rootScope.$countWatchers(); - $compile(element)($rootScope); - $rootScope.$apply('value = 0'); - var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount; - - expect(element.text()).toBe('102030'); - expect(newWatcherCount).toBe(3); - }); - } - ); - + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '
' + + '
' + + 'SuccessError' + + '
' + + '
')($rootScope); - // see issue https://github.com/angular/angular.js/issues/9413 - describe('passing a parent bound transclude function to the link ' + - 'function returned from `$compile`', function() { + $rootScope.$apply('t = false'); + expect($rootScope.$countChildScopes()).toBe(1); + expect(element.text()).toBe(''); - beforeEach(module(function() { - directive('lazyCompile', function($compile) { - return { - compile: function(tElement, tAttrs) { - var content = tElement.contents(); - tElement.empty(); - return function(scope, element, attrs, ctrls, transcludeFn) { - element.append(content); - $compile(content)(scope, undefined, { - parentBoundTranscludeFn: transcludeFn - }); - }; - } - }; - }); - directive('toggle', valueFn({ - scope: {t: '=toggle'}, - transclude: true, - template: '
' - })); - })); + $rootScope.$apply('t = true'); + expect($rootScope.$countChildScopes()).toBe(4); + expect(element.text()).toBe('Success'); - it('should preserve the bound scope', function() { + $rootScope.$apply('t = false'); + expect($rootScope.$countChildScopes()).toBe(1); + expect(element.text()).toBe(''); - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
' + - '
' + - 'SuccessError' + - '
' + - '
')($rootScope); - - $rootScope.$apply('t = false'); - expect($rootScope.$countChildScopes()).toBe(1); - expect(element.text()).toBe(''); - - $rootScope.$apply('t = true'); - expect($rootScope.$countChildScopes()).toBe(4); - expect(element.text()).toBe('Success'); - - $rootScope.$apply('t = false'); - expect($rootScope.$countChildScopes()).toBe(1); - expect(element.text()).toBe(''); - - $rootScope.$apply('t = true'); - expect($rootScope.$countChildScopes()).toBe(4); - expect(element.text()).toBe('Success'); - }); - }); + $rootScope.$apply('t = true'); + expect($rootScope.$countChildScopes()).toBe(4); + expect(element.text()).toBe('Success'); + }); + }); - it('should preserve the bound scope when using recursive transclusion', function() { + it('should preserve the bound scope when using recursive transclusion', function() { - directive('recursiveTransclude', valueFn({ - transclude: true, - template: '
' - })); + directive('recursiveTransclude', valueFn({ + transclude: true, + template: '
' + })); - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
' + - '
' + - '
' + - 'SuccessError' + - '
' + - '
' + - '
')($rootScope); - - $rootScope.$apply('t = false'); - expect($rootScope.$countChildScopes()).toBe(1); - expect(element.text()).toBe(''); - - $rootScope.$apply('t = true'); - expect($rootScope.$countChildScopes()).toBe(4); - expect(element.text()).toBe('Success'); - - $rootScope.$apply('t = false'); - expect($rootScope.$countChildScopes()).toBe(1); - expect(element.text()).toBe(''); - - $rootScope.$apply('t = true'); - expect($rootScope.$countChildScopes()).toBe(4); - expect(element.text()).toBe('Success'); - }); - }); - }); + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '
' + + '
' + + '
' + + 'SuccessError' + + '
' + + '
' + + '
')($rootScope); + $rootScope.$apply('t = false'); + expect($rootScope.$countChildScopes()).toBe(1); + expect(element.text()).toBe(''); - // see issue https://github.com/angular/angular.js/issues/9095 - describe('removing a transcluded element', function() { + $rootScope.$apply('t = true'); + expect($rootScope.$countChildScopes()).toBe(4); + expect(element.text()).toBe('Success'); - beforeEach(module(function() { - directive('toggle', function() { - return { - transclude: true, - template: '
' - }; - }); - })); + $rootScope.$apply('t = false'); + expect($rootScope.$countChildScopes()).toBe(1); + expect(element.text()).toBe(''); + $rootScope.$apply('t = true'); + expect($rootScope.$countChildScopes()).toBe(4); + expect(element.text()).toBe('Success'); + }); + }); + }); - it('should not leak the transclude scope when the transcluded content is an element transclusion directive', - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
{{ msg }}
' + - '
' - )($rootScope); + // see issue https://github.com/angular/angular.js/issues/9095 + describe('removing a transcluded element', function() { - $rootScope.$apply('t = true'); - expect(element.text()).toContain('msg-1'); - // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat - expect($rootScope.$countChildScopes()).toBe(3); - - $rootScope.$apply('t = false'); - expect(element.text()).not.toContain('msg-1'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - - $rootScope.$apply('t = true'); - expect(element.text()).toContain('msg-1'); - // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat - expect($rootScope.$countChildScopes()).toBe(3); - - $rootScope.$apply('t = false'); - expect(element.text()).not.toContain('msg-1'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - })); + beforeEach(module(function() { + directive('toggle', function() { + return { + transclude: true, + template: '
' + }; + }); + })); - it('should not leak the transclude scope when the transcluded content is an multi-element transclusion directive', - inject(function($compile, $rootScope) { + it('should not leak the transclude scope when the transcluded content is an element transclusion directive', + inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
{{ msg }}
' + - '
{{ msg }}
' + - '
' - )($rootScope); + element = $compile( + '
' + + '
{{ msg }}
' + + '
' + )($rootScope); + + $rootScope.$apply('t = true'); + expect(element.text()).toContain('msg-1'); + // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat + expect($rootScope.$countChildScopes()).toBe(3); + + $rootScope.$apply('t = false'); + expect(element.text()).not.toContain('msg-1'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + + $rootScope.$apply('t = true'); + expect(element.text()).toContain('msg-1'); + // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat + expect($rootScope.$countChildScopes()).toBe(3); + + $rootScope.$apply('t = false'); + expect(element.text()).not.toContain('msg-1'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + })); - $rootScope.$apply('t = true'); - expect(element.text()).toContain('msg-1msg-1'); - // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat - expect($rootScope.$countChildScopes()).toBe(3); - - $rootScope.$apply('t = false'); - expect(element.text()).not.toContain('msg-1msg-1'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - - $rootScope.$apply('t = true'); - expect(element.text()).toContain('msg-1msg-1'); - // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat - expect($rootScope.$countChildScopes()).toBe(3); - - $rootScope.$apply('t = false'); - expect(element.text()).not.toContain('msg-1msg-1'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - })); + it('should not leak the transclude scope when the transcluded content is an multi-element transclusion directive', + inject(function($compile, $rootScope) { - it('should not leak the transclude scope if the transcluded contains only comments', - inject(function($compile, $rootScope) { + element = $compile( + '
' + + '
{{ msg }}
' + + '
{{ msg }}
' + + '
' + )($rootScope); + + $rootScope.$apply('t = true'); + expect(element.text()).toContain('msg-1msg-1'); + // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat + expect($rootScope.$countChildScopes()).toBe(3); + + $rootScope.$apply('t = false'); + expect(element.text()).not.toContain('msg-1msg-1'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + + $rootScope.$apply('t = true'); + expect(element.text()).toContain('msg-1msg-1'); + // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat + expect($rootScope.$countChildScopes()).toBe(3); + + $rootScope.$apply('t = false'); + expect(element.text()).not.toContain('msg-1msg-1'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + })); - element = $compile( - '
' + - '' + - '
' - )($rootScope); - $rootScope.$apply('t = true'); - expect(element.html()).toContain('some comment'); - // Expected scopes: $rootScope, ngIf, transclusion - expect($rootScope.$countChildScopes()).toBe(2); - - $rootScope.$apply('t = false'); - expect(element.html()).not.toContain('some comment'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - - $rootScope.$apply('t = true'); - expect(element.html()).toContain('some comment'); - // Expected scopes: $rootScope, ngIf, transclusion - expect($rootScope.$countChildScopes()).toBe(2); - - $rootScope.$apply('t = false'); - expect(element.html()).not.toContain('some comment'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - })); + it('should not leak the transclude scope if the transcluded contains only comments', + inject(function($compile, $rootScope) { - it('should not leak the transclude scope if the transcluded contains only text nodes', - inject(function($compile, $rootScope) { + element = $compile( + '
' + + '' + + '
' + )($rootScope); + + $rootScope.$apply('t = true'); + expect(element.html()).toContain('some comment'); + // Expected scopes: $rootScope, ngIf, transclusion + expect($rootScope.$countChildScopes()).toBe(2); + + $rootScope.$apply('t = false'); + expect(element.html()).not.toContain('some comment'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + + $rootScope.$apply('t = true'); + expect(element.html()).toContain('some comment'); + // Expected scopes: $rootScope, ngIf, transclusion + expect($rootScope.$countChildScopes()).toBe(2); + + $rootScope.$apply('t = false'); + expect(element.html()).not.toContain('some comment'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + })); - element = $compile( - '
' + - 'some text' + - '
' - )($rootScope); + it('should not leak the transclude scope if the transcluded contains only text nodes', + inject(function($compile, $rootScope) { - $rootScope.$apply('t = true'); - expect(element.html()).toContain('some text'); - // Expected scopes: $rootScope, ngIf, transclusion - expect($rootScope.$countChildScopes()).toBe(2); - - $rootScope.$apply('t = false'); - expect(element.html()).not.toContain('some text'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - - $rootScope.$apply('t = true'); - expect(element.html()).toContain('some text'); - // Expected scopes: $rootScope, ngIf, transclusion - expect($rootScope.$countChildScopes()).toBe(2); - - $rootScope.$apply('t = false'); - expect(element.html()).not.toContain('some text'); - // Expected scopes: $rootScope - expect($rootScope.$countChildScopes()).toBe(0); - })); + element = $compile( + '
' + + 'some text' + + '
' + )($rootScope); + + $rootScope.$apply('t = true'); + expect(element.html()).toContain('some text'); + // Expected scopes: $rootScope, ngIf, transclusion + expect($rootScope.$countChildScopes()).toBe(2); + + $rootScope.$apply('t = false'); + expect(element.html()).not.toContain('some text'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + + $rootScope.$apply('t = true'); + expect(element.html()).toContain('some text'); + // Expected scopes: $rootScope, ngIf, transclusion + expect($rootScope.$countChildScopes()).toBe(2); + + $rootScope.$apply('t = false'); + expect(element.html()).not.toContain('some text'); + // Expected scopes: $rootScope + expect($rootScope.$countChildScopes()).toBe(0); + })); - it('should mark as destroyed all sub scopes of the scope being destroyed', - inject(function($compile, $rootScope) { + it('should mark as destroyed all sub scopes of the scope being destroyed', + inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
{{ msg }}
' + - '
' - )($rootScope); + element = $compile( + '
' + + '
{{ msg }}
' + + '
' + )($rootScope); - $rootScope.$apply('t = true'); - var childScopes = getChildScopes($rootScope); + $rootScope.$apply('t = true'); + var childScopes = getChildScopes($rootScope); - $rootScope.$apply('t = false'); - for (var i = 0; i < childScopes.length; ++i) { - expect(childScopes[i].$$destroyed).toBe(true); - } - })); - }); + $rootScope.$apply('t = false'); + for (var i = 0; i < childScopes.length; ++i) { + expect(childScopes[i].$$destroyed).toBe(true); + } + })); + }); - describe('nested transcludes', function() { + describe('nested transcludes', function() { - beforeEach(module(function($compileProvider) { + beforeEach(module(function($compileProvider) { - $compileProvider.directive('noop', valueFn({})); + $compileProvider.directive('noop', valueFn({})); - $compileProvider.directive('sync', valueFn({ - template: '
', - transclude: true - })); + $compileProvider.directive('sync', valueFn({ + template: '
', + transclude: true + })); - $compileProvider.directive('async', valueFn({ - templateUrl: 'async', - transclude: true - })); + $compileProvider.directive('async', valueFn({ + templateUrl: 'async', + transclude: true + })); - $compileProvider.directive('syncSync', valueFn({ - template: '
', - transclude: true - })); + $compileProvider.directive('syncSync', valueFn({ + template: '
', + transclude: true + })); - $compileProvider.directive('syncAsync', valueFn({ - template: '
', - transclude: true - })); + $compileProvider.directive('syncAsync', valueFn({ + template: '
', + transclude: true + })); - $compileProvider.directive('asyncSync', valueFn({ - templateUrl: 'asyncSync', - transclude: true - })); + $compileProvider.directive('asyncSync', valueFn({ + templateUrl: 'asyncSync', + transclude: true + })); - $compileProvider.directive('asyncAsync', valueFn({ - templateUrl: 'asyncAsync', - transclude: true - })); + $compileProvider.directive('asyncAsync', valueFn({ + templateUrl: 'asyncAsync', + transclude: true + })); - })); + })); - beforeEach(inject(function($templateCache) { - $templateCache.put('async', '
'); - $templateCache.put('asyncSync', '
'); - $templateCache.put('asyncAsync', '
'); - })); + beforeEach(inject(function($templateCache) { + $templateCache.put('async', '
'); + $templateCache.put('asyncSync', '
'); + $templateCache.put('asyncAsync', '
'); + })); - it('should allow nested transclude directives with sync template containing sync template', inject(function($compile, $rootScope) { - element = $compile('
transcluded content
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + it('should allow nested transclude directives with sync template containing sync template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - it('should allow nested transclude directives with sync template containing async template', inject(function($compile, $rootScope) { - element = $compile('
transcluded content
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + it('should allow nested transclude directives with sync template containing async template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - it('should allow nested transclude directives with async template containing sync template', inject(function($compile, $rootScope) { - element = $compile('
transcluded content
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + it('should allow nested transclude directives with async template containing sync template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - it('should allow nested transclude directives with async template containing asynch template', inject(function($compile, $rootScope) { - element = $compile('
transcluded content
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + it('should allow nested transclude directives with async template containing asynch template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - it('should not leak memory with nested transclusion', function() { - inject(function($compile, $rootScope) { - var size, initialSize = jqLiteCacheSize(); + it('should not leak memory with nested transclusion', function() { + inject(function($compile, $rootScope) { + var size, initialSize = jqLiteCacheSize(); - element = jqLite('
  • {{n}} => EvenOdd
'); - $compile(element)($rootScope.$new()); + element = jqLite('
  • {{n}} => EvenOdd
'); + $compile(element)($rootScope.$new()); - $rootScope.nums = [0,1,2]; - $rootScope.$apply(); - size = jqLiteCacheSize(); + $rootScope.nums = [0,1,2]; + $rootScope.$apply(); + size = jqLiteCacheSize(); - $rootScope.nums = [3,4,5]; - $rootScope.$apply(); - expect(jqLiteCacheSize()).toEqual(size); + $rootScope.nums = [3,4,5]; + $rootScope.$apply(); + expect(jqLiteCacheSize()).toEqual(size); - element.remove(); - expect(jqLiteCacheSize()).toEqual(initialSize); - }); - }); + element.remove(); + expect(jqLiteCacheSize()).toEqual(initialSize); }); + }); + }); - describe('nested isolated scope transcludes', function() { - beforeEach(module(function($compileProvider) { + describe('nested isolated scope transcludes', function() { + beforeEach(module(function($compileProvider) { - $compileProvider.directive('trans', valueFn({ - restrict: 'E', - template: '
', - transclude: true - })); + $compileProvider.directive('trans', valueFn({ + restrict: 'E', + template: '
', + transclude: true + })); - $compileProvider.directive('transAsync', valueFn({ - restrict: 'E', - templateUrl: 'transAsync', - transclude: true - })); + $compileProvider.directive('transAsync', valueFn({ + restrict: 'E', + templateUrl: 'transAsync', + transclude: true + })); - $compileProvider.directive('iso', valueFn({ - restrict: 'E', - transclude: true, - template: '', - scope: {} - })); - $compileProvider.directive('isoAsync1', valueFn({ - restrict: 'E', - transclude: true, - template: '', - scope: {} - })); - $compileProvider.directive('isoAsync2', valueFn({ - restrict: 'E', - transclude: true, - templateUrl: 'isoAsync', - scope: {} - })); - })); + $compileProvider.directive('iso', valueFn({ + restrict: 'E', + transclude: true, + template: '', + scope: {} + })); + $compileProvider.directive('isoAsync1', valueFn({ + restrict: 'E', + transclude: true, + template: '', + scope: {} + })); + $compileProvider.directive('isoAsync2', valueFn({ + restrict: 'E', + transclude: true, + templateUrl: 'isoAsync', + scope: {} + })); + })); + + beforeEach(inject(function($templateCache) { + $templateCache.put('transAsync', '
'); + $templateCache.put('isoAsync', ''); + })); - beforeEach(inject(function($templateCache) { - $templateCache.put('transAsync', '
'); - $templateCache.put('isoAsync', ''); - })); + it('should pass the outer scope to the transclude on the isolated template sync-sync', inject(function($compile, $rootScope) { - it('should pass the outer scope to the transclude on the isolated template sync-sync', inject(function($compile, $rootScope) { + $rootScope.val = 'transcluded content'; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - $rootScope.val = 'transcluded content'; - element = $compile('')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) { - it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) { + $rootScope.val = 'transcluded content'; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - $rootScope.val = 'transcluded content'; - element = $compile('')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + it('should pass the outer scope to the transclude on the isolated template async-async', inject(function($compile, $rootScope) { - it('should pass the outer scope to the transclude on the isolated template async-async', inject(function($compile, $rootScope) { + $rootScope.val = 'transcluded content'; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - $rootScope.val = 'transcluded content'; - element = $compile('')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + }); - }); + describe('multiple siblings receiving transclusion', function() { - describe('multiple siblings receiving transclusion', function() { + it('should only receive transclude from parent', function() { - it('should only receive transclude from parent', function() { + module(function($compileProvider) { - module(function($compileProvider) { + $compileProvider.directive('myExample', valueFn({ + scope: {}, + link: function link(scope, element, attrs) { + var foo = element[0].querySelector('.foo'); + scope.children = angular.element(foo).children().length; + }, + template: '
' + + '
myExample {{children}}!
' + + '
has children
' + + '
' + + '
', + transclude: true - $compileProvider.directive('myExample', valueFn({ - scope: {}, - link: function link(scope, element, attrs) { - var foo = element[0].querySelector('.foo'); - scope.children = angular.element(foo).children().length; - }, - template: '
' + - '
myExample {{children}}!
' + - '
has children
' + - '
' + - '
', - transclude: true - - })); + })); - }); + }); - inject(function($compile, $rootScope) { - var element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('myExample 0!'); - dealoc(element); + inject(function($compile, $rootScope) { + var element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('myExample 0!'); + dealoc(element); - element = $compile('

')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('myExample 1!has children'); - dealoc(element); - }); - }); + element = $compile('

')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('myExample 1!has children'); + dealoc(element); }); }); + }); + }); - describe('element transclusion', function() { + describe('element transclusion', function() { - it('should support basic element transclusion', function() { - module(function() { - directive('trans', function(log) { - return { - transclude: 'element', - priority: 2, - controller: function($transclude) { this.$transclude = $transclude; }, - compile: function(element, attrs, template) { - log('compile: ' + angular.mock.dump(element)); - return function(scope, element, attrs, ctrl) { - log('link'); - var cursor = element; - template(scope.$new(), function(clone) {cursor.after(cursor = clone);}); - ctrl.$transclude(function(clone) {cursor.after(clone);}); - }; - } + it('should support basic element transclusion', function() { + module(function() { + directive('trans', function(log) { + return { + transclude: 'element', + priority: 2, + controller: function($transclude) { this.$transclude = $transclude; }, + compile: function(element, attrs, template) { + log('compile: ' + angular.mock.dump(element)); + return function(scope, element, attrs, ctrl) { + log('link'); + var cursor = element; + template(scope.$new(), function(clone) {cursor.after(cursor = clone);}); + ctrl.$transclude(function(clone) {cursor.after(clone);}); }; - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
{{$parent.$id}}-{{$id}};
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('compile: ; link; LOG; LOG; HIGH'); - expect(element.text()).toEqual('1-2;1-3;'); - }); + } + }; }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
{{$parent.$id}}-{{$id}};
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('compile: ; link; LOG; LOG; HIGH'); + expect(element.text()).toEqual('1-2;1-3;'); + }); + }); - it('should only allow one element transclusion per element', function() { - module(function() { - directive('first', valueFn({ - transclude: 'element' - })); - directive('second', valueFn({ - transclude: 'element' - })); - }); - inject(function($compile) { - expect(function() { - $compile('
'); - }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' + - ''); - }); - }); + it('should only allow one element transclusion per element', function() { + module(function() { + directive('first', valueFn({ + transclude: 'element' + })); + directive('second', valueFn({ + transclude: 'element' + })); + }); + inject(function($compile) { + expect(function() { + $compile('
'); + }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' + + ''); + }); + }); - it('should only allow one element transclusion per element when directives have different priorities', function() { - // we restart compilation in this case and we need to remember the duplicates during the second compile - // regression #3893 - module(function() { - directive('first', valueFn({ - transclude: 'element', - priority: 100 - })); - directive('second', valueFn({ - transclude: 'element' - })); - }); - inject(function($compile) { - expect(function() { - $compile('
'); - }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
'); + }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
template.html

'); + it('should only allow one element transclusion per element when async replace directive is in the mix', function() { + module(function() { + directive('template', valueFn({ + templateUrl: 'template.html', + replace: true + })); + directive('first', valueFn({ + transclude: 'element', + priority: 100 + })); + directive('second', valueFn({ + transclude: 'element' + })); + }); + inject(function($compile, $httpBackend) { + $httpBackend.expectGET('template.html').respond('

template.html

'); - expect(function() { - $compile('
'); - $httpBackend.flush(); - }).toThrowMinErr('$compile', 'multidir', - 'Multiple directives [first, second] asking for transclusion on:

'); + $httpBackend.flush(); + }).toThrowMinErr('$compile', 'multidir', + 'Multiple directives [first, second] asking for transclusion on:

', - replace: true - })); - directive('first', valueFn({ - transclude: 'element', - priority: 100 - })); - directive('second', valueFn({ - transclude: 'element' - })); - }); - inject(function($compile) { - expect(function() { - $compile('
'); - }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:

', + replace: true + })); + directive('first', valueFn({ + transclude: 'element', + priority: 100 + })); + directive('second', valueFn({ + transclude: 'element' + })); + }); + inject(function($compile) { + expect(function() { + $compile('
'); + }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:

before

after
').contents(); - expect(element.length).toEqual(3); - expect(nodeName_(element[1])).toBe('div'); - $compile(element)($rootScope); - expect(nodeName_(element[1])).toBe('#comment'); - expect(nodeName_(comment)).toBe('#comment'); - }); - }); + it('should support transcluded element on root content', function() { + var comment; + module(function() { + directive('transclude', valueFn({ + transclude: 'element', + compile: function(element, attr, linker) { + return function(scope, element, attr) { + comment = element; + }; + } + })); + }); + inject(function($compile, $rootScope) { + var element = jqLite('
before
after
').contents(); + expect(element.length).toEqual(3); + expect(nodeName_(element[1])).toBe('div'); + $compile(element)($rootScope); + expect(nodeName_(element[1])).toBe('#comment'); + expect(nodeName_(comment)).toBe('#comment'); + }); + }); - it('should terminate compilation only for element transclusion', function() { - module(function() { - directive('elementTrans', function(log) { - return { - transclude: 'element', - priority: 50, - compile: log.fn('compile:elementTrans') - }; - }); - directive('regularTrans', function(log) { - return { - transclude: true, - priority: 50, - compile: log.fn('compile:regularTrans') - }; - }); - }); - inject(function(log, $compile, $rootScope) { - $compile('
')($rootScope); - expect(log).toEqual('compile:elementTrans; compile:regularTrans; regular'); - }); + it('should terminate compilation only for element transclusion', function() { + module(function() { + directive('elementTrans', function(log) { + return { + transclude: 'element', + priority: 50, + compile: log.fn('compile:elementTrans') + }; + }); + directive('regularTrans', function(log) { + return { + transclude: true, + priority: 50, + compile: log.fn('compile:regularTrans') + }; }); + }); + inject(function(log, $compile, $rootScope) { + $compile('
')($rootScope); + expect(log).toEqual('compile:elementTrans; compile:regularTrans; regular'); + }); + }); - it('should instantiate high priority controllers only once, but low priority ones each time we transclude', - function() { - module(function() { - directive('elementTrans', function(log) { - return { - transclude: 'element', - priority: 50, - controller: function($transclude, $element) { - log('controller:elementTrans'); - $transclude(function(clone) { - $element.after(clone); - }); - $transclude(function(clone) { - $element.after(clone); - }); - $transclude(function(clone) { - $element.after(clone); - }); - } - }; - }); - directive('normalDir', function(log) { - return { - controller: function() { - log('controller:normalDir'); - } - }; - }); - }); - inject(function($compile, $rootScope, log) { - element = $compile('
')($rootScope); - expect(log).toEqual([ - 'controller:elementTrans', - 'controller:normalDir', - 'controller:normalDir', - 'controller:normalDir' - ]); - }); + it('should instantiate high priority controllers only once, but low priority ones each time we transclude', + function() { + module(function() { + directive('elementTrans', function(log) { + return { + transclude: 'element', + priority: 50, + controller: function($transclude, $element) { + log('controller:elementTrans'); + $transclude(function(clone) { + $element.after(clone); + }); + $transclude(function(clone) { + $element.after(clone); + }); + $transclude(function(clone) { + $element.after(clone); + }); + } + }; }); - - it('should allow to access $transclude in the same directive', function() { - var _$transclude; - module(function() { - directive('transclude', valueFn({ - transclude: 'element', - controller: function($transclude) { - _$transclude = $transclude; - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - expect(_$transclude).toBeDefined(); - }); + directive('normalDir', function(log) { + return { + controller: function() { + log('controller:normalDir'); + } + }; }); + }); + inject(function($compile, $rootScope, log) { + element = $compile('
')($rootScope); + expect(log).toEqual([ + 'controller:elementTrans', + 'controller:normalDir', + 'controller:normalDir', + 'controller:normalDir' + ]); + }); + }); - it('should copy the directive controller to all clones', function() { - var transcludeCtrl, cloneCount = 2; - module(function() { - directive('transclude', valueFn({ - transclude: 'element', - controller: function() { - transcludeCtrl = this; - }, - link: function(scope, el, attr, ctrl, $transclude) { - var i; - for (i = 0; i < cloneCount; i++) { - $transclude(cloneAttach); - } + it('should allow to access $transclude in the same directive', function() { + var _$transclude; + module(function() { + directive('transclude', valueFn({ + transclude: 'element', + controller: function($transclude) { + _$transclude = $transclude; + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + expect(_$transclude).toBeDefined(); + }); + }); - function cloneAttach(clone) { - el.after(clone); - } - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - var children = element.children(), i; + it('should copy the directive controller to all clones', function() { + var transcludeCtrl, cloneCount = 2; + module(function() { + directive('transclude', valueFn({ + transclude: 'element', + controller: function() { + transcludeCtrl = this; + }, + link: function(scope, el, attr, ctrl, $transclude) { + var i; for (i = 0; i < cloneCount; i++) { - expect(children.eq(i).data('$transcludeController')).toBe(transcludeCtrl); + $transclude(cloneAttach); } - }); - }); - - it('should expose the directive controller to transcluded children', function() { - var capturedTranscludeCtrl; - module(function() { - directive('transclude', valueFn({ - transclude: 'element', - controller: function() { - }, - link: function(scope, element, attr, ctrl, $transclude) { - $transclude(scope, function(clone) { - element.after(clone); - }); - } - })); - directive('child', valueFn({ - require: '^transclude', - link: function(scope, element, attr, ctrl) { - capturedTranscludeCtrl = ctrl; - } - })); - }); - inject(function($compile) { - // 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(); - }); - }); - it('should allow access to $transclude in a templateUrl directive', function() { - var transclude; - module(function() { - directive('template', valueFn({ - templateUrl: 'template.html', - replace: true - })); - directive('transclude', valueFn({ - transclude: 'content', - controller: function($transclude) { - transclude = $transclude; - } - })); - }); - inject(function($compile, $httpBackend) { - $httpBackend.expectGET('template.html').respond('
'); - element = $compile('
')($rootScope); - $httpBackend.flush(); - expect(transclude).toBeDefined(); - }); - }); - - // issue #6006 - it('should link directive with $element as a comment node', function() { - module(function($provide) { - directive('innerAgain', function(log) { - return { - transclude: 'element', - link: function(scope, element, attr, controllers, transclude) { - log('innerAgain:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); - transclude(scope, function(clone) { - element.parent().append(clone); - }); - } - }; - }); - directive('inner', function(log) { - return { - replace: true, - templateUrl: 'inner.html', - link: function(scope, element) { - log('inner:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); - } - }; - }); - directive('outer', function(log) { - return { - transclude: 'element', - link: function(scope, element, attrs, controllers, transclude) { - log('outer:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); - transclude(scope, function(clone) { - element.parent().append(clone); - }); - } - }; + function cloneAttach(clone) { + el.after(clone); + } + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + var children = element.children(), i; + for (i = 0; i < cloneCount; i++) { + expect(children.eq(i).data('$transcludeController')).toBe(transcludeCtrl); + } + }); + }); + + it('should expose the directive controller to transcluded children', function() { + var capturedTranscludeCtrl; + module(function() { + directive('transclude', valueFn({ + transclude: 'element', + controller: function() { + }, + link: function(scope, element, attr, ctrl, $transclude) { + $transclude(scope, function(clone) { + element.after(clone); }); - }); - inject(function(log, $compile, $rootScope, $templateCache) { - $templateCache.put('inner.html', '

Content

'); - element = $compile('
')($rootScope); - $rootScope.$digest(); - var child = element.children(); - - expect(log.toArray()).toEqual([ - 'outer:#comment:outer:', - 'innerAgain:#comment:innerAgain:', - 'inner:#comment:innerAgain:' - ]); - expect(child.length).toBe(1); - expect(child.contents().length).toBe(2); - expect(lowercase(nodeName_(child.contents().eq(0)))).toBe('#comment'); - expect(lowercase(nodeName_(child.contents().eq(1)))).toBe('div'); - }); - }); + } + })); + directive('child', valueFn({ + require: '^transclude', + link: function(scope, element, attr, ctrl) { + capturedTranscludeCtrl = ctrl; + } + })); }); + inject(function($compile) { + // 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(); + }); + }); + it('should allow access to $transclude in a templateUrl directive', function() { + var transclude; + module(function() { + directive('template', valueFn({ + templateUrl: 'template.html', + replace: true + })); + directive('transclude', valueFn({ + transclude: 'content', + controller: function($transclude) { + transclude = $transclude; + } + })); + }); + inject(function($compile, $httpBackend) { + $httpBackend.expectGET('template.html').respond('
'); + element = $compile('
')($rootScope); + $httpBackend.flush(); + expect(transclude).toBeDefined(); + }); + }); - it('should be possible to change the scope of a directive using $provide', function() { - module(function($provide) { - directive('foo', function() { - return { - scope: {}, - template: '
' - }; - }); - $provide.decorator('fooDirective', function($delegate) { - var directive = $delegate[0]; - directive.scope.something = '='; - directive.template = '{{something}}'; - return $delegate; - }); + // issue #6006 + it('should link directive with $element as a comment node', function() { + module(function($provide) { + directive('innerAgain', function(log) { + return { + transclude: 'element', + link: function(scope, element, attr, controllers, transclude) { + log('innerAgain:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); + transclude(scope, function(clone) { + element.parent().append(clone); + }); + } + }; }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.text()).toBe('bar'); + directive('inner', function(log) { + return { + replace: true, + templateUrl: 'inner.html', + link: function(scope, element) { + log('inner:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); + } + }; }); + directive('outer', function(log) { + return { + transclude: 'element', + link: function(scope, element, attrs, controllers, transclude) { + log('outer:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); + transclude(scope, function(clone) { + element.parent().append(clone); + }); + } + }; + }); + }); + inject(function(log, $compile, $rootScope, $templateCache) { + $templateCache.put('inner.html', '

Content

'); + element = $compile('
')($rootScope); + $rootScope.$digest(); + var child = element.children(); + + expect(log.toArray()).toEqual([ + 'outer:#comment:outer:', + 'innerAgain:#comment:innerAgain:', + 'inner:#comment:innerAgain:' + ]); + expect(child.length).toBe(1); + expect(child.contents().length).toBe(2); + expect(lowercase(nodeName_(child.contents().eq(0)))).toBe('#comment'); + expect(lowercase(nodeName_(child.contents().eq(1)))).toBe('div'); }); + }); + }); - it('should distinguish different bindings with the same binding name', function() { - module(function() { - directive('foo', function() { - return { - scope: { - foo: '=', - bar: '=' - }, - template: '
{{foo}}
{{bar}}
' - }; - }); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toBe('foobar'); - }); + it('should be possible to change the scope of a directive using $provide', function() { + module(function($provide) { + directive('foo', function() { + return { + scope: {}, + template: '
' + }; + }); + $provide.decorator('fooDirective', function($delegate) { + var directive = $delegate[0]; + directive.scope.something = '='; + directive.template = '{{something}}'; + return $delegate; }); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.text()).toBe('bar'); + }); + }); - it('should safely create transclude comment node and not break with "-->"', - inject(function($rootScope) { - // see: https://github.com/angular/angular.js/issues/1740 - element = $compile('
  • {{item}}|
')($rootScope); - $rootScope.$digest(); + it('should distinguish different bindings with the same binding name', function() { + module(function() { + directive('foo', function() { + return { + scope: { + foo: '=', + bar: '=' + }, + template: '
{{foo}}
{{bar}}
' + }; + }); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('foobar'); + }); + }); - expect(element.text()).toBe('-->|x|'); - })); + it('should safely create transclude comment node and not break with "-->"', + inject(function($rootScope) { + // see: https://github.com/angular/angular.js/issues/1740 + element = $compile('
  • {{item}}|
')($rootScope); + $rootScope.$digest(); - describe('lazy compilation', function() { - // See https://github.com/angular/angular.js/issues/7183 - it('should pass transclusion through to template of a \'replace\' directive', function() { - module(function() { - directive('transSync', function() { - return { - transclude: true, - link: function(scope, element, attr, ctrl, transclude) { + expect(element.text()).toBe('-->|x|'); + })); - expect(transclude).toEqual(jasmine.any(Function)); - transclude(function(child) { element.append(child); }); - } - }; - }); + describe('lazy compilation', function() { + // See https://github.com/angular/angular.js/issues/7183 + it('should pass transclusion through to template of a \'replace\' directive', function() { + module(function() { + directive('transSync', function() { + return { + transclude: true, + link: function(scope, element, attr, ctrl, transclude) { - directive('trans', function($timeout) { - return { - transclude: true, - link: function(scope, element, attrs, ctrl, transclude) { + expect(transclude).toEqual(jasmine.any(Function)); - // We use timeout here to simulate how ng-if works - $timeout(function() { - transclude(function(child) { element.append(child); }); - }); - } - }; - }); + transclude(function(child) { element.append(child); }); + } + }; + }); - directive('replaceWithTemplate', function() { - return { - templateUrl: 'template.html', - replace: true - }; - }); - }); + directive('trans', function($timeout) { + return { + transclude: true, + link: function(scope, element, attrs, ctrl, transclude) { - inject(function($compile, $rootScope, $templateCache, $timeout) { + // We use timeout here to simulate how ng-if works + $timeout(function() { + transclude(function(child) { element.append(child); }); + }); + } + }; + }); - $templateCache.put('template.html', '
Content To Be Transcluded
'); + directive('replaceWithTemplate', function() { + return { + templateUrl: 'template.html', + replace: true + }; + }); + }); - expect(function() { - element = $compile('
')($rootScope); - $timeout.flush(); - }).not.toThrow(); + inject(function($compile, $rootScope, $templateCache, $timeout) { - expect(element.text()).toEqual('Content To Be Transcluded'); - }); + $templateCache.put('template.html', '
Content To Be Transcluded
'); - }); + expect(function() { + element = $compile('
')($rootScope); + $timeout.flush(); + }).not.toThrow(); - it('should lazily compile the contents of directives that are transcluded', function() { - var innerCompilationCount = 0, transclude; + expect(element.text()).toEqual('Content To Be Transcluded'); + }); - module(function() { - directive('trans', valueFn({ - transclude: true, - controller: function($transclude) { - transclude = $transclude; - } - })); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + it('should lazily compile the contents of directives that are transcluded', function() { + var innerCompilationCount = 0, transclude; - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - expect(innerCompilationCount).toBe(0); - transclude(function(child) { element.append(child); }); - expect(innerCompilationCount).toBe(1); - expect(element.text()).toBe('FooBar'); - }); - }); + module(function() { + directive('trans', valueFn({ + transclude: true, + controller: function($transclude) { + transclude = $transclude; + } + })); - it('should lazily compile the contents of directives that are transcluded with a template', function() { - var innerCompilationCount = 0, transclude; + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - module(function() { - directive('trans', valueFn({ - transclude: true, - template: '
Baz
', - controller: function($transclude) { - transclude = $transclude; - } - })); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(innerCompilationCount).toBe(0); + transclude(function(child) { element.append(child); }); + expect(innerCompilationCount).toBe(1); + expect(element.text()).toBe('FooBar'); + }); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + it('should lazily compile the contents of directives that are transcluded with a template', function() { + var innerCompilationCount = 0, transclude; - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - expect(innerCompilationCount).toBe(0); - transclude(function(child) { element.append(child); }); - expect(innerCompilationCount).toBe(1); - expect(element.text()).toBe('BazFooBar'); - }); - }); + module(function() { + directive('trans', valueFn({ + transclude: true, + template: '
Baz
', + controller: function($transclude) { + transclude = $transclude; + } + })); - it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() { - var innerCompilationCount = 0, transclude; + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - module(function() { - directive('trans', valueFn({ - transclude: true, - templateUrl: 'baz.html', - controller: function($transclude) { - transclude = $transclude; - } - })); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(innerCompilationCount).toBe(0); + transclude(function(child) { element.append(child); }); + expect(innerCompilationCount).toBe(1); + expect(element.text()).toBe('BazFooBar'); + }); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() { + var innerCompilationCount = 0, transclude; - inject(function($compile, $rootScope, $httpBackend) { - $httpBackend.expectGET('baz.html').respond('
Baz
'); - element = $compile('')($rootScope); - $httpBackend.flush(); + module(function() { + directive('trans', valueFn({ + transclude: true, + templateUrl: 'baz.html', + controller: function($transclude) { + transclude = $transclude; + } + })); - expect(innerCompilationCount).toBe(0); - transclude(function(child) { element.append(child); }); - expect(innerCompilationCount).toBe(1); - expect(element.text()).toBe('BazFooBar'); - }); - }); + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - it('should lazily compile the contents of directives that are transclude element', function() { - var innerCompilationCount = 0, transclude; + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.expectGET('baz.html').respond('
Baz
'); + element = $compile('')($rootScope); + $httpBackend.flush(); - module(function() { - directive('trans', valueFn({ - transclude: 'element', - controller: function($transclude) { - transclude = $transclude; - } - })); + expect(innerCompilationCount).toBe(0); + transclude(function(child) { element.append(child); }); + expect(innerCompilationCount).toBe(1); + expect(element.text()).toBe('BazFooBar'); + }); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + it('should lazily compile the contents of directives that are transclude element', function() { + var innerCompilationCount = 0, transclude; - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - expect(innerCompilationCount).toBe(0); - transclude(function(child) { element.append(child); }); - expect(innerCompilationCount).toBe(1); - expect(element.text()).toBe('FooBar'); - }); - }); + module(function() { + directive('trans', valueFn({ + transclude: 'element', + controller: function($transclude) { + transclude = $transclude; + } + })); - it('should lazily compile transcluded directives with ngIf on them', function() { - var innerCompilationCount = 0, outerCompilationCount = 0, transclude; + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - module(function() { - directive('outer', valueFn({ - transclude: true, - compile: function() { - outerCompilationCount += 1; - }, - controller: function($transclude) { - transclude = $transclude; - } - })); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + expect(innerCompilationCount).toBe(0); + transclude(function(child) { element.append(child); }); + expect(innerCompilationCount).toBe(1); + expect(element.text()).toBe('FooBar'); + }); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + it('should lazily compile transcluded directives with ngIf on them', function() { + var innerCompilationCount = 0, outerCompilationCount = 0, transclude; - inject(function($compile, $rootScope) { - $rootScope.shouldCompile = false; - - element = $compile('
')($rootScope); - expect(outerCompilationCount).toBe(0); - expect(innerCompilationCount).toBe(0); - expect(transclude).toBeUndefined(); - $rootScope.$apply('shouldCompile=true'); - expect(outerCompilationCount).toBe(1); - expect(innerCompilationCount).toBe(0); - expect(transclude).toBeDefined(); - transclude(function(child) { element.append(child); }); - expect(outerCompilationCount).toBe(1); - expect(innerCompilationCount).toBe(1); - expect(element.text()).toBe('FooBar'); - }); - }); + module(function() { + directive('outer', valueFn({ + transclude: true, + compile: function() { + outerCompilationCount += 1; + }, + controller: function($transclude) { + transclude = $transclude; + } + })); - it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() { - var innerCompilationCount = 0; + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - module(function() { - directive('outer', valueFn({ - transclude: true - })); + inject(function($compile, $rootScope) { + $rootScope.shouldCompile = false; - directive('outer', valueFn({ - templateUrl: 'inner.html', - replace: true - })); + element = $compile('
')($rootScope); + expect(outerCompilationCount).toBe(0); + expect(innerCompilationCount).toBe(0); + expect(transclude).toBeUndefined(); + $rootScope.$apply('shouldCompile=true'); + expect(outerCompilationCount).toBe(1); + expect(innerCompilationCount).toBe(0); + expect(transclude).toBeDefined(); + transclude(function(child) { element.append(child); }); + expect(outerCompilationCount).toBe(1); + expect(innerCompilationCount).toBe(1); + expect(element.text()).toBe('FooBar'); + }); + }); - directive('inner', valueFn({ - compile: function() { - innerCompilationCount += 1; - } - })); - }); + it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() { + var innerCompilationCount = 0; - inject(function($compile, $rootScope, $httpBackend) { - $httpBackend.expectGET('inner.html').respond(''); - element = $compile('')($rootScope); - $httpBackend.flush(); + module(function() { + directive('outer', valueFn({ + transclude: true + })); - expect(innerCompilationCount).toBe(1); - }); - }); + directive('outer', valueFn({ + templateUrl: 'inner.html', + replace: true + })); + + directive('inner', valueFn({ + compile: function() { + innerCompilationCount += 1; + } + })); }); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.expectGET('inner.html').respond(''); + element = $compile('')($rootScope); + $httpBackend.flush(); + + expect(innerCompilationCount).toBe(1); + }); }); }); + }); describe('multi-slot transclude', function() { diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 70b8a97fd98b..441509376561 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -2039,14 +2039,29 @@ describe('ngMock', function() { describe('$controllerDecorator', function() { - describe('with `preAssignBindingsEnabled(true)`', function() { - - beforeEach(module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(true); - })); + it('should support creating controller with bindings', function() { + var called = false; + var data = [ + { name: 'derp1', id: 0 }, + { name: 'testname', id: 1 }, + { name: 'flurp', id: 2 } + ]; + module(function($controllerProvider) { + $controllerProvider.register('testCtrl', function() { + expect(this.data).toBeUndefined(); + called = true; + }); + }); + inject(function($controller, $rootScope) { + var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); + expect(ctrl.data).toBe(data); + expect(called).toBe(true); + }); + }); - it('should support creating controller with bindings', function() { + it('should support assigning bindings when a value is returned from the constructor', + function() { var called = false; var data = [ { name: 'derp1', id: 0 }, @@ -2055,8 +2070,9 @@ describe('ngMock', function() { ]; module(function($controllerProvider) { $controllerProvider.register('testCtrl', function() { - expect(this.data).toBe(data); + expect(this.data).toBeUndefined(); called = true; + return {}; }); }); inject(function($controller, $rootScope) { @@ -2064,64 +2080,12 @@ describe('ngMock', function() { expect(ctrl.data).toBe(data); expect(called).toBe(true); }); - }); - - - it('should support assigning bindings when a value is returned from the constructor', - function() { - var called = false; - var data = [ - { name: 'derp1', id: 0 }, - { name: 'testname', id: 1 }, - { name: 'flurp', id: 2 } - ]; - module(function($controllerProvider) { - $controllerProvider.register('testCtrl', function() { - expect(this.data).toBe(data); - called = true; - return {}; - }); - }); - inject(function($controller, $rootScope) { - var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); - expect(ctrl.data).toBe(data); - expect(called).toBe(true); - }); - } - ); - - - if (/chrome/.test(window.navigator.userAgent)) { - it('should support assigning bindings to class-based controller', function() { - var called = false; - var data = [ - { name: 'derp1', id: 0 }, - { name: 'testname', id: 1 }, - { name: 'flurp', id: 2 } - ]; - module(function($controllerProvider) { - // eslint-disable-next-line no-eval - var TestCtrl = eval('(class { constructor() { called = true; } })'); - $controllerProvider.register('testCtrl', TestCtrl); - }); - inject(function($controller, $rootScope) { - var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); - expect(ctrl.data).toBe(data); - expect(called).toBe(true); - }); - }); } - }); - + ); - describe('with `preAssignBindingsEnabled(false)`', function() { - beforeEach(module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(false); - })); - - - it('should support creating controller with bindings', function() { + if (/chrome/.test(window.navigator.userAgent)) { + it('should support assigning bindings to class-based controller', function() { var called = false; var data = [ { name: 'derp1', id: 0 }, @@ -2129,10 +2093,9 @@ describe('ngMock', function() { { name: 'flurp', id: 2 } ]; module(function($controllerProvider) { - $controllerProvider.register('testCtrl', function() { - expect(this.data).toBeUndefined(); - called = true; - }); + // eslint-disable-next-line no-eval + var TestCtrl = eval('(class { constructor() { called = true; } })'); + $controllerProvider.register('testCtrl', TestCtrl); }); inject(function($controller, $rootScope) { var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); @@ -2140,53 +2103,7 @@ describe('ngMock', function() { expect(called).toBe(true); }); }); - - - it('should support assigning bindings when a value is returned from the constructor', - function() { - var called = false; - var data = [ - { name: 'derp1', id: 0 }, - { name: 'testname', id: 1 }, - { name: 'flurp', id: 2 } - ]; - module(function($controllerProvider) { - $controllerProvider.register('testCtrl', function() { - expect(this.data).toBeUndefined(); - called = true; - return {}; - }); - }); - inject(function($controller, $rootScope) { - var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); - expect(ctrl.data).toBe(data); - expect(called).toBe(true); - }); - } - ); - - - if (/chrome/.test(window.navigator.userAgent)) { - it('should support assigning bindings to class-based controller', function() { - var called = false; - var data = [ - { name: 'derp1', id: 0 }, - { name: 'testname', id: 1 }, - { name: 'flurp', id: 2 } - ]; - module(function($controllerProvider) { - // eslint-disable-next-line no-eval - var TestCtrl = eval('(class { constructor() { called = true; } })'); - $controllerProvider.register('testCtrl', TestCtrl); - }); - inject(function($controller, $rootScope) { - var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); - expect(ctrl.data).toBe(data); - expect(called).toBe(true); - }); - }); - } - }); + } }); From 0c3620bbf5e654a0f1b07953b62f6619c31c06e4 Mon Sep 17 00:00:00 2001 From: Vitaliy Date: Wed, 15 Mar 2017 19:19:06 +0200 Subject: [PATCH 054/719] docs($interpolate): fix typo There was missing `JS` at line 141. Closes #15817 --- src/ng/interpolate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index 342202b6e9ec..4a3998e77f59 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -138,7 +138,7 @@ function $InterpolateProvider() { * ```js * var $interpolate = ...; // injected * var exp = $interpolate('Hello {{name | uppercase}}!'); - * expect(exp({name:'AngularJS'})).toEqual('Hello ANGULAR!'); + * expect(exp({name:'AngularJS'})).toEqual('Hello ANGULARJS!'); * ``` * * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is From 24c94b83fad3aa1e4c555448e3dbff2e0b399e04 Mon Sep 17 00:00:00 2001 From: TheRealMaxion Date: Fri, 17 Mar 2017 12:38:13 +0100 Subject: [PATCH 055/719] docs(tutorial/step_04): fix typo (each --> its) Closes #15826 --- docs/content/tutorial/step_04.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/tutorial/step_04.ngdoc b/docs/content/tutorial/step_04.ngdoc index 2b96e876398b..2865ad765866 100644 --- a/docs/content/tutorial/step_04.ngdoc +++ b/docs/content/tutorial/step_04.ngdoc @@ -41,7 +41,7 @@ maintain. As we add more and more features, our files will get bigger and bigger difficult to navigate and find the code we are looking for. Instead we should put each feature/entity in its own file. Each stand-alone controller will be -defined in its own file, each component will be defined in each own file, etc. +defined in its own file, each component will be defined in its own file, etc. Luckily, we don't need to change anything with respect to that guideline in our code, since we have already defined our `phoneList` component in its own `phone-list.component.js` file. Good job! From bb2a247aa51d750d2348b8a0bccc712ec742749c Mon Sep 17 00:00:00 2001 From: TheRealMaxion Date: Fri, 17 Mar 2017 15:54:51 +0100 Subject: [PATCH 056/719] docs(tutorial/step_09) fix typo Closes #15829 --- docs/content/tutorial/step_09.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/tutorial/step_09.ngdoc b/docs/content/tutorial/step_09.ngdoc index c5162a229558..99683ccfeb44 100644 --- a/docs/content/tutorial/step_09.ngdoc +++ b/docs/content/tutorial/step_09.ngdoc @@ -402,7 +402,7 @@ You can now rerun `npm run protractor` to see the tests run (and hopefully pass)
-* Try to add a `{{$ctrl.phoneId}` binding in the template string for the phone details view: +* Try to add a `{{$ctrl.phoneId}}` binding in the template string for the phone details view: ```js when('/phones/:phoneId', { From eacf06f5cfa69989b5277dd77007c79c96721160 Mon Sep 17 00:00:00 2001 From: xfg Date: Thu, 16 Mar 2017 23:35:00 +0500 Subject: [PATCH 057/719] docs(ngMock/$httpBackend): add `catch()` block to example Make the `it should fail authentication` test pass. Closes #15822 --- src/ngMock/angular-mocks.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 6d3337fe9d98..d6e84c75584f 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1133,6 +1133,8 @@ angular.mock.dump = function(object) { $http.get('/auth.py').then(function(response) { authToken = response.headers('A-Token'); $scope.user = response.data; + }).catch(function() { + $scope.status = 'Failed...'; }); $scope.saveMessage = function(message) { From d94153939cb9e78621feaec8bb315f899e13f6b8 Mon Sep 17 00:00:00 2001 From: BobChao87 Date: Fri, 17 Mar 2017 00:26:42 -0700 Subject: [PATCH 058/719] docs($resource): encode `)` in link JSDoc to HTML converter was treating the close parenthesis in `[MDN](...#toJson()_behavior)` as the final close parenthesis, thus resulting in a broken link. This commit fixes it by percent-encoding the parentesis in the link address. Closes #15825 --- src/ngResource/resource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index 060d66b7f120..b5b8151ecc22 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -280,7 +280,7 @@ function shallowClearAndCopy(src, dst) { * the Resource API. This object can be serialized through {@link angular.toJson} safely * without attaching AngularJS-specific fields. Notice that `JSON.stringify` (and * `angular.toJson`) automatically use this method when serializing a Resource instance - * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior)). + * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior)). * * @example * From 70dbb158469541a6f712ebe03b970a4a4d1abe57 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 20 Mar 2017 16:26:14 +0200 Subject: [PATCH 059/719] docs($sanitize): fix incorrect test description --- test/ngSanitize/sanitizeSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index dd8c2ba97d43..0e3b1f6a0627 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -246,7 +246,7 @@ describe('HTML', function() { .toEqual('

text1text2

'); }); - it('should remove clobbered elements', function() { + it('should throw on clobbered elements', function() { inject(function($sanitize) { expect(function() { $sanitize('
'); From d96e58fdc87572225846c181e45e700ec2b55c40 Mon Sep 17 00:00:00 2001 From: Richard Kaufhold Date: Sun, 21 Jun 2015 11:16:03 +0200 Subject: [PATCH 060/719] feat($resource): add `hasBody` action configuration option By default, only `PUT`, `POST` and `PATCH` requests have a body, but you can use `hasBody` to configure any action to either have or not have a body, regardless of its HTTP method. Fixes #10128 Closes #12181 --- src/ngResource/resource.js | 20 +++++++--- test/ngResource/resourceSpec.js | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index b5b8151ecc22..c1d6ec8a818b 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -125,8 +125,8 @@ function shallowClearAndCopy(src, dst) { * URL `/path/greet?salutation=Hello`. * * If the parameter value is prefixed with `@`, then the value for that parameter will be - * extracted from the corresponding property on the `data` object (provided when calling a - * "non-GET" action method). + * extracted from the corresponding property on the `data` object (provided when calling actions + * with a request body). * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of * `someParam` will be `data.someProp`. * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action @@ -193,6 +193,8 @@ function shallowClearAndCopy(src, dst) { * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - * `response` and `responseError`. Both `response` and `responseError` interceptors get called * with `http response` object. See {@link ng.$http $http interceptors}. + * - **`hasBody`** - `{boolean}` - allows to specify if a request body should be included or not. + * If not specified only POST, PUT and PATCH requests will have a body. * * @param {Object} options Hash with custom settings that should extend the * default `$resourceProvider` behavior. The supported options are: @@ -237,9 +239,15 @@ function shallowClearAndCopy(src, dst) { * The action methods on the class object or instance object can be invoked with the following * parameters: * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * - "class" actions without a body: `Resource.action([parameters], [success], [error])` + * - "class" actions with a body: `Resource.action([parameters], postData, [success], [error])` + * - instance actions: `instance.$action([parameters], [success], [error])` + * + * + * When calling instance methods, the instance itself is used as the request body (if the action + * should have a body). By default, only actions using `POST`, `PUT` or `PATCH` have request + * bodies, but you can use the `hasBody` configuration option to specify whether an action + * should have a body or not (regardless of its HTTP method). * * * Success callback is called with (value (Object|Array), responseHeaders (Function), @@ -643,7 +651,7 @@ angular.module('ngResource', ['ng']). }; forEach(actions, function(action, name) { - var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); + var hasBody = action.hasBody === true || (action.hasBody !== false && /^(POST|PUT|PATCH)$/i.test(action.method)); var numericTimeout = action.timeout; var cancellable = isDefined(action.cancellable) ? action.cancellable : route.defaults.cancellable; diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js index 82ba095e98d6..bc8bbd079642 100644 --- a/test/ngResource/resourceSpec.js +++ b/test/ngResource/resourceSpec.js @@ -97,6 +97,76 @@ describe('basic usage', function() { $httpBackend.flush(); }); + it('should include a request body when calling custom method with hasBody is true', function() { + var instant = {name: 'info.txt'}; + var condition = {at: '2038-01-19 03:14:08'}; + + $httpBackend.expect('CREATE', '/fooresource', instant).respond({fid: 42}); + $httpBackend.expect('DELETE', '/fooresource', condition).respond({}); + + var r = $resource('/fooresource', {}, { + create: {method: 'CREATE', hasBody: true}, + delete: {method: 'DELETE', hasBody: true} + }); + + var creationResponse = r.create(instant); + var deleteResponse = r.delete(condition); + + $httpBackend.flush(); + + expect(creationResponse.fid).toBe(42); + expect(deleteResponse.$resolved).toBe(true); + }); + + it('should not include a request body if hasBody is false on POST, PUT and PATCH', function() { + function verifyRequest(method, url, data) { + expect(data).toBeUndefined(); + return [200, {id: 42}]; + } + + $httpBackend.expect('POST', '/foo').respond(verifyRequest); + $httpBackend.expect('PUT', '/foo').respond(verifyRequest); + $httpBackend.expect('PATCH', '/foo').respond(verifyRequest); + + var R = $resource('/foo', {}, { + post: {method: 'POST', hasBody: false}, + put: {method: 'PUT', hasBody: false}, + patch: {method: 'PATCH', hasBody: false} + }); + + var postResponse = R.post(); + var putResponse = R.put(); + var patchResponse = R.patch(); + + $httpBackend.flush(); + + expect(postResponse.id).toBe(42); + expect(putResponse.id).toBe(42); + expect(patchResponse.id).toBe(42); + }); + + it('should expect a body if hasBody is true', function() { + var username = 'yathos'; + var loginRequest = {name: username, password: 'Smile'}; + var user = {id: 1, name: username}; + + $httpBackend.expect('LOGIN', '/user/me', loginRequest).respond(user); + + $httpBackend.expect('LOGOUT', '/user/me', null).respond(null); + + var UserService = $resource('/user/me', {}, { + login: {method: 'LOGIN', hasBody: true}, + logout: {method: 'LOGOUT', hasBody: false} + }); + + var loginResponse = UserService.login(loginRequest); + var logoutResponse = UserService.logout(); + + $httpBackend.flush(); + + expect(loginResponse.id).toBe(user.id); + expect(logoutResponse.$resolved).toBe(true); + }); it('should build resource', function() { expect(typeof CreditCard).toBe('function'); From 0d9d57d4184eb3105e390d96e2d1aa2230874604 Mon Sep 17 00:00:00 2001 From: Raphael Jamet Date: Mon, 20 Feb 2017 16:59:05 +0100 Subject: [PATCH 061/719] docs($sce): overhaul the `$sce` service documentation A big docs update around `$sce`: There is a lot of content in there that is often misunderstood, and some of the documentation starts to get really old too. Also fixed capitalization, formatting, indentation and uniformized `@param` descriptions. Closes #15735 --- src/ng/sce.js | 432 +++++++++++++++++++++++++++++--------------------- 1 file changed, 251 insertions(+), 181 deletions(-) diff --git a/src/ng/sce.js b/src/ng/sce.js index 03c382581aab..d47d6a0d335d 100644 --- a/src/ng/sce.js +++ b/src/ng/sce.js @@ -16,12 +16,21 @@ var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { + // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding). HTML: 'html', + + // Style statements or stylesheets. Currently unused in AngularJS. CSS: 'css', + + // An URL used in a context where it does not refer to a resource that loads code. Currently + // unused in AngularJS. URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) + + // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as + // code. (e.g. ng-include, script src binding, templateUrl) RESOURCE_URL: 'resourceUrl', + + // Script. Currently unused in AngularJS. JS: 'js' }; @@ -83,6 +92,16 @@ function adjustMatchers(matchers) { * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularJS. * + * For an overview of this service and the functionnality it provides in AngularJS, see the main + * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how + * SCE works in their application, which shouldn't be needed in most cases. + * + *
+ * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or + * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, + * changes to this service will also influence users, so be extra careful and document your changes. + *
+ * * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to @@ -108,10 +127,14 @@ function adjustMatchers(matchers) { * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing AngularJS templates are safe. Refer {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. + * + * The `$sceDelegateProvider` allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing AngularJS templates and other script-running URLs are safe (all + * places that use the `$sce.RESOURCE_URL` context). See + * {@link ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} + * and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}, * * For the general details about this service in AngularJS, read the main page for {@link ng.$sce * Strict Contextual Escaping (SCE)}. @@ -140,6 +163,13 @@ function adjustMatchers(matchers) { * ]); * }); * ``` + * Note that an empty whitelist will block every resource URL from being loaded, and will require + * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates + * requested by {@link ng.$templateRequest $templateRequest} that are present in + * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism + * to populate your templates in that cache at config time, then it is a good idea to remove 'self' + * from that whitelist. This helps to mitigate the security impact of certain types of issues, like + * for instance attacker-controlled `ng-includes`. */ function $SceDelegateProvider() { @@ -155,23 +185,23 @@ function $SceDelegateProvider() { * @kind function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. * - *
- * **Note:** an empty whitelist array will block all URLs! - *
+ * @return {Array} The currently set whitelist array. * - * @return {Array} the currently set whitelist array. + * @description + * Sets/Gets the whitelist of trusted resource URLs. * * The **default value** when no whitelist has been explicitly set is `['self']` allowing only * same origin resource requests. * - * @description - * Sets/Gets the whitelist of trusted resource URLs. + *
+ * **Note:** the default whitelist of 'self' is not recommended if your app shares its origin + * with other apps! It is a good idea to limit it to only your application's directory. + *
*/ this.resourceUrlWhitelist = function(value) { if (arguments.length) { @@ -186,25 +216,23 @@ function $SceDelegateProvider() { * @kind function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored.

+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array.

+ * The typical usage for the blacklist is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. + *

+ * Finally, **the blacklist overrides the whitelist** and has the final say. * - * The typical usage for the blacklist is to **block - * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as - * these would otherwise be trusted but actually return content from the redirected domain. + * @return {Array} The currently set blacklist array. * - * Finally, **the blacklist overrides the whitelist** and has the final say. - * - * @return {Array} the currently set blacklist array. + * @description + * Sets/Gets the blacklist of trusted resource URLs. * * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there * is no blacklist.) - * - * @description - * Sets/Gets the blacklist of trusted resource URLs. */ this.resourceUrlBlacklist = function(value) { @@ -288,17 +316,24 @@ function $SceDelegateProvider() { * @name $sceDelegate#trustAs * * @description - * Returns an object that is trusted by AngularJS for use in specified strict - * contextual escaping contexts (such as ng-bind-html, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. + * Returns a trusted representation of the parameter for the specified context. This trusted + * object will later on be used as-is, without any security check, by bindings or directives + * that require this security context. + * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass + * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as + * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the + * sanitizer loaded, passing the value itself will render all the HTML that does not pose a + * security risk. * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where AngularJS expects a $sce.trustAs() return value. + * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those + * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual + * escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that should be considered trusted. + * @return {*} A trusted representation of value, that can be used in the given context. */ function trustAs(type, trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); @@ -330,11 +365,11 @@ function $SceDelegateProvider() { * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is. * * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * call or anything else. + * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ @@ -351,33 +386,38 @@ function $SceDelegateProvider() { * @name $sceDelegate#getTrusted * * @description - * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. + * Takes any input, and either returns a value that's safe to use in the specified context, or + * throws an exception. * - *

- * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting - * (XSS) vulnerability in your application. - *
+ * In practice, there are several cases. When given a string, this function runs checks + * and sanitization to make it safe without prior assumptions. When given the result of a {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied + * value if that value's context is valid for this call's context. Finally, this function can + * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization + * is available or possible.) * - * @param {string} type The kind of context in which this value is to be used. + * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ function getTrusted(type, maybeTrusted) { if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return + // as-is. if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. + // Otherwise, if we get here, then we may either make it safe, or throw an exception. This + // depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL), + // some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS + // has no corresponding sinks. if (type === SCE_CONTEXTS.RESOURCE_URL) { + // RESOURCE_URL uses a whitelist. if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { @@ -386,8 +426,10 @@ function $SceDelegateProvider() { maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { + // htmlSanitizer throws its own error when no sanitizer is available. return htmlSanitizer(maybeTrusted); } + // Default error when the $sce service has no way to make the input safe. throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } @@ -423,21 +465,27 @@ function $SceDelegateProvider() { * * # Strict Contextual Escaping * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. + * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render + * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and + * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. * - * As of version 1.2, AngularJS ships with SCE enabled by default. + * ## Overview + * + * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in + * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically + * run security checks on them (sanitizations, whitelists, depending on context), or throw when it + * cannot guarantee the security of the result. That behavior depends strongly on contexts: HTML + * can be sanitized, but template URLs cannot, for instance. + * + * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML: + * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it + * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and + * render the input as-is, you will need to mark it as trusted for that context before attempting + * to bind it. * - * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow - * one to execute arbitrary javascript by the use of the expression() syntax. Refer - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. + * As of version 1.2, AngularJS ships with SCE enabled by default. * - * SCE assists in writing code in a way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * ## In practice * * Here's an example of a binding in a privileged context: * @@ -447,10 +495,10 @@ function $SceDelegateProvider() { * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) + * disabled, this application allows the user to render arbitrary HTML into the DIV, which would + * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog + * articles, etc. via bindings. (HTML is just one example of a context where rendering user + * controlled input creates security vulnerabilities.) * * For the case of HTML, you might use a library, either on the client side, or on the server side, * to sanitize unsafe HTML before binding to the value and rendering it in the document. @@ -460,25 +508,29 @@ function $SceDelegateProvider() { * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some * properties/fields and forgot to update the binding to the sanitized value? * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * To be secure by default, AngularJS makes sure bindings go through that sanitization, or + * any similar validation process, unless there's a good reason to trust the given value in this + * context. That trust is formalized with a function call. This means that as a developer, you + * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues, + * you just need to ensure the values you mark as trusted indeed are safe - because they were + * received from your server, sanitized by your library, etc. You can organize your codebase to + * help with this - perhaps allowing only the files in a specific directory to do this. + * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then + * becomes a more manageable task. * * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. - * + * build the trusted versions of your values. * * ## How does it work? * * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted - * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the - * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as + * a way to enforce the required security context in your data sink. Directives use {@link + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs + * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also, + * when binding without directives, AngularJS will understand the context of your bindings + * automatically. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly @@ -519,11 +571,12 @@ function $SceDelegateProvider() { * It's important to remember that SCE only applies to interpolation expressions. * * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. - * `
`) just works. - * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * call `$sce.trustAs` on them (e.g. + * `
`) just works. The `$sceDelegate` will + * also use the `$sanitize` service if it is available when binding untrusted values to + * `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you + * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in + * your application. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. @@ -541,11 +594,17 @@ function $SceDelegateProvider() { * * | Context | Notes | * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | - * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`

Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. | + * + * + * Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings + * in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This + * might evolve. * * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
* @@ -664,14 +723,15 @@ function $SceDelegateProvider() { * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if + * you are writing a library, you will cause security bugs applications using it. * * That said, here's how you can completely disable SCE: * * ``` * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. + * // Do not use in new projects or libraries. * $sceProvider.enabled(false); * }); * ``` @@ -686,8 +746,8 @@ function $SceProvider() { * @name $sceProvider#enabled * @kind function * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. + * @param {boolean=} value If provided, then enables/disables SCE application-wide. + * @return {boolean} True if SCE is enabled, false otherwise. * * @description * Enables/disables SCE and returns the current value. @@ -741,9 +801,9 @@ function $SceProvider() { * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) * will also succeed. * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. + * Inheritance happens to capture this in a natural way. In some future, we may not use + * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to + * be aware of this detail. */ this.$get = ['$parse', '$sceDelegate', function( @@ -765,8 +825,8 @@ function $SceProvider() { * @name $sce#isEnabled * @kind function * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. * * @description * Returns a boolean indicating if SCE is enabled. @@ -793,14 +853,14 @@ function $SceProvider() { * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, * *result*)} * - * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} type The SCE context in which this result will be used. * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); @@ -818,18 +878,18 @@ function $SceProvider() { * @name $sce#trustAs * * @description - * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, - * returns an object that is trusted by AngularJS for use in specified strict contextual - * escaping contexts (such as ng-bind-html, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a + * wrapped object that represents your value, and the trust you have in its safety for the given + * context. AngularJS can then use that value as-is in bindings of the specified secure context. + * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute + * interpolations. See {@link ng.$sce $sce} for strict contextual escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where AngularJS expects a $sce.trustAs() return value. + * @param {*} value The value that that should be considered trusted. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in the context you specified. */ /** @@ -840,11 +900,23 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsHtml(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.HTML` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.HTML` context (like `ng-bind-html`). + */ + + /** + * @ngdoc method + * @name $sce#trustAsCss + * + * @description + * Shorthand method. `$sce.trustAsCss(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.CSS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant + * of your `value` in `$sce.CSS` context. This context is currently unused, so there are + * almost no reasons to use this function so far. */ /** @@ -855,11 +927,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.URL` context. That context is currently unused, so there are almost no reasons + * to use this function so far. */ /** @@ -870,11 +941,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsResourceUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute + * bindings, ...) */ /** @@ -885,11 +955,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsJs(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.JS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to + * use this function so far. */ /** @@ -898,16 +967,17 @@ function $SceProvider() { * * @description * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. + * takes any input, and either returns a value that's safe to use in the specified context, + * or throws an exception. This function is aware of trusted values created by the `trustAs` + * function and its shorthands, and when contexts are appropriate, returns the unwrapped value + * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a + * safe value (e.g., no sanitization is available or possible.) * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. + * @param {string} type The context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs + * `$sce.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ /** @@ -919,7 +989,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)` */ /** @@ -931,7 +1001,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)` */ /** @@ -943,7 +1013,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.URL, value)` */ /** @@ -955,7 +1025,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` */ /** @@ -967,7 +1037,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.JS, value)` */ /** @@ -979,12 +1049,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -996,12 +1066,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -1013,12 +1083,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -1030,12 +1100,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -1047,12 +1117,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ // Shorthand delegations. From ba2e0d3d9b40c5023b85530b1fe401e3fea53278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 15 Mar 2017 16:47:02 +0100 Subject: [PATCH 062/719] docs(faq): document the AngularJS/jqLite deprecation strategy Fixes #15282 --- docs/content/misc/faq.ngdoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/content/misc/faq.ngdoc b/docs/content/misc/faq.ngdoc index ece83a335028..a05a2edc0b97 100644 --- a/docs/content/misc/faq.ngdoc +++ b/docs/content/misc/faq.ngdoc @@ -55,6 +55,16 @@ extracted from the commits. We can quickly see in the new changelog exactly what changes and so can application developers when they are deciding whether to update to a new version of AngularJS. +#### When are deprecated features removed from the library? +Most of the time we remove a deprecated feature in a next minor version bump. For example, the +`preAssignBindingsEnabled` `$compileProvider` method was defined in AngularJS `1.5.10`, deprecated in `1.6` and +will be removed in `1.7`. + +In case of jqLite we apply a different strategy - we deprecate features that have an equivalent in jQuery that +is also deprecated but we only remove the feature once it's removed from jQuery to improve compatibility between +jqLite and jQuery. One such example is the `bind` method, deprecated in favor of `on` but unlikely to be removed +from jqLite any time soon. + ### Is AngularJS a templating system? From bf7685abbd9a7056342add69b259fed3c7816ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 20 Aug 2014 20:25:50 +0200 Subject: [PATCH 063/719] test(jqLite): test not firing $destroy on jqLite.cleanData with jQuery UI So far it wasn't tested that Angular's logic for skipping it triggering the $destroy event on jQuery.cleanData in the replaceWith internal function works correctly when Angular is not the last one to patch the cleanData method (e.g. if jQuery UI does the patching later). This commits adds the relevant test. Ref #8486 --- test/ng/compileSpec.js | 114 ++++++++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 8422fc767139..d99d9e9c1826 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2080,17 +2080,49 @@ describe('$compile', function() { )); - it('should work when directive is in a repeater', inject( - function($compile, $httpBackend, $rootScope) { - $httpBackend.expect('GET', 'hello.html'). + describe('when directive is in a repeater', function() { + var is; + beforeEach(function() { + is = [1, 2]; + }); + + function runTest() { + inject(function($compile, $httpBackend, $rootScope) { + $httpBackend.expect('GET', 'hello.html'). respond('i=;'); - element = jqLite('
{{i}}
'); - $compile(element)($rootScope); + element = jqLite('
{{i}}
'); + $compile(element)($rootScope); - $httpBackend.flush(); - expect(element.text()).toEqual('i=1;i=2;'); + $httpBackend.flush(); + expect(element.text()).toEqual('i=' + is.join(';i=') + ';'); + }); } - )); + + it('should work in jqLite and jQuery with jQuery.cleanData last patched by Angular', runTest); + + if (jQuery) { + it('should work with another library patching jQuery.cleanData after Angular', function() { + var cleanedCount = 0; + var currentCleanData = jqLite.cleanData; + jqLite.cleanData = function(elems) { + cleanedCount += elems.length; + // Don't return the output and explicitly pass only the first parameter + // so that we're sure we're not relying on either of them. jQuery UI patch + // behaves in this way. + currentCleanData(elems); + }; + + runTest(); + + // The initial ng-repeat div is dumped after parsing hence we expect cleanData + // count to be one larger than size of the iterated array. + expect(cleanedCount).toBe(is.length + 1); + + // Restore the previous cleanData. + jqLite.cleanData = currentCleanData; + }); + } + }); describe('replace and not exactly one root element', function() { @@ -8573,46 +8605,46 @@ describe('$compile', function() { }); }); - if (jQuery) { - describe('cleaning up after a replaced element', function() { - var $compile, xs; - beforeEach(inject(function(_$compile_) { - $compile = _$compile_; - xs = [0, 1]; - })); + describe('cleaning up after a replaced element', function() { + var $compile, xs; + beforeEach(inject(function(_$compile_) { + $compile = _$compile_; + xs = [0, 1]; + })); - function testCleanup() { - var privateData, firstRepeatedElem; + function testCleanup() { + var privateData, firstRepeatedElem; - element = $compile('
{{x}}
')($rootScope); + element = $compile('
{{x}}
')($rootScope); - $rootScope.$apply('xs = [' + xs + ']'); - firstRepeatedElem = element.children('.ng-scope').eq(0); + $rootScope.$apply('xs = [' + xs + ']'); + firstRepeatedElem = element.children('.ng-scope').eq(0); - expect(firstRepeatedElem.data('$scope')).toBeDefined(); - privateData = jQuery._data(firstRepeatedElem[0]); - expect(privateData.events).toBeDefined(); - expect(privateData.events.click).toBeDefined(); - expect(privateData.events.click[0]).toBeDefined(); + expect(firstRepeatedElem.data('$scope')).toBeDefined(); + privateData = jqLite._data(firstRepeatedElem[0]); + expect(privateData.events).toBeDefined(); + expect(privateData.events.click).toBeDefined(); + expect(privateData.events.click[0]).toBeDefined(); - //Ensure the AngularJS $destroy event is still sent - var destroyCount = 0; - element.find('div').on('$destroy', function() { destroyCount++; }); + // Ensure the AngularJS $destroy event is still sent + var destroyCount = 0; + element.find('div').on('$destroy', function() { destroyCount++; }); - $rootScope.$apply('xs = null'); + $rootScope.$apply('xs = null'); - expect(destroyCount).toBe(2); - expect(firstRepeatedElem.data('$scope')).not.toBeDefined(); - privateData = jQuery._data(firstRepeatedElem[0]); - expect(privateData && privateData.events).not.toBeDefined(); - } + expect(destroyCount).toBe(2); + expect(firstRepeatedElem.data('$scope')).not.toBeDefined(); + privateData = jqLite._data(firstRepeatedElem[0]); + expect(privateData && privateData.events).not.toBeDefined(); + } - it('should work without external libraries (except jQuery)', testCleanup); + it('should work without external libraries (except jQuery)', testCleanup); + if (jQuery) { it('should work with another library patching jQuery.cleanData after AngularJS', function() { var cleanedCount = 0; - var currentCleanData = jQuery.cleanData; - jQuery.cleanData = function(elems) { + var currentCleanData = jqLite.cleanData; + jqLite.cleanData = function(elems) { cleanedCount += elems.length; // Don't return the output and explicitly pass only the first parameter // so that we're sure we're not relying on either of them. jQuery UI patch @@ -8626,11 +8658,11 @@ describe('$compile', function() { // and each clone of the ng-repeat template is also removed (xs.length) expect(cleanedCount).toBe(xs.length + 1); - // Restore the previous jQuery.cleanData. - jQuery.cleanData = currentCleanData; + // Restore the previous cleanData. + jqLite.cleanData = currentCleanData; }); - }); - } + } + }); it('should add a $$transcluded property onto the transcluded scope', function() { From 5377baf0fccaec52d01481b67c495aa2bd21187f Mon Sep 17 00:00:00 2001 From: Joshua J Wilborn Date: Wed, 22 Mar 2017 06:51:55 -0400 Subject: [PATCH 064/719] docs ($compile): add error documentation for noslot error in $compile Fixes #15790 Closes #15828 --- docs/content/error/$compile/noslot.ngdoc | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/content/error/$compile/noslot.ngdoc diff --git a/docs/content/error/$compile/noslot.ngdoc b/docs/content/error/$compile/noslot.ngdoc new file mode 100644 index 000000000000..a882ddde0e7a --- /dev/null +++ b/docs/content/error/$compile/noslot.ngdoc @@ -0,0 +1,38 @@ +@ngdoc error +@name $compile:noslot +@fullName No matching slot in parent directive +@description + +This error occurs when declaring a specific slot in a {@link ng.ngTransclude `ngTransclude`} +which does not map to a specific slot defined in the transclude property of the directive. + +In this example the template has declared a slot missing from the transclude definition. +This example will generate a noslot error. +```js +var componentConfig = { + template: '
' + + '
' + + '
' + + '
', + transclude: { + // The key value pairs here are considered "slots" that are provided for components to slot into. + slotProvided: 'slottedComponent', // mandatory transclusion + // There is no slot provided here for the transclude 'noSlotProvided' declared in the above template. + } +}; +``` + +If we make the following change we will no longer get the noslot error. +```js +var componentConfig = { + template: '
' + + '
' + + '
' + + '
', + transclude: { + slotProvided: 'slottedComponent', + noSlotProvided: 'otherComponent' // now it is declared and the error should cease + } +}; + +``` From f132ce740a869ac68d76e88ab1b2d50a829ed52e Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Wed, 22 Mar 2017 03:54:55 -0700 Subject: [PATCH 065/719] refactor($controller): remove unused injected $window Closes #15839 --- src/ng/controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/controller.js b/src/ng/controller.js index 1d707b243890..3b8d6196449b 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -54,7 +54,7 @@ function $ControllerProvider() { } }; - this.$get = ['$injector', '$window', function($injector, $window) { + this.$get = ['$injector', function($injector) { /** * @ngdoc service From 421040588c04a01319a97b2c7289abd5bfbd9607 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 15 Mar 2017 17:40:05 +0100 Subject: [PATCH 066/719] docs(component-router): give deprecation notice red color --- docs/content/guide/component-router.ngdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/guide/component-router.ngdoc b/docs/content/guide/component-router.ngdoc index 06a16cf4a57f..f0bac32b869e 100644 --- a/docs/content/guide/component-router.ngdoc +++ b/docs/content/guide/component-router.ngdoc @@ -5,7 +5,7 @@ # Component Router -
+
**Deprecation Notice:** In an effort to keep synchronized with router changes in the new Angular, this implementation of the Component Router (ngComponentRouter module) has been deprecated and will not receive further updates. We are investigating backporting the new Angular Router to AngularJS, but alternatively, use the {@link ngRoute} module or community developed projects (e.g. [ui-router](https://github.com/angular-ui/ui-router)).
@@ -13,7 +13,7 @@ We are investigating backporting the new Angular Router to AngularJS, but altern This guide describes the Component Router for AngularJS.
- If you are looking for information about the old router for AngularJS have a look at the {@link ngRoute} module. + If you are looking for information about the default router for AngularJS have a look at the {@link ngRoute} module. If you are looking for information about the Component Router for the new Angular then check out the [Angular Router Guide](https://angular.io/docs/ts/latest/guide/router.html). From 233f47b98a64eda7aaa7083581c9b64b50c6efea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 22 Mar 2017 12:08:05 +0100 Subject: [PATCH 067/719] chore($parse): make sure no one changes .toEqual(-0) to .toBe(-0) --- test/ng/parseSpec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 8c515a1c515f..e524eaabd23c 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1867,6 +1867,8 @@ describe('parser', function() { expect(scope.$eval('+\'1\'')).toEqual(+'1'); expect(scope.$eval('-\'1\'')).toEqual(-'1'); expect(scope.$eval('+undefined')).toEqual(0); + + // Note: don't change toEqual to toBe as toBe collapses 0 & -0. expect(scope.$eval('-undefined')).toEqual(-0); expect(scope.$eval('+null')).toEqual(+null); expect(scope.$eval('-null')).toEqual(-null); From 60644d5d0533dc85fca6f1d47f1a542922216f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 22 Mar 2017 16:45:55 +0100 Subject: [PATCH 068/719] chore(*): remove unused docs/bower_components mentions --- .eslintignore | 1 - .travis.yml | 3 +-- Gruntfile.js | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.eslintignore b/.eslintignore index 315b45467795..dc7259927e1c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ bower_components/** build/** -docs/bower_components/** docs/app/assets/js/angular-bootstrap/** docs/config/templates/** node_modules/** diff --git a/.travis.yml b/.travis.yml index e51e815aa7c4..0a2d6945cdb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ cache: directories: - node_modules - bower_components - - docs/bower_components branches: except: @@ -41,7 +40,7 @@ before_install: - export PATH=$HOME/.yarn/bin:$PATH before_script: - - du -sh ./node_modules ./bower_components/ ./docs/bower_components/ || true + - du -sh ./node_modules ./bower_components/ || true - ./scripts/travis/before_build.sh script: diff --git a/Gruntfile.js b/Gruntfile.js index fb49afeb2667..031612615690 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -172,7 +172,6 @@ module.exports = function(grunt) { 'test/**/*.js', 'i18n/**/*.js', '!docs/app/assets/js/angular-bootstrap/**', - '!docs/bower_components/**', '!docs/config/templates/**', '!src/angular.bind.js', '!i18n/closure/**', From c35786ae2634fc82a79dc42f58e138a9d33cf67e Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 22 Mar 2017 17:03:44 +0100 Subject: [PATCH 069/719] chore(github): update issue template Closes #15824 --- .github/ISSUE_TEMPLATE.md | 63 ++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0f73c4f67efd..82e6b016d1cb 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,27 +1,36 @@ -***Note*: for support questions, please use one of these channels: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#question. This repository's issues are reserved for feature requests and bug reports.** - -**Do you want to request a *feature* or report a *bug*?** - - - -**What is the current behavior?** - - - -**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (template: http://plnkr.co/edit/tpl:yBpEi4).** - - - -**What is the expected behavior?** - - - -**What is the motivation / use case for changing the behavior?** - - - -**Which versions of AngularJS, and which browser / OS are affected by this issue? Did this work in previous versions of AngularJS? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.** - - - -**Other information (e.g. stacktraces, related issues, suggestions how to fix)** + + + + +**I'm submitting a ...** + +- [ ] bug report +- [ ] feature request +- [ ] other (Please do not submit support requests here (see above)) + +**Current behavior:** + + +**Expected / new behavior:** + + +**Minimal reproduction of the problem with instructions:** + + +**Angular version:** 1.x.y + + +**Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] + + +**Anything else:** + \ No newline at end of file From 3d0abffbc04479fb1c5b704925807ab1bd4d345f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski?= Date: Wed, 22 Mar 2017 22:49:51 +0100 Subject: [PATCH 070/719] chore(yarn): rely on Travis built-in Yarn support, update Yarn in Jenkins On Travis we now rely on built-in Yarn support and we only cache the Yarn cache, not node_modules. This creates a more stable environment as we don't install over previous node_modules state but we still won't download packages from the internet in the second run for the same yarn.lock as Yarn takes packages from its local cache if they exist there. We install a new Yarn verison manually on Jenkins; the location of the install script changed. Closes #15851 --- .travis.yml | 6 +----- package.json | 2 +- scripts/jenkins/init-node.sh | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a2d6945cdb1..478de09a5484 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ node_js: - '6' cache: + yarn: true directories: - - node_modules - bower_components branches: @@ -35,10 +35,6 @@ addons: packages: - g++-4.8 -before_install: - - curl -o- -L https://raw.githubusercontent.com/yarnpkg/yarn/2a0afc73210c7a82082585283e518eeb88ca19ae/scripts/install-latest.sh | bash -s -- --version 0.17.9 - - export PATH=$HOME/.yarn/bin:$PATH - before_script: - du -sh ./node_modules ./bower_components/ || true - ./scripts/travis/before_build.sh diff --git a/package.json b/package.json index 64926608fa0d..10c601f133a9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "engines": { "node": "^6.9.1", - "yarn": ">=0.17.9", + "yarn": ">=0.21.3", "grunt": "^1.2.0" }, "scripts": { diff --git a/scripts/jenkins/init-node.sh b/scripts/jenkins/init-node.sh index 445924335de8..dcb97e659677 100755 --- a/scripts/jenkins/init-node.sh +++ b/scripts/jenkins/init-node.sh @@ -8,7 +8,7 @@ nvm install # clean out and install yarn rm -rf ~/.yarn -curl -o- -L https://raw.githubusercontent.com/yarnpkg/yarn/2a0afc73210c7a82082585283e518eeb88ca19ae/scripts/install-latest.sh | bash -s -- --version 0.17.9 +curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.21.3 export PATH="$HOME/.yarn/bin:$PATH" # Ensure that we have the local dependencies installed From b9d2b30808db367af1c31fa0fa6cef8f1bc086d3 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 17 Mar 2017 08:46:18 -0300 Subject: [PATCH 071/719] docs(filterFilter): clarify the comparator parameter Closes #15827 --- src/ng/filter/filter.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ng/filter/filter.js b/src/ng/filter/filter.js index 37cdb0d6ca3b..a11a0ce4d25d 100644 --- a/src/ng/filter/filter.js +++ b/src/ng/filter/filter.js @@ -45,8 +45,9 @@ * The final result is an array of those elements that the predicate returned true for. * * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in - * determining if the expected value (from the filter expression) and actual value (from - * the object in the array) should be considered a match. + * determining if values retrieved using `expression` (when it is not a function) should be + * considered a match based on the the expected value (from the filter expression) and actual + * value (from the object in the array). * * Can be one of: * From 9de0842719630b8e42fc9874717bbd41aee69309 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 22 Mar 2017 11:09:40 +0100 Subject: [PATCH 072/719] chore(doc-gen): report on missing or obsolete error docs Closes #12527 --- docs/config/processors/error-docs.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/config/processors/error-docs.js b/docs/config/processors/error-docs.js index c80297efbba5..ebcd18658939 100644 --- a/docs/config/processors/error-docs.js +++ b/docs/config/processors/error-docs.js @@ -5,18 +5,36 @@ * @description * Process "error" docType docs and generate errorNamespace docs */ -module.exports = function errorDocsProcessor(errorNamespaceMap, getMinerrInfo) { +module.exports = function errorDocsProcessor(log, errorNamespaceMap, getMinerrInfo) { return { $runAfter: ['tags-extracted'], $runBefore: ['extra-docs-added'], $process: function(docs) { + // Get the extracted min errors to compare with the error docs, and report any mismatch + var collectedErrors = require('../../../build/errors.json').errors; + var flatErrors = []; + + for (var namespace in collectedErrors) { + for (var error in collectedErrors[namespace]) { + flatErrors.push(namespace + ':' + error); + } + } + // Create error namespace docs and attach error docs to each docs.forEach(function(doc) { var parts, namespaceDoc; if (doc.docType === 'error') { + var matchingMinErr = flatErrors.indexOf(doc.name); + + if (matchingMinErr === -1) { + log.warn('Error doc: ' + doc.name + ' has no matching min error'); + } else { + flatErrors.splice(matchingMinErr, 1); + } + // Parse out the error info from the id parts = doc.name.split(':'); doc.namespace = parts[0]; @@ -41,6 +59,10 @@ module.exports = function errorDocsProcessor(errorNamespaceMap, getMinerrInfo) { } }); + flatErrors.forEach(function(value) { + log.warn('No error doc exists for min error: ' + value); + }); + errorNamespaceMap.forEach(function(errorNamespace) { docs.push(errorNamespace); }); From 32f38a33b68144e32021fecad44c788f8bab5e33 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 22 Mar 2017 17:11:53 +0100 Subject: [PATCH 073/719] docs($animate): remove obsolete error doc --- docs/content/error/$animate/nocb.ngdoc | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 docs/content/error/$animate/nocb.ngdoc diff --git a/docs/content/error/$animate/nocb.ngdoc b/docs/content/error/$animate/nocb.ngdoc deleted file mode 100644 index e0e2e95e0304..000000000000 --- a/docs/content/error/$animate/nocb.ngdoc +++ /dev/null @@ -1,12 +0,0 @@ -@ngdoc error -@name $animate:nocb -@fullName Do not pass a callback to animate methods -@description - -Since AngularJS 1.3, the methods of {@link ng.$animate} do not accept a callback as the last parameter. -Instead, they return a promise to which you can attach `then` handlers to be run when the animation completes. - -If you are getting this error then you need to update your code to use the promise-based API. - -See https://github.com/angular/angular.js/commit/bf0f5502b1bbfddc5cdd2f138efd9188b8c652a9 for information about -the change to the animation API and the changes you need to make. From 80077f35d042e04b6da19a6cb91b659334b8246f Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 24 Mar 2017 12:45:28 +0100 Subject: [PATCH 074/719] docs(faq): clarify the versioning strategy - When do breaking changes appear - Relationship with Semver - Compatibility of modules Closes #15845 --- docs/content/misc/faq.ngdoc | 88 +++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/docs/content/misc/faq.ngdoc b/docs/content/misc/faq.ngdoc index a05a2edc0b97..cb3838f6e219 100644 --- a/docs/content/misc/faq.ngdoc +++ b/docs/content/misc/faq.ngdoc @@ -25,37 +25,30 @@ So it's definitely not a plugin or some other native browser extension. In AngularJS we do not allow intentional breaking changes to appear in versions where only the "patch" number changes. For example between 1.3.12 and 1.3.13 there can be no breaking changes. We do allow breaking changes happen between "minor" number changes. For example between 1.3.15 and 1.4.0 there -will be a number of breaking changes. We also allow breaking changes between beta releases of AngularJS. +are a number of breaking changes. That means AngularJS does not follow +[semantic versioning (semver)](http://semver.org/) where breaking changes are only +allowed when the "major" version changes. + +We also allow breaking changes between beta releases of AngularJS. For example between 1.4.0-beta.4 and 1.4.0-beta.5 there may be breaking changes. We try hard to minimize these kinds of change only to those where there is a strong use case such as a strongly requested feature -improvement, a considerable simplification of the code or a measurable performance improvement. - -When adding new code to branches of AngularJS, have a very stringent commit policy: - -- Every commit must contain tests and documentation updates alongside the code changes and that all the -tests must pass; -- Commit messages must be written in a specific manner that allows us to parse them and extract the changes -for release notes. - -The AngularJS code base has a very large set of unit tests (over 4000) and end to end tests, which are pretty -comprehensive. This means that a breaking change will require one or more tests to be changed to allow the -tests to pass. So when a commit includes tests that are being removed or modified, this is a flag that the -code might include a breaking change. When reviewing the commit we can then decide whether there really is -a breaking change and if it is appropriate for the branch to which it is being merged. If so, then we -require that the commit message contains an appropriate breaking change message. +improvement, a considerable simplification of the code, a measurable performance improvement, or a better +developer experience (especially with regard to upgrading to Angular). -Additionally, when a commit lands in our master repository it is synced to Google where we test it against -over 2000 applications using the test suites of these applications. This allows us to catch regressions -quickly before a release. We've had a pretty good experience with this setup. Only bugs that affect features -not used at Google or without sufficient test coverage, have a chance of making it through. - -Lastly, when we are making a release we generate updates to the changelog directly from the commits. This +When we are making a release we generate updates to the changelog directly from the commits. This generated update contains a highlighted section that contains all the breaking changes that have been extracted from the commits. We can quickly see in the new changelog exactly what commits contain breaking changes and so can application developers when they are deciding whether to update to a new version of AngularJS. +Features with non-breaking changes can also appear in the "patch" version, e.g. in version 1.6.3 there might +be a feature that is not available in 1.6.2. + +Finally, deprecation of features might also appear in "minor" version updates. That means the features +will still work in this version, but sometimes must be activated specifically. + #### When are deprecated features removed from the library? + Most of the time we remove a deprecated feature in a next minor version bump. For example, the `preAssignBindingsEnabled` `$compileProvider` method was defined in AngularJS `1.5.10`, deprecated in `1.6` and will be removed in `1.7`. @@ -65,6 +58,53 @@ is also deprecated but we only remove the feature once it's removed from jQuery jqLite and jQuery. One such example is the `bind` method, deprecated in favor of `on` but unlikely to be removed from jqLite any time soon. +#### What is the version compatibility between AngularJS main and optional modules? + +AngularJS code is separated into a main module ("angular"), and a few different optional modules +("angular-animate", "angular-route" etc) that are dependant on the main module. +When a new AngularJS version is released, all modules are updated to the new version. +This means that the main module and the optional modules must always have the exact same version, +down to the patch number, otherwise your application might break. + +Therefore you must always explicitly lock down your dependencies, for example in the package.json, +the following means that "angular" and "angular-animate" are always updated to the same version: + +``` +{ + "angular": "~1.6.0", + "angular-animate": "~1.6.0" +} +``` + +If you define exact versions, make sure core and optional modules are the same: + +``` +{ + "angular": "1.6.3", + "angular-animate": "1.6.3" +} +``` + + +#### How does AngularJS ensure code quality and guard against regressions? + +When adding new code to AngularJS, we have a very stringent commit policy: + +- Every commit must pass all existing tests, contain tests for code changes, and update the documentation +- Commit messages must be written in a specific manner that allows us to parse them and extract the changes +for release notes ([see the contributing guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md)) + +The AngularJS code base has a very large set of unit tests and end-to-end tests. This means that a breaking change will require one or more tests to be changed to allow the +tests to pass. So when a commit includes tests that are being removed or modified, this is a flag that the +code might include a breaking change. When reviewing the commit we can then decide whether there really is +a breaking change and if it is appropriate for the branch to which it is being merged. If so, then we +require that the commit message contains an appropriate breaking change message. + +Additionally, commits are periodically synced to Google where we test it against applications using +the test suites of these applications. This allows us to catch regressions +quickly before a release. We've had a pretty good experience with this setup. Only bugs that affect features +not used at Google or without sufficient test coverage, have a chance of making it through. + ### Is AngularJS a templating system? @@ -99,8 +139,8 @@ Yes. See instructions in {@link downloading}. ### What browsers does AngularJS work with? We run our extensive test suite against the following browsers: the latest versions of Chrome, -Firefox, Safari, and Safari for iOs, as well as Internet Explorer versions 9-11. See {@link guide/ie -Internet Explorer Compatibility} for more details on supporting legacy IE browsers. +Firefox, Safari, and Safari for iOS, as well as Internet Explorer versions 9-11. See +{@link guide/ie Internet Explorer Compatibility} for more details on supporting legacy IE browsers. If a browser is untested, it doesn't mean it won't work; for example, older Android (2.3.x) is supported in the sense that we avoid the dot notation for reserved words as property names, From f3aa1d183a73e4eeeb105d861bbf2f985449ed94 Mon Sep 17 00:00:00 2001 From: Peter Mertz Date: Fri, 24 Mar 2017 11:22:12 -0400 Subject: [PATCH 075/719] docs($interval): Update interval promise docs It's currently not clear when or why the promise returned by `$interval` resolves. This updates the docs to be more specific. Closes #15862 --- src/ng/interval.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/interval.js b/src/ng/interval.js index f42a87bf972a..673df2d4ef51 100644 --- a/src/ng/interval.js +++ b/src/ng/interval.js @@ -41,7 +41,7 @@ function $IntervalProvider() { * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. + * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. * * @example * From dcdd5def8ff896776f8d89fc4182d228dc1d3d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 22 Mar 2017 10:34:35 +0100 Subject: [PATCH 076/719] test(jQuery): run tests with jQuery 2.1, 2.2 & 3.2 Also, update jQuery 2.2.x mentions in the tutorial to 3.2.x. Closes #15843 --- bower.json | 2 +- docs/content/tutorial/step_14.ngdoc | 4 ++-- package.json | 2 +- yarn.lock | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bower.json b/bower.json index 95db7a55a742..099d5b1ed3e6 100644 --- a/bower.json +++ b/bower.json @@ -2,7 +2,7 @@ "name": "angularjs", "license": "MIT", "devDependencies": { - "jquery": "3.1.0", + "jquery": "3.2.1", "jquery-2.2": "jquery#2.2.4", "jquery-2.1": "jquery#2.1.4", "closure-compiler": "https://dl.google.com/closure-compiler/compiler-20140814.zip", diff --git a/docs/content/tutorial/step_14.ngdoc b/docs/content/tutorial/step_14.ngdoc index db07adb8b7cc..409d3f984cc3 100644 --- a/docs/content/tutorial/step_14.ngdoc +++ b/docs/content/tutorial/step_14.ngdoc @@ -46,14 +46,14 @@ Since we are using [Bower][bower] to install client-side dependencies, this step "angular-resource": "1.5.x", "angular-route": "1.5.x", "bootstrap": "3.3.x", - "jquery": "2.2.x" + "jquery": "3.2.x" } } ``` * `"angular-animate": "1.5.x"` tells bower to install a version of the angular-animate module that is compatible with version 1.5.x of AngularJS. -* `"jquery": "2.2.x"` tells bower to install the latest patch release of the 2.2 version of jQuery. +* `"jquery": "3.2.x"` tells bower to install the latest patch release of the 3.2 version of jQuery. Note that this is not an AngularJS library; it is the standard jQuery library. We can use bower to install a wide range of 3rd party libraries. diff --git a/package.json b/package.json index 10c601f133a9..89c48fca8f3e 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "jasmine-core": "^2.4.0", "jasmine-node": "^2.0.0", "jasmine-reporters": "^2.2.0", - "jquery": "^3.1.1", + "jquery": "^3.2.1", "karma": "^1.1.2", "karma-browserstack-launcher": "^1.0.1", "karma-chrome-launcher": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index 410bbec46f7d..9ef42ec2a9fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3508,9 +3508,9 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" -jquery@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.1.1.tgz#347c1c21c7e004115e0a4da32cece041fad3c8a3" +jquery@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" js-tokens@^3.0.0: version "3.0.1" From bf5c2eef34a314d5d2298fdcb6deaa7282cc563f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 20 Aug 2014 20:25:50 +0200 Subject: [PATCH 077/719] fix(jqLite): make jqLite invoke jqLite.cleanData as a method The previous implementation of jqLite didn't use cleanData from the jqLite object but instead used a cached version which maede it impossible to monkey-patch jqLite.cleanData similarly to how you can do it in jQuery. The cleanData method is not meant to be called directly by userland code; its purpose is mainly to be able to be monkey-patched; therefore, the previous implementation didn't make a lot of sense. This commit enables one of the tests so far run only with jQuery to run with jqLite as well. Ref #8486 Ref #8695 Closes #15846 --- src/jqLite.js | 19 ++++------- test/ng/compileSpec.js | 72 ++++++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index a3ba66b16cf5..ad59fbd1af2b 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -201,12 +201,6 @@ function jqLiteHasData(node) { return false; } -function jqLiteCleanData(nodes) { - for (var i = 0, ii = nodes.length; i < ii; i++) { - jqLiteRemoveData(nodes[i]); - } -} - function jqLiteBuildFragment(html, context) { var tmp, tag, wrap, fragment = context.createDocumentFragment(), @@ -309,13 +303,10 @@ function jqLiteClone(element) { } function jqLiteDealoc(element, onlyDescendants) { - if (!onlyDescendants) jqLiteRemoveData(element); + if (!onlyDescendants && jqLiteAcceptsData(element)) jqLite.cleanData([element]); if (element.querySelectorAll) { - var descendants = element.querySelectorAll('*'); - for (var i = 0, l = descendants.length; i < l; i++) { - jqLiteRemoveData(descendants[i]); - } + jqLite.cleanData(element.querySelectorAll('*')); } } @@ -613,7 +604,11 @@ forEach({ data: jqLiteData, removeData: jqLiteRemoveData, hasData: jqLiteHasData, - cleanData: jqLiteCleanData + cleanData: function jqLiteCleanData(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + jqLiteRemoveData(nodes[i]); + } + } }, function(fn, name) { JQLite[name] = fn; }); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index d99d9e9c1826..2d01c4ed5f0c 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2100,28 +2100,26 @@ describe('$compile', function() { it('should work in jqLite and jQuery with jQuery.cleanData last patched by Angular', runTest); - if (jQuery) { - it('should work with another library patching jQuery.cleanData after Angular', function() { - var cleanedCount = 0; - var currentCleanData = jqLite.cleanData; - jqLite.cleanData = function(elems) { - cleanedCount += elems.length; - // Don't return the output and explicitly pass only the first parameter - // so that we're sure we're not relying on either of them. jQuery UI patch - // behaves in this way. - currentCleanData(elems); - }; + it('should work with another library patching jqLite/jQuery.cleanData after Angular', function() { + var cleanedCount = 0; + var currentCleanData = jqLite.cleanData; + jqLite.cleanData = function(elems) { + cleanedCount += elems.length; + // Don't return the output and explicitly pass only the first parameter + // so that we're sure we're not relying on either of them. jQuery UI patch + // behaves in this way. + currentCleanData(elems); + }; - runTest(); + runTest(); - // The initial ng-repeat div is dumped after parsing hence we expect cleanData - // count to be one larger than size of the iterated array. - expect(cleanedCount).toBe(is.length + 1); + // The initial ng-repeat div is dumped after parsing hence we expect cleanData + // count to be one larger than size of the iterated array. + expect(cleanedCount).toBe(is.length + 1); - // Restore the previous cleanData. - jqLite.cleanData = currentCleanData; - }); - } + // Restore the previous cleanData. + jqLite.cleanData = currentCleanData; + }); }); describe('replace and not exactly one root element', function() { @@ -8640,28 +8638,26 @@ describe('$compile', function() { it('should work without external libraries (except jQuery)', testCleanup); - if (jQuery) { - it('should work with another library patching jQuery.cleanData after AngularJS', function() { - var cleanedCount = 0; - var currentCleanData = jqLite.cleanData; - jqLite.cleanData = function(elems) { - cleanedCount += elems.length; - // Don't return the output and explicitly pass only the first parameter - // so that we're sure we're not relying on either of them. jQuery UI patch - // behaves in this way. - currentCleanData(elems); - }; + it('should work with another library patching jqLite/jQuery.cleanData after AngularJS', function() { + var cleanedCount = 0; + var currentCleanData = jqLite.cleanData; + jqLite.cleanData = function(elems) { + cleanedCount += elems.length; + // Don't return the output and explicitly pass only the first parameter + // so that we're sure we're not relying on either of them. jQuery UI patch + // behaves in this way. + currentCleanData(elems); + }; - testCleanup(); + testCleanup(); - // The ng-repeat template is removed/cleaned (the +1) - // and each clone of the ng-repeat template is also removed (xs.length) - expect(cleanedCount).toBe(xs.length + 1); + // The ng-repeat template is removed/cleaned (the +1) + // and each clone of the ng-repeat template is also removed (xs.length) + expect(cleanedCount).toBe(xs.length + 1); - // Restore the previous cleanData. - jqLite.cleanData = currentCleanData; - }); - } + // Restore the previous cleanData. + jqLite.cleanData = currentCleanData; + }); }); From 2931a6df034ad0b93b24718e86814e4592ade43e Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Mon, 20 Mar 2017 22:47:56 -0700 Subject: [PATCH 078/719] refactor(ngModel): use local scope param in watcher --- src/ng/directive/ngModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index a1f47678e491..063ebf05f003 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -890,8 +890,8 @@ function setupModelWatcher(ctrl) { // -> scope value did not change since the last digest as // ng-change executes in apply phase // 4. view should be changed back to 'a' - ctrl.$$scope.$watch(function ngModelWatch() { - var modelValue = ctrl.$$ngModelGet(ctrl.$$scope); + ctrl.$$scope.$watch(function ngModelWatch(scope) { + var modelValue = ctrl.$$ngModelGet(scope); // if scope model value and ngModel value are out of sync // TODO(perf): why not move this to the action fn? From b0f6afcdacc53fd91b37ad0c5d70544f7e37ebde Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Mon, 20 Mar 2017 22:49:02 -0700 Subject: [PATCH 079/719] test($compile): add test for #15833 --- test/ng/compileSpec.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2d01c4ed5f0c..000068c45168 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5944,6 +5944,30 @@ describe('$compile', function() { })); + // https://github.com/angular/angular.js/issues/15833 + it('should work with ng-model inputs', function() { + var componentScope; + + module(function($compileProvider) { + $compileProvider.directive('undi', function() { + return { + restrict: 'A', + scope: { + undi: '<' + }, + link: function($scope) { componentScope = $scope; } + }; + }); + }); + + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(componentScope.undi).toBeDefined(); + }); + }); + + it('should not complain when the isolated scope changes', inject(function() { compile('
'); From f1d0f0386301adc7dea029253d1c0277aa451f37 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 4 Feb 2017 20:47:39 -0800 Subject: [PATCH 080/719] refactor($parse): make use of local variable instead of refetching property --- src/ng/parse.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 98179e8f549b..6d928c3f6774 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1942,9 +1942,8 @@ function $ParseProvider() { // Propagate $$watchDelegates other then inputsWatchDelegate useInputs = !parsedExpression.inputs; - if (parsedExpression.$$watchDelegate && - parsedExpression.$$watchDelegate !== inputsWatchDelegate) { - fn.$$watchDelegate = parsedExpression.$$watchDelegate; + if (watchDelegate && watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = watchDelegate; fn.inputs = parsedExpression.inputs; } else if (!interceptorFn.$stateful) { // If there is an interceptor, but no watchDelegate then treat the interceptor like From 9d74f0fdcb44aa597b81a2bde967d4c37b60dce2 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 4 Feb 2017 17:51:41 -0800 Subject: [PATCH 081/719] refactor($compile): reuse shared simpleCompare method --- src/.eslintrc.json | 1 + src/Angular.js | 7 ++++++- src/ng/compile.js | 7 ++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 4cf6b0f95e00..4c240b733bf9 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -69,6 +69,7 @@ "arrayRemove": false, "copy": false, "shallowCopy": false, + "simpleCompare": false, "equals": false, "csp": false, "concat": false, diff --git a/src/Angular.js b/src/Angular.js index af6379e22ad1..7f539e81fb52 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -62,6 +62,7 @@ includes, arrayRemove, copy, + simpleCompare, equals, csp, jq, @@ -1024,6 +1025,10 @@ function copy(source, destination, maxDepth) { } +// eslint-disable-next-line no-self-compare +function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } + + /** * @ngdoc function * @name angular.equals @@ -1104,7 +1109,7 @@ function equals(o1, o2) { } } else if (isDate(o1)) { if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); + return simpleCompare(o1.getTime(), o2.getTime()); } else if (isRegExp(o1)) { if (!isRegExp(o2)) return false; return o1.toString() === o2.toString(); diff --git a/src/ng/compile.js b/src/ng/compile.js index 7c89444fcb5a..5c1f255849b8 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3434,8 +3434,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - // eslint-disable-next-line no-self-compare - compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }; + compare = simpleCompare; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest @@ -3510,9 +3509,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); function recordChanges(key, currentValue, previousValue) { - if (isFunction(destination.$onChanges) && currentValue !== previousValue && - // eslint-disable-next-line no-self-compare - (currentValue === currentValue || previousValue === previousValue)) { + if (isFunction(destination.$onChanges) && !simpleCompare(currentValue, previousValue)) { // If we have not already scheduled the top level onChangesQueue handler then do so now if (!onChangesQueue) { scope.$$postDigest(flushOnChangesQueue); From 2b0c0505e222e28449829e6f4f49826b1d1ce59e Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sun, 16 Oct 2016 18:04:08 -0700 Subject: [PATCH 082/719] refactor($parse): move duplicate $parse interpreter/compiler logic into Parser - the construction of the AST is now in the Parser - the assigning of the literal and constant flags is now in the Parser - remove unused references to the lexer, $filter and options on the Parser --- src/ng/parse.js | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 6d928c3f6774..8956c618fa4c 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -769,15 +769,13 @@ function isConstant(ast) { return ast.constant; } -function ASTCompiler(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTCompiler($filter) { this.$filter = $filter; } ASTCompiler.prototype = { - compile: function(expression) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); this.state = { nextId: 0, filters: {}, @@ -832,8 +830,6 @@ ASTCompiler.prototype = { ifDefined, plusFn); this.state = this.stage = undefined; - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, @@ -1236,15 +1232,13 @@ ASTCompiler.prototype = { }; -function ASTInterpreter(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTInterpreter($filter) { this.$filter = $filter; } ASTInterpreter.prototype = { - compile: function(expression) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -1283,8 +1277,6 @@ ASTInterpreter.prototype = { if (inputs) { fn.inputs = inputs; } - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, @@ -1613,20 +1605,21 @@ ASTInterpreter.prototype = { /** * @constructor */ -var Parser = function Parser(lexer, $filter, options) { - this.lexer = lexer; - this.$filter = $filter; - this.options = options; +function Parser(lexer, $filter, options) { this.ast = new AST(lexer, options); - this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : - new ASTCompiler(this.ast, $filter); -}; + this.astCompiler = options.csp ? new ASTInterpreter($filter) : + new ASTCompiler($filter); +} Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text); + var ast = this.ast.ast(text); + var fn = this.astCompiler.compile(ast); + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; } }; From 1e841a89c1c999a9d819a14d729e6eb7c2d40bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski?= Date: Wed, 29 Mar 2017 14:15:26 +0200 Subject: [PATCH 083/719] docs($compile): remove a mention of preassigning bindings in controllers The deprecation warning is no longer needed as the feature has been removed in 1.7. Closes #15870 --- src/ng/compile.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 5c1f255849b8..0405d3be76d3 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -374,12 +374,6 @@ * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings * initialized. * - *
- * **Deprecation warning:** although bindings for non-ES6 class controllers are currently - * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization - * code that relies upon bindings inside a `$onInit` method on the controller, instead. - *
- * * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. * This will set up the scope bindings to the controller directly. Note that `scope` can still be used * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate From f403925ee034be3c95d3a0f78fa11dcfc905ae13 Mon Sep 17 00:00:00 2001 From: bennycoomans Date: Wed, 29 Mar 2017 14:49:00 +0200 Subject: [PATCH 084/719] docs(guide/Migrating from Previous Versions): fix typo "indentifiers" Closes #15871 --- docs/content/guide/migration.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/guide/migration.ngdoc b/docs/content/guide/migration.ngdoc index 6a2c25787cff..4a416357f0b0 100644 --- a/docs/content/guide/migration.ngdoc +++ b/docs/content/guide/migration.ngdoc @@ -1296,7 +1296,7 @@ Due to [b71d7c3f](https://github.com/angular/angular.js/commit/b71d7c3f3c04e65b0 falsy values (`''`, `0`, `false` and `null`) are properly recognized as option group identifiers for options passed to `ngOptions`. Previously, all of these values were ignored and the option was not assigned to any group. `undefined` is still interpreted as "no group". -If you have options with falsy group indentifiers that should still not be assigned to any group, +If you have options with falsy group identifiers that should still not be assigned to any group, then you must filter the values before passing them to `ngOptions`, converting falsy values to `undefined`. From c2b8fab0a480204374d561d6b9b3d47347ac5570 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 28 Jan 2017 17:16:48 -0800 Subject: [PATCH 085/719] fix($rootScope): provide correct value of one-time bindings in watchGroup --- src/ng/rootScope.js | 19 +++++---- test/ng/rootScopeSpec.js | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 88edcf3faa31..27f8ab992aec 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -490,9 +490,8 @@ function $RootScopeProvider() { } forEach(watchExpressions, function(expr, i) { - var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value) { newValues[i] = value; - oldValues[i] = oldValue; if (!changeReactionScheduled) { changeReactionScheduled = true; self.$evalAsync(watchGroupAction); @@ -504,11 +503,17 @@ function $RootScopeProvider() { function watchGroupAction() { changeReactionScheduled = false; - if (firstRun) { - firstRun = false; - listener(newValues, newValues, self); - } else { - listener(newValues, oldValues, self); + try { + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } finally { + for (var i = 0; i < watchExpressions.length; i++) { + oldValues[i] = newValues[i]; + } } } diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 3f5a444f4a58..364196a94b2b 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1097,6 +1097,94 @@ describe('Scope', function() { expect(log).toEqual(''); }); + it('should remove all watchers once one-time/constant bindings are stable', function() { + //empty + scope.$watchGroup([], noop); + //single one-time + scope.$watchGroup(['::a'], noop); + //multi one-time + scope.$watchGroup(['::a', '::b'], noop); + //single constant + scope.$watchGroup(['1'], noop); + //multi constant + scope.$watchGroup(['1', '2'], noop); + + expect(scope.$$watchersCount).not.toBe(0); + scope.$apply('a = b = 1'); + expect(scope.$$watchersCount).toBe(0); + }); + + it('should maintain correct new/old values with one time bindings', function() { + var newValues; + var oldValues; + scope.$watchGroup(['a', '::b', 'b', '4'], function(n, o) { + newValues = n.slice(); + oldValues = o.slice(); + }); + + scope.$apply(); + expect(newValues).toEqual(oldValues); + expect(oldValues).toEqual([undefined, undefined, undefined, 4]); + + scope.$apply('a = 1'); + expect(newValues).toEqual([1, undefined, undefined, 4]); + expect(oldValues).toEqual([undefined, undefined, undefined, 4]); + + scope.$apply('b = 2'); + expect(newValues).toEqual([1, 2, 2, 4]); + expect(oldValues).toEqual([1, undefined, undefined, 4]); + + scope.$apply('b = 3'); + expect(newValues).toEqual([1, 2, 3, 4]); + expect(oldValues).toEqual([1, 2, 2, 4]); + + scope.$apply('b = 4'); + expect(newValues).toEqual([1, 2, 4, 4]); + expect(oldValues).toEqual([1, 2, 3, 4]); + }); + }); + + describe('$watchGroup with logging $exceptionHandler', function() { + it('should maintain correct new/old values even when listener throws', function() { + module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + }); + + inject(function($rootScope, $exceptionHandler) { + var newValues; + var oldValues; + $rootScope.$watchGroup(['a', '::b', 'b', '4'], function(n, o) { + newValues = n.slice(); + oldValues = o.slice(); + throw 'test'; + }); + + $rootScope.$apply(); + expect(newValues).toEqual(oldValues); + expect(oldValues).toEqual([undefined, undefined, undefined, 4]); + expect($exceptionHandler.errors.length).toBe(1); + + $rootScope.$apply('a = 1'); + expect(newValues).toEqual([1, undefined, undefined, 4]); + expect(oldValues).toEqual([undefined, undefined, undefined, 4]); + expect($exceptionHandler.errors.length).toBe(2); + + $rootScope.$apply('b = 2'); + expect(newValues).toEqual([1, 2, 2, 4]); + expect(oldValues).toEqual([1, undefined, undefined, 4]); + expect($exceptionHandler.errors.length).toBe(3); + + $rootScope.$apply('b = 3'); + expect(newValues).toEqual([1, 2, 3, 4]); + expect(oldValues).toEqual([1, 2, 2, 4]); + expect($exceptionHandler.errors.length).toBe(4); + + $rootScope.$apply('b = 4'); + expect(newValues).toEqual([1, 2, 4, 4]); + expect(oldValues).toEqual([1, 2, 3, 4]); + expect($exceptionHandler.errors.length).toBe(5); + }); + }); }); describe('$destroy', function() { From 93879b3c721f4c0273c90e9bfeb368425b0078c4 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 31 Mar 2017 00:17:55 -0700 Subject: [PATCH 086/719] fix($parse): fix infinite digest errors when watching objects with .valueOf in literals Closes #15867 --- src/ng/parse.js | 4 +- test/ng/parseSpec.js | 91 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 8956c618fa4c..28ac49ac10c9 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1788,14 +1788,14 @@ function $ParseProvider() { return newValue === oldValueOfValue; } - if (typeof newValue === 'object' && !compareObjectIdentity) { + if (typeof newValue === 'object') { // attempt to convert the value to a primitive type // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can // be cheaply dirty-checked newValue = getValueOf(newValue); - if (typeof newValue === 'object') { + if (typeof newValue === 'object' && !compareObjectIdentity) { // objects/arrays are not supported - deep-watching them would be too expensive return false; } diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index e524eaabd23c..d664b58fb8b9 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2872,6 +2872,40 @@ describe('parser', function() { expect(called).toBe(true); })); + it('should not invoke interceptorFns unless the input.valueOf changes even if the instance changes', inject(function($parse) { + var called = false; + function interceptor(v) { + called = true; + return v; + } + scope.$watch($parse('a', interceptor)); + scope.a = new Date(); + scope.$digest(); + expect(called).toBe(true); + + called = false; + scope.a = new Date(scope.a.valueOf()); + scope.$digest(); + expect(called).toBe(false); + })); + + it('should invoke interceptorFns if input.valueOf changes even if the instance does not', inject(function($parse) { + var called = false; + function interceptor(v) { + called = true; + return v; + } + scope.$watch($parse('a', interceptor)); + scope.a = new Date(); + scope.$digest(); + expect(called).toBe(true); + + called = false; + scope.a.setTime(scope.a.getTime() + 1); + scope.$digest(); + expect(called).toBe(true); + })); + it('should invoke interceptors when the expression is `undefined`', inject(function($parse) { var called = false; function interceptor(v) { @@ -3040,6 +3074,63 @@ describe('parser', function() { expect(called).toBe(true); })); + it('should not reevaluate literals with non-primitive input that does support valueOf()', + inject(function($parse) { + + var date = scope.date = new Date(); + + var parsed = $parse('[date]'); + var watcherCalls = 0; + scope.$watch(parsed, function(input) { + expect(input[0]).toBe(date); + watcherCalls++; + }); + + scope.$digest(); + expect(watcherCalls).toBe(1); + + scope.$digest(); + expect(watcherCalls).toBe(1); + })); + + it('should not reevaluate literals with non-primitive input that does support valueOf()' + + ' when the instance changes but valueOf() does not', inject(function($parse) { + + scope.date = new Date(1234567890123); + + var parsed = $parse('[date]'); + var watcherCalls = 0; + scope.$watch(parsed, function(input) { + watcherCalls++; + }); + + scope.$digest(); + expect(watcherCalls).toBe(1); + + scope.date = new Date(1234567890123); + scope.$digest(); + expect(watcherCalls).toBe(1); + })); + + it('should reevaluate literals with non-primitive input that does support valueOf()' + + ' when the instance does not change but valueOf() does', inject(function($parse) { + + scope.date = new Date(1234567890123); + + var parsed = $parse('[date]'); + var watcherCalls = 0; + scope.$watch(parsed, function(input) { + watcherCalls++; + }); + + scope.$digest(); + expect(watcherCalls).toBe(1); + + scope.date.setTime(scope.date.getTime() + 1); + scope.$digest(); + expect(watcherCalls).toBe(2); + })); + it('should continue with the evaluation of the expression without invoking computed parts', inject(function($parse) { var value = 'foo'; From 189461f9bf6fda18ddbd16c42f2e959cf939c3da Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 31 Mar 2017 00:36:19 -0700 Subject: [PATCH 087/719] fix($parse): standardize one-time literal vs non-literal and interceptors Previously literal one-time bindings did not use the expression `inputs`, causing infinite digest issues with literal values. This often forces the use of deepEquals when watching one-time literals. `ng-class` is one example of deepEquals which is no longer required. This one-time/literal behavior is now also consistently propogated through interceptors. Closes #15858 --- src/ng/directive/ngClass.js | 53 ++---------- src/ng/parse.js | 64 ++++++--------- test/ng/parseSpec.js | 156 ++++++++++++++++++------------------ 3 files changed, 114 insertions(+), 159 deletions(-) diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js index a26c1c77187b..090c2c387f8c 100644 --- a/src/ng/directive/ngClass.js +++ b/src/ng/directive/ngClass.js @@ -14,13 +14,6 @@ function classDirective(name, selector) { return { restrict: 'AC', link: function(scope, element, attr) { - var expression = attr[name].trim(); - var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':'); - - var watchInterceptor = isOneTime ? toFlatValue : toClassString; - var watchExpression = $parse(expression, watchInterceptor); - var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction; - var classCounts = element.data('$classCounts'); var oldModulo = true; var oldClassString; @@ -43,7 +36,7 @@ function classDirective(name, selector) { scope.$watch(indexWatchExpression, ngClassIndexWatchAction); } - scope.$watch(watchExpression, watchAction, isOneTime); + scope.$watch($parse(attr[name], toClassString), ngClassWatchAction); function addClasses(classString) { classString = digestClassCounts(split(classString), 1); @@ -85,9 +78,9 @@ function classDirective(name, selector) { } function ngClassIndexWatchAction(newModulo) { - // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it + // This watch-action should run before the `ngClassWatchAction()`, thus it // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the - // `ngClass[OneTime]WatchAction()` will update the classes. + // `ngClassWatchAction()` will update the classes. if (newModulo === selector) { addClasses(oldClassString); } else { @@ -97,15 +90,13 @@ function classDirective(name, selector) { oldModulo = newModulo; } - function ngClassOneTimeWatchAction(newClassValue) { - var newClassString = toClassString(newClassValue); - - if (newClassString !== oldClassString) { - ngClassWatchAction(newClassString); + function ngClassWatchAction(newClassString) { + // When using a one-time binding the newClassString will return + // the pre-interceptor value until the one-time is complete + if (!isString(newClassString)) { + newClassString = toClassString(newClassString); } - } - function ngClassWatchAction(newClassString) { if (oldModulo === selector) { updateClasses(oldClassString, newClassString); } @@ -152,34 +143,6 @@ function classDirective(name, selector) { return classString; } - - function toFlatValue(classValue) { - var flatValue = classValue; - - if (isArray(classValue)) { - flatValue = classValue.map(toFlatValue); - } else if (isObject(classValue)) { - var hasUndefined = false; - - flatValue = Object.keys(classValue).filter(function(key) { - var value = classValue[key]; - - if (!hasUndefined && isUndefined(value)) { - hasUndefined = true; - } - - return value; - }); - - if (hasUndefined) { - // Prevent the `oneTimeLiteralWatchInterceptor` from unregistering - // the watcher, by including at least one `undefined` value. - flatValue.push(undefined); - } - } - - return flatValue; - } } /** diff --git a/src/ng/parse.js b/src/ng/parse.js index 28ac49ac10c9..c9fa42b63e87 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1765,8 +1765,8 @@ function $ParseProvider() { if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; } else if (oneTime) { - parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + parsedExpression.oneTime = true; + parsedExpression.$$watchDelegate = oneTimeWatchDelegate; } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } @@ -1852,6 +1852,7 @@ function $ParseProvider() { } function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var isDone = parsedExpression.literal ? isAllDefined : isDefined; var unwatch, lastValue; if (parsedExpression.inputs) { unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); @@ -1868,9 +1869,9 @@ function $ParseProvider() { if (isFunction(listener)) { listener(value, old, scope); } - if (isDefined(value)) { + if (isDone(value)) { scope.$$postDigest(function() { - if (isDefined(lastValue)) { + if (isDone(lastValue)) { unwatch(); } }); @@ -1878,31 +1879,12 @@ function $ParseProvider() { } } - function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch, lastValue; - unwatch = scope.$watch(function oneTimeWatch(scope) { - return parsedExpression(scope); - }, function oneTimeListener(value, old, scope) { - lastValue = value; - if (isFunction(listener)) { - listener(value, old, scope); - } - if (isAllDefined(value)) { - scope.$$postDigest(function() { - if (isAllDefined(lastValue)) unwatch(); - }); - } - }, objectEquality); - - return unwatch; - - function isAllDefined(value) { - var allDefined = true; - forEach(value, function(val) { - if (!isDefined(val)) allDefined = false; - }); - return allDefined; - } + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; + }); + return allDefined; } function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { @@ -1918,22 +1900,28 @@ function $ParseProvider() { var watchDelegate = parsedExpression.$$watchDelegate; var useInputs = false; - var regularWatch = - watchDelegate !== oneTimeLiteralWatchDelegate && - watchDelegate !== oneTimeWatchDelegate; + var isDone = parsedExpression.literal ? isAllDefined : isDefined; - var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + function regularInterceptedExpression(scope, locals, assign, inputs) { var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); return interceptorFn(value, scope, locals); - } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { - var value = parsedExpression(scope, locals, assign, inputs); + } + + function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); var result = interceptorFn(value, scope, locals); // we only return the interceptor's result if the // initial value is defined (for bind-once) - return isDefined(value) ? result : value; - }; + return isDone(value) ? result : value; + } + + var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression; + + // Propogate the literal/oneTime attributes + fn.literal = parsedExpression.literal; + fn.oneTime = parsedExpression.oneTime; - // Propagate $$watchDelegates other then inputsWatchDelegate + // Propagate or create inputs / $$watchDelegates useInputs = !parsedExpression.inputs; if (watchDelegate && watchDelegate !== inputsWatchDelegate) { fn.$$watchDelegate = watchDelegate; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index d664b58fb8b9..13d7a10039b0 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2688,82 +2688,86 @@ describe('parser', function() { expect($parse(':: ').literal).toBe(true); })); - it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) { - var fn = $parse('::{foo: foo, bar: bar}'); - $rootScope.$watch(fn, function(value) { log(value); }, true); - - expect(log.empty()).toEqual([]); - expect($rootScope.$$watchers.length).toBe(1); - - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]); - - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]); - - $rootScope.foo = 'foobar'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]); - - $rootScope.foo = 'baz'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([]); - })); - - it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) { - var fn = $parse('::[foo,bar]'); - $rootScope.$watch(fn, function(value) { log(value); }, true); - - expect(log.empty()).toEqual([]); - expect($rootScope.$$watchers.length).toBe(1); - - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([[undefined, undefined]]); - - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([['foo', undefined]]); - - $rootScope.foo = 'foobar'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([['foobar', 'bar']]); - - $rootScope.foo = 'baz'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([]); - })); - - it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) { - var fn = $parse('::[foo]'); - $rootScope.$watch(fn, function(value) { log(value); }, true); - $rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } }); - - $rootScope.foo = 'bar'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(2); - expect(log.empty()).toEqual([['bar'], [undefined]]); - - $rootScope.foo = 'baz'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([['baz']]); - - $rootScope.bar = 'qux'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log).toEqual([]); - })); + [true, false].forEach(function(isDeep) { + describe(isDeep ? 'deepWatch' : 'watch', function() { + it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) { + var fn = $parse('::{foo: foo, bar: bar}'); + $rootScope.$watch(fn, function(value) { log(value); }, isDeep); + + expect(log.empty()).toEqual([]); + expect($rootScope.$$watchers.length).toBe(1); + + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]); + + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]); + + $rootScope.foo = 'foobar'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]); + + $rootScope.foo = 'baz'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([]); + })); + + it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) { + var fn = $parse('::[foo,bar]'); + $rootScope.$watch(fn, function(value) { log(value); }, isDeep); + + expect(log.empty()).toEqual([]); + expect($rootScope.$$watchers.length).toBe(1); + + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([[undefined, undefined]]); + + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([['foo', undefined]]); + + $rootScope.foo = 'foobar'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([['foobar', 'bar']]); + + $rootScope.foo = 'baz'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([]); + })); + + it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) { + var fn = $parse('::[foo]'); + $rootScope.$watch(fn, function(value) { log(value); }, isDeep); + $rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } }); + + $rootScope.foo = 'bar'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(2); + expect(log.empty()).toEqual([['bar'], [undefined]]); + + $rootScope.foo = 'baz'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([['baz']]); + + $rootScope.bar = 'qux'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log).toEqual([]); + })); + }); + }); }); }); From 617b36117a1e66e29deca19c347dedd096316ae9 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 31 Mar 2017 09:49:31 +0200 Subject: [PATCH 088/719] docs(changelog): add release notes for 1.6.4 --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f77b08402e2..17ac2b3c83e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ + +# 1.6.4 phenomenal-footnote (2017-03-31) + + +## Bug Fixes +- **$parse:** + - standardize one-time literal vs non-literal and interceptors + ([60394a](https://github.com/angular/angular.js/commit/60394a9d91dad8932fa900af7c8529837f1d4557), + [#15858](https://github.com/angular/angular.js/issues/15858)) + - fix infinite digest errors when watching objects with .valueOf in literals + ([f5ddb1](https://github.com/angular/angular.js/commit/f5ddb10b56676c2ad912ce453acb87f0a7a94e01), + [#15867](https://github.com/angular/angular.js/issues/15867)) +- **ngModel:** prevent internal scope reference from being copied + ([e1f8a6](https://github.com/angular/angular.js/commit/e1f8a6e82bb8a70079ef3db9a891b1c08b5bae31), + [#15833](https://github.com/angular/angular.js/issues/15833)) +- **jqLite:** make jqLite invoke jqLite.cleanData as a method + ([9cde98](https://github.com/angular/angular.js/commit/9cde98cbc770f8d33fc074ba563b7ab6e2baaf8b), + [#15846](https://github.com/angular/angular.js/issues/15846)) +- **$http:** throw more informative error on invalid JSON response + ([df8887](https://github.com/angular/angular.js/commit/df88873bb79213057057adb47151b626a7ec0e5d), + [#15695](https://github.com/angular/angular.js/issues/15695), + [#15724](https://github.com/angular/angular.js/issues/15724)) +- **dateFilter:** correctly handle newlines in `format` string + ([982271](https://github.com/angular/angular.js/commit/9822711ad2a401c2449239edc13d18b301714757), + [#15794](https://github.com/angular/angular.js/issues/15794), + [#15792](https://github.com/angular/angular.js/issues/15792)) + + +## New Features +- **$resource:** add `hasBody` action configuration option + ([a9f987](https://github.com/angular/angular.js/commit/a9f987a0c9653246ea471a89197907d94c0cea2a), + [#10128](https://github.com/angular/angular.js/issues/10128), + [#12181](https://github.com/angular/angular.js/issues/12181)) + + # 1.6.3 scriptalicious-bootstrapping (2017-03-08) From 06516d7c27a32614b1d45070c409e2d0112c9a11 Mon Sep 17 00:00:00 2001 From: michaelb958 Date: Sun, 2 Apr 2017 16:58:31 +1000 Subject: [PATCH 089/719] docs(guide/i18n): fix links to CLDR The old link target is dead, deceased, pushing up daisies. I quote: > The cldr-tmp repository is no longer available. > For access to CLDR sources and data, please see the [CLDR pages](link to new one). Closes #15879 --- docs/content/guide/i18n.ngdoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/content/guide/i18n.ngdoc b/docs/content/guide/i18n.ngdoc index ebe28c2b57fc..c090f70d1a95 100644 --- a/docs/content/guide/i18n.ngdoc +++ b/docs/content/guide/i18n.ngdoc @@ -281,18 +281,18 @@ categories as you need. #### Selection Keywords The selection keywords can be either exact matches or language dependent [plural -categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). +categories](http://cldr.unicode.org/index/cldr-spec/plural-rules). Exact matches are written as the equal sign followed by the exact value. `=0`, `=1`, `=2` and `=123` are all examples of exact matches. Note that there should be no space between the equal sign and the numeric value. Plural category matches are single words corresponding to the [plural -categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) of -the CLDR plural category spec. These categories vary by locale. The "en" (English) locale, for -example, defines just "one" and "other" while the "ga" (Irish) locale defines "one", "two", "few", -"many" and "other". Typically, you would just write the categories for your language. During -translation, the translators will add or remove more categories depending on the target locale. +categories](http://cldr.unicode.org/index/cldr-spec/plural-rules) of the CLDR plural category spec. +These categories vary by locale. The "en" (English) locale, for example, defines just "one" and +"other" while the "ga" (Irish) locale defines "one", "two", "few", "many" and "other". Typically, +you would just write the categories for your language. During translation, the translators will add +or remove more categories depending on the target locale. Exact matches always win over keyword matches. Therefore, if you define both `=0` and `zero`, when the value of the expression is zero, the `=0` message is the one that will be selected. (The From 136a42abc162a64ac57ff45417553b58a031990b Mon Sep 17 00:00:00 2001 From: Atul Shimpi Date: Mon, 3 Apr 2017 13:49:35 +0530 Subject: [PATCH 090/719] docs(README): improve vocabulary and orthography Closes #15876 Closes #15875 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c6a8dd5c369c..ea372ec0a658 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ synchronizes data from your UI (view) with your JavaScript objects (model) throu binding. To help you structure your application better and make it easy to test, AngularJS teaches the browser how to do dependency injection and inversion of control. -It also helps with server-side communication, taming async callbacks with promises and deferreds, -and it makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a +It also helps with server-side communication, taming async callbacks with promises and deferred objects, +and it makes client-side navigation and deep linking with hashbang urls or HTML5 pushState a piece of cake. Best of all? It makes development fun! * Web site: https://angularjs.org From 0fbb1187b8d9f04576c146221aaba95e983bdac7 Mon Sep 17 00:00:00 2001 From: Atef Ben Ali Date: Wed, 5 Apr 2017 14:13:28 +0100 Subject: [PATCH 091/719] docs(guide/directive): delete redundant 'the' Closes #15891 --- docs/content/guide/directive.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index f47cf368120d..a40e3ee85803 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -123,7 +123,7 @@ The other forms shown above are accepted for legacy reasons but we advise you to `$compile` can match directives based on element names (E), attributes (A), class names (C), and comments (M). -The built-in the AngularJS directives show in their documentation page which type of matching they support. +The built-in AngularJS directives show in their documentation page which type of matching they support. The following demonstrates the various ways a directive (`myDir` in this case) that matches all 4 types can be referenced from within a template. From 6ee7c29ca74e416bd53ca6174e2003a001e1356c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 7 Apr 2017 15:01:49 +0200 Subject: [PATCH 092/719] docs(filter/filter): remove duplicate 'the' Closes #15893 --- src/ng/filter/filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/filter/filter.js b/src/ng/filter/filter.js index a11a0ce4d25d..d0848209feed 100644 --- a/src/ng/filter/filter.js +++ b/src/ng/filter/filter.js @@ -46,7 +46,7 @@ * * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in * determining if values retrieved using `expression` (when it is not a function) should be - * considered a match based on the the expected value (from the filter expression) and actual + * considered a match based on the expected value (from the filter expression) and actual * value (from the object in the array). * * Can be one of: From e812b9fa9ec7086ab8d64a32d86f6e991f84bc55 Mon Sep 17 00:00:00 2001 From: Eli Sadoff Date: Fri, 7 Apr 2017 15:23:19 +0000 Subject: [PATCH 093/719] docs(filter/uppercase): add an example I saw that the uppercase filter had no example so I decided to add a minimal example to explain how the uppercase filter works. Thank you very much to @narretz for helping me through this process. Closes #15885 --- src/ng/filter/filters.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js index 3f83895f74c2..5a79a7799929 100644 --- a/src/ng/filter/filters.js +++ b/src/ng/filter/filters.js @@ -698,6 +698,9 @@ function jsonFilter() { * @kind function * @description * Converts string to lowercase. + * + * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example. + * * @see angular.lowercase */ var lowercaseFilter = valueFn(lowercase); @@ -709,6 +712,22 @@ var lowercaseFilter = valueFn(lowercase); * @kind function * @description * Converts string to uppercase. - * @see angular.uppercase + * @example + + + +
+ +

{{title}}

+ +

{{title | uppercase}}

+
+
+
*/ var uppercaseFilter = valueFn(uppercase); From d0622d06499ae514dd618f593e103d9e8857b217 Mon Sep 17 00:00:00 2001 From: Vigneshkumar Chinnachamy Date: Sat, 8 Apr 2017 17:40:21 +0530 Subject: [PATCH 094/719] docs(guide/Developer Guide): Update twitter handle Replaced the old Angular twitter handle with the new one. Closes #15903 --- docs/content/guide/index.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/guide/index.ngdoc b/docs/content/guide/index.ngdoc index b3cc1e5385d3..fa5be9875d4e 100644 --- a/docs/content/guide/index.ngdoc +++ b/docs/content/guide/index.ngdoc @@ -75,7 +75,7 @@ Official announcements, news and releases are posted to our blog, G+ and Twitter * [AngularJS Blog](http://blog.angularjs.org/) * [Google+](https://plus.google.com/u/0/+AngularJS) -* [Twitter](https://twitter.com/angularjs) +* [Twitter](https://twitter.com/angular) * [AngularJS on YouTube](http://youtube.com/angularjs) ## Contributing to AngularJS From e23782b8c23fc766efb29a87a25bc054af3159fd Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 12 Apr 2017 11:21:10 +0200 Subject: [PATCH 095/719] docs($http): correct and clarify default transforms - baddata error described incorrect http behavior, and workarounds - httpProvider defaults were missing transformResponse / transformRequest - http was not clear about JSON detection strategy Closes #15897 Closes #15906 --- docs/content/error/$http/baddata.ngdoc | 12 +++---- src/ng/http.js | 44 +++++++++++++++++++------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/content/error/$http/baddata.ngdoc b/docs/content/error/$http/baddata.ngdoc index 9349b76639c5..512a73046145 100644 --- a/docs/content/error/$http/baddata.ngdoc +++ b/docs/content/error/$http/baddata.ngdoc @@ -3,12 +3,12 @@ @fullName Bad JSON Data @description -The default @{link ng.$http#default-transformations `transformResponse`} will try to parse the -response as JSON if the `Content-Type` header is `application/json` or the response looks like a +The default {@link ng.$http#default-transformations `transformResponse`} will try to parse the +response as JSON if the `Content-Type` header is `application/json`, or the response looks like a valid JSON-stringified object or array. This error occurs when that data is not a valid JSON object. -The error message should provide additional context such as the actual response. - -To resolve this error, make sure you pass valid JSON data to `transformResponse` or use an -appropriate `Content-Type` header for non-JSON data. +To resolve this error, make sure you pass valid JSON data to `transformResponse`. If the response +data looks like JSON, but has a different `Content-Type` header, you must +{@link ng.$http#overriding-the-default-transformations-per-request implement your own response +transformer on a per request basis}, or {@link ng.$http#default-transformations modify the default `$http` responseTransform}. diff --git a/src/ng/http.js b/src/ng/http.js index fe67455e3ca9..6c9785e5ac06 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -266,12 +266,6 @@ function $HttpProvider() { * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses * by default. See {@link $http#caching $http Caching} for more information. * - * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. - * Defaults value is `'XSRF-TOKEN'`. - * - * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the - * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. - * * - **`defaults.headers`** - {Object} - Default headers for all $http requests. * Refer to {@link ng.$http#setting-http-headers $http} for documentation on * setting default headers. @@ -280,15 +274,38 @@ function $HttpProvider() { * - **`defaults.headers.put`** * - **`defaults.headers.patch`** * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. * * - **`defaults.paramSerializer`** - `{string|function(Object):string}` - A function * used to the prepare string representation of request parameters (specified as an object). * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * - * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the - * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the - * {@link $jsonpCallbacks} service. Defaults to `'callback'`. + * - **`defaults.transformRequest`** - + * `{Array|function(data, headersGetter)}` - + * An array of functions (or a single function) which are applied to the request data. + * By default, this is an array with one request transformation function: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * - **`defaults.transformResponse`** - + * `{Array|function(data, headersGetter, status)}` - + * An array of functions (or a single function) which are applied to the response data. By default, + * this is an array which applies one response transformation function that does two things: + * + * - If XSRF prefix is detected, strip it + * (see {@link ng.$http#security-considerations Security Considerations in the $http docs}). + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. + * + * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. * **/ var defaults = this.defaults = { @@ -552,15 +569,18 @@ function $HttpProvider() { * * AngularJS provides the following default transformations: * - * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`) is + * an array with one function that does the following: * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`) is + * an array with one function that does the following: * * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. * * * ### Overriding the Default Transformations Per Request From ad0bb83819458563eac9729b448894164cc2da53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski?= Date: Wed, 12 Apr 2017 13:20:43 +0200 Subject: [PATCH 096/719] chore(browserstack): Update browserstacktunnel-wrapper, fix options Only the latest version of the package works correctly (the backend for it at BrowserStack is not versioned) and the options have changed in the new version of the package. Also, iOS 8.0 is no longer available on BrowserStack, only 8.3 is. Instead, this commit changes it to 9.3 as we shouldn't be testing on 8 anymore anyway. Closes #15892 --- karma-shared.conf.js | 4 +-- lib/browserstack/start_tunnel.js | 2 +- package.json | 2 +- yarn.lock | 55 ++++++++++++-------------------- 4 files changed, 24 insertions(+), 39 deletions(-) diff --git a/karma-shared.conf.js b/karma-shared.conf.js index 44acad709f48..46b98b2c4724 100644 --- a/karma-shared.conf.js +++ b/karma-shared.conf.js @@ -122,9 +122,9 @@ module.exports = function(config, specificOptions) { }, 'BS_iOS': { base: 'BrowserStack', - device: 'iPhone 6', + device: 'iPhone 6S', os: 'ios', - os_version: '8.0' + os_version: '9.3' } } }); diff --git a/lib/browserstack/start_tunnel.js b/lib/browserstack/start_tunnel.js index 59519a320438..a97235af8487 100644 --- a/lib/browserstack/start_tunnel.js +++ b/lib/browserstack/start_tunnel.js @@ -25,7 +25,7 @@ PORTS.forEach(function(port) { var tunnel = new BrowserStackTunnel({ key: ACCESS_KEY, - tunnelIdentifier: TUNNEL_IDENTIFIER, + localIdentifier: TUNNEL_IDENTIFIER, hosts: hosts }); diff --git a/package.json b/package.json index 89c48fca8f3e..64a2bcb12efb 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "benchmark": "1.x.x", "bootstrap": "3.1.1", "bower": "~1.3.9", - "browserstacktunnel-wrapper": "^1.4.2", + "browserstacktunnel-wrapper": "2.0.0", "canonical-path": "0.0.2", "changez": "^2.1.1", "changez-angular": "^2.1.2", diff --git a/yarn.lock b/yarn.lock index 9ef42ec2a9fb..60ae25c1d951 100644 --- a/yarn.lock +++ b/yarn.lock @@ -754,7 +754,13 @@ browserstack@1.5.0: dependencies: https-proxy-agent "1.0.0" -browserstacktunnel-wrapper@^1.4.2, browserstacktunnel-wrapper@~1.4.2: +browserstacktunnel-wrapper@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browserstacktunnel-wrapper/-/browserstacktunnel-wrapper-2.0.0.tgz#4d6ebf6a667451ad4ee9325fddcf3546607b4d92" + dependencies: + unzip "~0.1.9" + +browserstacktunnel-wrapper@~1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/browserstacktunnel-wrapper/-/browserstacktunnel-wrapper-1.4.2.tgz#6598fb7d784b6ff348e3df7c104b0d9c27ea5275" dependencies: @@ -1191,15 +1197,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.6, concat-stream@^1.4.7: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -concat-stream@~1.4.1, concat-stream@~1.4.5: +concat-stream@^1.4.6, concat-stream@^1.4.7, concat-stream@~1.4.1, concat-stream@~1.4.5: version "1.4.10" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.4.10.tgz#acc3bbf5602cb8cc980c6ac840fa7d8603e3ef36" dependencies: @@ -1701,11 +1699,7 @@ domain-browser@~1.1.0: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -domelementtype@1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" - -domelementtype@~1.1.1: +domelementtype@1, domelementtype@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" @@ -2244,7 +2238,7 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -findup-sync@0.4.2: +findup-sync@0.4.2, findup-sync@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.2.tgz#a8117d0f73124f5a4546839579fe52d7129fb5e5" dependencies: @@ -2253,15 +2247,6 @@ findup-sync@0.4.2: micromatch "^2.3.7" resolve-dir "^0.1.0" -findup-sync@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" - dependencies: - detect-file "^0.1.0" - is-glob "^2.0.1" - micromatch "^2.3.7" - resolve-dir "^0.1.0" - findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -3110,7 +3095,7 @@ inherits@1: version "1.0.2" resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -5018,18 +5003,18 @@ pump@^0.3.5: end-of-stream "~1.0.0" once "~1.2.0" -punycode@1.3.2: +punycode@1.3.2, punycode@>=0.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" -punycode@>=0.2.0, punycode@~1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.2.4.tgz#54008ac972aec74175def9cba6df7fa9d3918740" - punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +punycode@~1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.2.4.tgz#54008ac972aec74175def9cba6df7fa9d3918740" + q-io@^1.10.9: version "1.13.2" resolved "https://registry.yarnpkg.com/q-io/-/q-io-1.13.2.tgz#eea130d481ddb5e1aa1bc5a66855f7391d06f003" @@ -5163,7 +5148,7 @@ read@~1.0.4: dependencies: mute-stream "~0.0.4" -readable-stream@1.1, "readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.0.27-1, readable-stream@^1.0.33-1, readable-stream@^1.1.13, readable-stream@^1.1.13-1, readable-stream@~1.1.8, readable-stream@~1.1.9: +readable-stream@1.1, "readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.0.27-1, readable-stream@^1.1.13, readable-stream@^1.1.13-1, readable-stream@~1.1.8, readable-stream@~1.1.9: version "1.1.13" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" dependencies: @@ -5172,7 +5157,7 @@ readable-stream@1.1, "readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.0 isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.0, readable-stream@~1.0.17, readable-stream@~1.0.2, readable-stream@~1.0.26, readable-stream@~1.0.31: +"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@^1.0.33-1, readable-stream@~1.0.0, readable-stream@~1.0.17, readable-stream@~1.0.2, readable-stream@~1.0.26, readable-stream@~1.0.31: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" dependencies: @@ -5181,7 +5166,7 @@ readable-stream@1.1, "readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.0 isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2: +readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5: version "2.2.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" dependencies: @@ -6312,7 +6297,7 @@ type-is@~1.6.14: media-typer "0.3.0" mime-types "~2.1.13" -typedarray@^0.0.6, typedarray@~0.0.5: +typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" From 8eb925d7c58a64dd2a63e68e706a9dda309fcfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Apr 2017 13:57:00 +0200 Subject: [PATCH 097/719] chore(browserstack): Update OS X, make iOS 8-10 available to test --- karma-shared.conf.js | 20 ++++++++++++++++---- scripts/travis/build.sh | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/karma-shared.conf.js b/karma-shared.conf.js index 46b98b2c4724..88138cc8e606 100644 --- a/karma-shared.conf.js +++ b/karma-shared.conf.js @@ -85,19 +85,19 @@ module.exports = function(config, specificOptions) { base: 'BrowserStack', browser: 'chrome', os: 'OS X', - os_version: 'Yosemite' + os_version: 'Sierra' }, 'BS_Safari': { base: 'BrowserStack', browser: 'safari', os: 'OS X', - os_version: 'Yosemite' + os_version: 'Sierra' }, 'BS_Firefox': { base: 'BrowserStack', browser: 'firefox', os: 'Windows', - os_version: '8' + os_version: '10' }, 'BS_IE_9': { base: 'BrowserStack', @@ -120,11 +120,23 @@ module.exports = function(config, specificOptions) { os: 'Windows', os_version: '8.1' }, - 'BS_iOS': { + 'BS_iOS_8': { + base: 'BrowserStack', + device: 'iPhone 6', + os: 'ios', + os_version: '8.3' + }, + 'BS_iOS_9': { base: 'BrowserStack', device: 'iPhone 6S', os: 'ios', os_version: '9.3' + }, + 'BS_iOS_10': { + base: 'BrowserStack', + device: 'iPhone 7', + os: 'ios', + os_version: '10.0' } } }); diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index 70dd4182f645..bc2b6635a1db 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -9,7 +9,7 @@ if [ "$JOB" == "ci-checks" ]; then grunt ci-checks elif [ "$JOB" == "unit" ]; then if [ "$BROWSER_PROVIDER" == "browserstack" ]; then - BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_iOS" + BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_iOS_8,BS_iOS_9" else BROWSERS="SL_Chrome,SL_Firefox,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_iOS" fi From 37a2c20bb85f6a2ffd3c6338116c67ced6e5c022 Mon Sep 17 00:00:00 2001 From: TheHalcyonSavant Date: Mon, 17 Apr 2017 13:45:48 +0200 Subject: [PATCH 098/719] docs(guide/migration): remove duplicate entry for commit 13c252 Closes #15919 --- docs/content/guide/migration.ngdoc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/content/guide/migration.ngdoc b/docs/content/guide/migration.ngdoc index 4a416357f0b0..46ca03ec9d02 100644 --- a/docs/content/guide/migration.ngdoc +++ b/docs/content/guide/migration.ngdoc @@ -484,14 +484,6 @@ lifecycle hook), you may need to manually call `$onInit()` from your constructor }) ``` -
- -**Due to [13c252](https://github.com/angular/angular.js/commit/13c2522baf7c8f616b2efcaab4bffd54c8736591)**, -on **IE11 only**, consecutive text nodes will always get merged. Previously, they would not get -merged if they had no parent. The new behavior, which fixes an IE11 bug affecting interpolation -under certain circumstances, might in some edge-cases have unexpected side effects that you should -be aware of. Please, check the commit message for more details. -
**Due to [04cad4](https://github.com/angular/angular.js/commit/04cad41d26ebaf44b5ee0c29a152d61f235f3efa)**, From 496138f12a03028daa37bb23f63cb61826c857f6 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Sat, 9 Apr 2016 14:25:15 +0200 Subject: [PATCH 099/719] chore: test on Microsoft Edge Closes #13687 Closes #14401 --- karma-shared.conf.js | 13 +++++++++++++ scripts/travis/build.sh | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/karma-shared.conf.js b/karma-shared.conf.js index 88138cc8e606..1fcf53d4a4d6 100644 --- a/karma-shared.conf.js +++ b/karma-shared.conf.js @@ -74,6 +74,12 @@ module.exports = function(config, specificOptions) { platform: 'Windows 8.1', version: '11' }, + 'SL_EDGE': { + base: 'SauceLabs', + browserName: 'microsoftedge', + platform: 'Windows 10', + version: '14' + }, 'SL_iOS': { base: 'SauceLabs', browserName: 'iphone', @@ -120,6 +126,13 @@ module.exports = function(config, specificOptions) { os: 'Windows', os_version: '8.1' }, + 'BS_EDGE': { + base: 'BrowserStack', + browser: 'edge', + browser_version: '14', + os: 'Windows', + os_version: '10' + }, 'BS_iOS_8': { base: 'BrowserStack', device: 'iPhone 6', diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index bc2b6635a1db..c193039bc02d 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -9,9 +9,9 @@ if [ "$JOB" == "ci-checks" ]; then grunt ci-checks elif [ "$JOB" == "unit" ]; then if [ "$BROWSER_PROVIDER" == "browserstack" ]; then - BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_iOS_8,BS_iOS_9" + BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9" else - BROWSERS="SL_Chrome,SL_Firefox,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_iOS" + BROWSERS="SL_Chrome,SL_Firefox,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_iOS" fi grunt test:promises-aplus From 7efed006327df7e6f0784b54e856f0a4e87331e5 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 14 Apr 2016 00:07:10 +0200 Subject: [PATCH 100/719] test(input): exclude tests that are only failing on Edge --- test/ng/animateRunnerSpec.js | 2 +- test/ng/directive/inputSpec.js | 96 ++++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/test/ng/animateRunnerSpec.js b/test/ng/animateRunnerSpec.js index cd3ddea7f33d..601d3fba3427 100644 --- a/test/ng/animateRunnerSpec.js +++ b/test/ng/animateRunnerSpec.js @@ -329,7 +329,7 @@ describe('$$AnimateRunner', function() { expect(status).toBe(true); })); - it('should break the chian when a function evaluates to false', + it('should break the chain when a function evaluates to false', inject(function($$rAF, $$AnimateRunner) { var runner1 = new $$AnimateRunner(); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 7014e00624d2..b29fba489fe5 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -5,6 +5,9 @@ describe('input', function() { var helper = {}, $compile, $rootScope, $browser, $sniffer, $timeout, $q; + // UA sniffing to exclude Edge from some date input tests + var isEdge = /\bEdge\//.test(window.navigator.userAgent); + generateInputCompilerHelper(helper); beforeEach(inject(function(_$compile_, _$rootScope_, _$browser_, _$sniffer_, _$timeout_, _$q_) { @@ -688,18 +691,20 @@ describe('input', function() { expect($rootScope.form.alias.$error.month).toBeTruthy(); }); - it('should allow four or more digits in year', function() { - var inputElm = helper.compileInput(''); - helper.changeInputValueTo('10123-03'); - expect(+$rootScope.value).toBe(Date.UTC(10123, 2, 1, 0, 0, 0)); + if (!isEdge) { + it('should allow four or more digits in year', function() { + var inputElm = helper.compileInput(''); - $rootScope.$apply(function() { - $rootScope.value = new Date(Date.UTC(20456, 3, 1, 0, 0, 0)); - }); - expect(inputElm.val()).toBe('20456-04'); - }); + helper.changeInputValueTo('10123-03'); + expect(+$rootScope.value).toBe(Date.UTC(10123, 2, 1, 0, 0, 0)); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(20456, 3, 1, 0, 0, 0)); + }); + expect(inputElm.val()).toBe('20456-04'); + }); + } it('should only change the month of a bound date', function() { var inputElm = helper.compileInput(''); @@ -899,17 +904,19 @@ describe('input', function() { expect(inputElm).toBeValid(); }); - it('should allow four or more digits in year', function() { - var inputElm = helper.compileInput(''); + if (!isEdge) { + it('should allow four or more digits in year', function() { + var inputElm = helper.compileInput(''); - helper.changeInputValueTo('10123-W03'); - expect(+$rootScope.value).toBe(Date.UTC(10123, 0, 21)); + helper.changeInputValueTo('10123-W03'); + expect(+$rootScope.value).toBe(Date.UTC(10123, 0, 21)); - $rootScope.$apply(function() { - $rootScope.value = new Date(Date.UTC(20456, 0, 28)); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(20456, 0, 28)); + }); + expect(inputElm.val()).toBe('20456-W04'); }); - expect(inputElm.val()).toBe('20456-W04'); - }); + } it('should use UTC if specified in the options', function() { var inputElm = helper.compileInput(''); @@ -1195,18 +1202,22 @@ describe('input', function() { expect(+$rootScope.value).toBe(+new Date(2000, 0, 1, 1, 2, 0)); }); - it('should allow four or more digits in year', function() { - var inputElm = helper.compileInput(''); - helper.changeInputValueTo('10123-01-01T01:02'); - expect(+$rootScope.value).toBe(+new Date(10123, 0, 1, 1, 2, 0)); + if (!isEdge) { + it('should allow four or more digits in year', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('10123-01-01T01:02'); + expect(+$rootScope.value).toBe(+new Date(10123, 0, 1, 1, 2, 0)); + + $rootScope.$apply(function() { + $rootScope.value = new Date(20456, 1, 1, 1, 2, 0); + }); + expect(inputElm.val()).toBe('20456-02-01T01:02:00.000'); + } + ); + } - $rootScope.$apply(function() { - $rootScope.value = new Date(20456, 1, 1, 1, 2, 0); - }); - expect(inputElm.val()).toBe('20456-02-01T01:02:00.000'); - } - ); it('should label parse errors as `datetimelocal`', function() { var inputElm = helper.compileInput('', { @@ -1800,19 +1811,20 @@ describe('input', function() { } ); - it('should allow four or more digits in year', function() { - var inputElm = helper.compileInput(''); - - helper.changeInputValueTo('10123-01-01'); - expect(+$rootScope.value).toBe(Date.UTC(10123, 0, 1, 0, 0, 0)); + if (!isEdge) { + it('should allow four or more digits in year', function() { + var inputElm = helper.compileInput(''); - $rootScope.$apply(function() { - $rootScope.value = new Date(Date.UTC(20456, 1, 1, 0, 0, 0)); - }); - expect(inputElm.val()).toBe('20456-02-01'); - } - ); + helper.changeInputValueTo('10123-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(10123, 0, 1, 0, 0, 0)); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(20456, 1, 1, 0, 0, 0)); + }); + expect(inputElm.val()).toBe('20456-02-01'); + } + ); + } it('should label parse errors as `date`', function() { var inputElm = helper.compileInput('', { @@ -4224,10 +4236,14 @@ describe('input', function() { }); expect(inputElm[0].value).toBe(''); - // Support: IE 9-11 + // Support: IE 9-11, Edge // In IE it is not possible to remove the `value` attribute from an input element. - if (!msie) { + if (!msie && !isEdge) { expect(inputElm[0].getAttribute('value')).toBeNull(); + } else { + // Support: IE 9-11, Edge + // This will fail if the Edge bug gets fixed + expect(inputElm[0].getAttribute('value')).toBe('something'); } }); From 69c3faf40589b4ac9e7fa75f3b4fb83349aba60d Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 13 Apr 2017 12:09:41 +0200 Subject: [PATCH 101/719] test(core): expect that Edge cannot auto-bootstrap in extensions --- test/AngularSpec.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 5e9ef2167429..4b6610527c7c 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1755,8 +1755,7 @@ describe('angular', function() { }; } - it('should bootstrap from an extension into an extension document for same-origin documents only', function() { - + describe('from extensions into extension documents', function() { // Extension URLs are browser-specific, so we must choose a scheme that is supported by the browser to make // sure that the URL is properly parsed. var protocol; @@ -1773,10 +1772,29 @@ describe('angular', function() { protocol = 'browserext:'; // Upcoming standard scheme. } - expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something'}, protocol))).toBe(true); - expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something-else'}, protocol))).toBe(false); + + if (protocol === 'ms-browser-extension:') { + // Support: Edge 13-15 + // In Edge, URLs with protocol 'ms-browser-extension:' return "null" for the origin, + // therefore it's impossible to know if a script is same-origin. + it('should not bootstrap for same-origin documents', function() { + expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something'}, protocol))).toBe(false); + }); + + } else { + it('should bootstrap for same-origin documents', function() { + + expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something'}, protocol))).toBe(true); + }); + } + + it('should not bootstrap for cross-origin documents', function() { + expect(allowAutoBootstrap(createFakeDoc({src: protocol + '//something-else'}, protocol))).toBe(false); + }); + }); + it('should bootstrap from a script with no source (e.g. src, href or xlink:href attributes)', function() { expect(allowAutoBootstrap(createFakeDoc({src: null}))).toBe(true); From 080357e906e2ec34e669091ef345fc4442e23ea0 Mon Sep 17 00:00:00 2001 From: Jacob Hansson Date: Fri, 21 Apr 2017 06:39:36 -0500 Subject: [PATCH 102/719] feat(ngMock): describe unflushed http requests The current implementation of $httpBackend.verifyNoOutstandingRequest gives an integer number describing how many requests are unflushed. While it's superficially easy to solve test errors from that message by simply adding an additional $httpBackend.flush(), if a developer is truly not expecting the code to make further requests this is not ideal. This change explicitly prints out which additional requests remain unflushed in the error message, helping her determine if the code needs changing, or if an additional flush is appropriate. Before this change: Unflushed requests: 1 After this change: Unflushed requests: 1 GET /some Closes #10596 Closes #15928 --- src/ngMock/angular-mocks.js | 5 ++++- test/ngMock/angular-mocksSpec.js | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index d6e84c75584f..1d21364da7bb 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1378,6 +1378,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } } + handleResponse.description = method + ' ' + url; return handleResponse; function handleResponse() { @@ -1884,7 +1885,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { $httpBackend.verifyNoOutstandingRequest = function(digest) { if (digest !== false) $rootScope.$digest(); if (responses.length) { - throw new Error('Unflushed requests: ' + responses.length); + var unflushedDescriptions = responses.map(function(res) { return res.description; }); + throw new Error('Unflushed requests: ' + responses.length + '\n ' + + unflushedDescriptions.join('\n ')); } }; diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 441509376561..4bfad9d3bb10 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1678,7 +1678,8 @@ describe('ngMock', function() { expect(function() { hb.verifyNoOutstandingRequest(); - }).toThrowError('Unflushed requests: 1'); + }).toThrowError('Unflushed requests: 1\n' + + ' GET /some'); }); @@ -1690,8 +1691,23 @@ describe('ngMock', function() { expect(function() { hb.verifyNoOutstandingRequest(); - }).toThrowError('Unflushed requests: 1'); + }).toThrowError('Unflushed requests: 1\n' + + ' GET /some'); })); + + + it('should describe multiple unflushed requests', function() { + hb.when('GET').respond(200); + hb.when('PUT').respond(200); + hb('GET', '/some', null, noop, {}); + hb('PUT', '/elsewhere', null, noop, {}); + + expect(function() { + hb.verifyNoOutstandingRequest(); + }).toThrowError('Unflushed requests: 2\n' + + ' GET /some\n' + + ' PUT /elsewhere'); + }); }); From 8d7c7f4a8eed3dbf46ecb277c54b5c0f1eb1958e Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 9 Aug 2016 22:06:55 +0200 Subject: [PATCH 103/719] test(select, ngOptions): add more tests for "required" with "empty" or "unknown" option --- src/ng/directive/ngOptions.js | 1 - test/ng/directive/ngOptionsSpec.js | 127 +++++++++++++++++++---- test/ng/directive/selectSpec.js | 157 ++++++++++++++++++++++------- 3 files changed, 231 insertions(+), 54 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index e82d5e49813a..0d4323487cae 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -704,7 +704,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, ngModelCtrl.$render(); } } - } } diff --git a/test/ng/directive/ngOptionsSpec.js b/test/ng/directive/ngOptionsSpec.js index 7a3c4ba492bf..4aae08cabcf0 100644 --- a/test/ng/directive/ngOptionsSpec.js +++ b/test/ng/directive/ngOptionsSpec.js @@ -2,12 +2,13 @@ describe('ngOptions', function() { - var scope, formElement, element, $compile, linkLog; + var scope, formElement, element, $compile, linkLog, ngModelCtrl; function compile(html) { formElement = jqLite('
' + html + '
'); element = formElement.find('select'); $compile(formElement)(scope); + ngModelCtrl = element.controller('ngModel'); scope.$apply(); } @@ -181,6 +182,7 @@ describe('ngOptions', function() { afterEach(function() { scope.$destroy(); //disables unknown option work during destruction dealoc(formElement); + ngModelCtrl = null; }); function createSelect(attrs, blank, unknown) { @@ -2925,42 +2927,68 @@ describe('ngOptions', function() { }); - describe('ngRequired', function() { + describe('required state', function() { - it('should allow bindings on ngRequired', function() { + it('should set the error if the empty option is selected', function() { + createSelect({ + 'ng-model': 'selection', + 'ng-options': 'item for item in values', + 'required': '' + }, true); + + scope.$apply(function() { + scope.values = ['a', 'b']; + scope.selection = scope.values[0]; + }); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + var options = element.find('option'); + + // view -> model + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + + browserTrigger(options[1], 'click'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + // model -> view + scope.$apply('selection = "unmatched value"'); + expect(options[0]).toBeMarkedAsSelected(); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + }); + + + it('should validate with empty option and bound ngRequired', function() { createSelect({ 'ng-model': 'value', 'ng-options': 'item.name for item in values', 'ng-required': 'required' }, true); - scope.$apply(function() { scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}]; scope.required = false; }); - element.val(''); - browserTrigger(element, 'change'); + var options = element.find('option'); + + browserTrigger(options[0], 'click'); expect(element).toBeValid(); - scope.$apply(function() { - scope.required = true; - }); + scope.$apply('required = true'); expect(element).toBeInvalid(); - scope.$apply(function() { - scope.value = scope.values[0]; - }); + scope.$apply('value = values[0]'); expect(element).toBeValid(); - element.val(''); - browserTrigger(element, 'change'); + browserTrigger(options[0], 'click'); expect(element).toBeInvalid(); - scope.$apply(function() { - scope.required = false; - }); + scope.$apply('required = false'); expect(element).toBeValid(); }); @@ -2989,6 +3017,43 @@ describe('ngOptions', function() { }); + it('should NOT set the error if the empty option is present but required attribute is not', + function() { + scope.$apply(function() { + scope.values = ['a', 'b']; + }); + + createSingleSelect(); + + expect(element).toBeValid(); + expect(element).toBePristine(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + } + ); + + + it('should NOT set the error if the unknown option is selected', function() { + createSelect({ + 'ng-model': 'selection', + 'ng-options': 'item for item in values', + 'required': '' + }); + + scope.$apply(function() { + scope.values = ['a', 'b']; + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply('selection = "c"'); + expect(element).toEqualSelect(['?'], 'string:a', 'string:b'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + }); + + it('should allow falsy values as values', function() { createSelect({ 'ng-model': 'value', @@ -3009,6 +3074,34 @@ describe('ngOptions', function() { expect(element).toBeValid(); expect(scope.value).toBe(false); }); + + + it('should validate after option list was updated', function() { + createSelect({ + 'ng-model': 'selection', + 'ng-options': 'item for item in values', + 'required': '' + }, true); + + scope.$apply(function() { + scope.values = ['A', 'B']; + scope.selection = scope.values[0]; + }); + + expect(element).toEqualSelect('', ['string:A'], 'string:B'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply(function() { + scope.values = ['C', 'D']; + }); + + expect(element).toEqualSelect([''], 'string:C', 'string:D'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + // ngModel sets undefined for invalid values + expect(scope.selection).toBeUndefined(); + }); }); describe('required and empty option', function() { diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 975e4b594d77..1a372f772973 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -7,6 +7,7 @@ describe('select', function() { formElement = jqLite('
' + html + '
'); element = formElement.find('select'); $compile(formElement)(scope); + ngModelCtrl = element.controller('ngModel'); scope.$digest(); } @@ -79,6 +80,7 @@ describe('select', function() { afterEach(function() { scope.$destroy(); //disables unknown option work during destruction dealoc(formElement); + ngModelCtrl = null; }); @@ -190,54 +192,108 @@ describe('select', function() { }); - it('should require', function() { - compile( - ''); + describe('required state', function() { - scope.change = function() { - scope.log += 'change;'; - }; + it('should set the error if the empty option is selected', function() { + compile( + ''); - scope.$apply(function() { - scope.log = ''; - scope.selection = 'c'; + scope.$apply(function() { + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + var options = element.find('option'); + + // view -> model + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + + browserTrigger(options[1], 'click'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + // model -> view + scope.$apply('selection = null'); + options = element.find('option'); + expect(options[0]).toBeMarkedAsSelected(); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); }); - expect(scope.form.select.$error.required).toBeFalsy(); - expect(element).toBeValid(); - expect(element).toBePristine(); - scope.$apply(function() { - scope.selection = ''; + it('should validate with empty option and bound ngRequired', function() { + compile( + ''); + + scope.$apply(function() { + scope.required = false; + }); + + var options = element.find('option'); + + browserTrigger(options[0], 'click'); + expect(element).toBeValid(); + + scope.$apply('required = true'); + expect(element).toBeInvalid(); + + scope.$apply('selection = "a"'); + expect(element).toBeValid(); + expect(element).toEqualSelect('', ['a'], 'b'); + + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + + scope.$apply('required = false'); + expect(element).toBeValid(); }); - expect(scope.form.select.$error.required).toBeTruthy(); - expect(element).toBeInvalid(); - expect(element).toBePristine(); - expect(scope.log).toEqual(''); - element[0].value = 'c'; - browserTrigger(element, 'change'); - expect(element).toBeValid(); - expect(element).toBeDirty(); - expect(scope.log).toEqual('change;'); - }); + it('should not be invalid if no required attribute is present', function() { + compile( + ''); + expect(element).toBeValid(); + expect(element).toBePristine(); + }); - it('should not be invalid if no require', function() { - compile( - ''); - expect(element).toBeValid(); - expect(element).toBePristine(); - }); + it('should NOT set the error if the unknown option is selected', function() { + compile( + ''); + scope.$apply(function() { + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply('selection = "c"'); + expect(element).toEqualSelect([unknownValue('c')], 'a', 'b'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + }); + + }); it('should work with repeated value options', function() { scope.robots = ['c3p0', 'r2d2']; @@ -2358,6 +2414,35 @@ describe('select', function() { expect(previouslySelectedOptionElement).not.toBe(optionElements[0]); }); + + it('should validate when the options change', function() { + scope.values = ['A', 'B']; + scope.selection = 'A'; + + compile( + '' + ); + + expect(element).toEqualSelect('', ['A'], 'B'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply(function() { + // Only when new objects are used, ngRepeat re-creates the element from scratch + scope.values = ['B', 'C']; + }); + + expect(element).toEqualSelect([''], 'B', 'C'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + // ngModel sets undefined for invalid values + expect(scope.selection).toBeUndefined(); + }); + + }); From 5878f07474755cb3df1e727cef4e7e4716f44783 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 18 Apr 2017 15:24:33 +0200 Subject: [PATCH 104/719] fix(ngOptions): select unknown option if unmatched model does not match empty option When a regular / ngOptions select has an explicit *empty* option, this option can be selected by the user and will set the model to `null`. It is also selected when the model is set to `null` or `undefined`. When the model is set to a value that does not match any option value, and is also not `null` or `undefined`, the *unknown* option is inserted and selected - this is an explicit marker that the select is in an invalid / unknown state, which is different from an allowed empty state. Previously, regular selects followed this logic, whereas ngOptions selects selected the empty option in the case described above. This patch makes the behavior consistent between regular / ngOptions select - the latter will now insert and select the unknown option. The order of the options has been fixed to unknown -> empty -> actual options. --- src/ng/directive/ngOptions.js | 10 ++++-- src/ng/directive/select.js | 12 ++++--- test/helpers/matchers.js | 21 ++++++++++++ test/ng/directive/ngOptionsSpec.js | 51 ++++++++++++++++++++++++++-- test/ng/directive/selectSpec.js | 54 +++++++++++++++--------------- 5 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index 0d4323487cae..97b4507fd871 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -473,7 +473,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, option.element.setAttribute('selected', 'selected'); } else { - if (providedEmptyOption) { + if (value == null && providedEmptyOption) { + selectCtrl.removeUnknownOption(); selectCtrl.selectEmptyOption(); } else if (selectCtrl.unknownOption.parent().length) { selectCtrl.updateUnknownOption(value); @@ -657,7 +658,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // Ensure that the empty option is always there if it was explicitly provided if (providedEmptyOption) { - selectElement.prepend(selectCtrl.emptyOption); + + if (selectCtrl.unknownOption.parent().length) { + selectCtrl.unknownOption.after(selectCtrl.emptyOption); + } else { + selectElement.prepend(selectCtrl.emptyOption); + } } options.items.forEach(function addOption(option) { diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index dc828764d3da..1f9d0cba0e7f 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -44,11 +44,13 @@ var SelectController = // to create it in ' + + '' + '' + '' + ''); @@ -411,24 +390,45 @@ describe('select', function() { scope.$digest(); options = element.find('option'); - expect(options.length).toBe(2); - expect(options[0]).toBeMarkedAsSelected(); - expect(options[1]).not.toBeMarkedAsSelected(); + expect(options.length).toBe(3); + expect(options[0]).not.toBeMarkedAsSelected(); + expect(options[1]).toBeMarkedAsSelected(); + expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'b'; scope.$digest(); options = element.find('option'); expect(options[0]).not.toBeMarkedAsSelected(); - expect(options[1]).toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).toBeMarkedAsSelected(); - scope.selected = 'no match'; + // This will select the empty option + scope.selected = null; scope.$digest(); + expect(options[0]).toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).not.toBeMarkedAsSelected(); + + // This will add and select the unknown option + scope.selected = 'unmatched value'; + scope.$digest(); options = element.find('option'); + expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); + expect(options[3]).not.toBeMarkedAsSelected(); + + // Back to matched value + scope.selected = 'b'; + scope.$digest(); + options = element.find('option'); + + expect(options[0]).not.toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).toBeMarkedAsSelected(); }); describe('empty option', function() { From 4b06637f703d2a94baedfda64a8e3ac8eea26403 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 18 Apr 2017 15:16:15 +0200 Subject: [PATCH 105/719] chore(matchers): improve output for toBeMarkedAsSelected --- test/helpers/matchers.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js index c543bbb965aa..ac297609e579 100644 --- a/test/helpers/matchers.js +++ b/test/helpers/matchers.js @@ -365,13 +365,15 @@ beforeEach(function() { return { compare: function(actual) { var errors = []; + var optionVal = toJson(actual.value); + if (actual.selected === null || typeof actual.selected === 'undefined' || actual.selected === false) { - errors.push('Expected option property "selected" to be truthy'); + errors.push('Expected option with value ' + optionVal + ' to have property "selected" set to truthy'); } // Support: IE 9 only if (msie !== 9 && actual.hasAttribute('selected') === false) { - errors.push('Expected option to have attribute "selected"'); + errors.push('Expected option with value ' + optionVal + ' to have attribute "selected"'); } var result = { @@ -383,13 +385,15 @@ beforeEach(function() { }, negativeCompare: function(actual) { var errors = []; + var optionVal = toJson(actual.value); + if (actual.selected) { - errors.push('Expected option property "selected" to be falsy'); + errors.push('Expected option with value ' + optionVal + ' property "selected" to be falsy'); } // Support: IE 9 only if (msie !== 9 && actual.hasAttribute('selected')) { - errors.push('Expected option not to have attribute "selected"'); + errors.push('Expected option with value ' + optionVal + ' not to have attribute "selected"'); } var result = { From ff0e61166d3dca59351e3913e0360c24d1bce99c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 18 Apr 2017 19:59:42 +0200 Subject: [PATCH 106/719] refactor(select, ngOptions): extract common methods; make consistent --- src/ng/directive/ngOptions.js | 15 +++------------ src/ng/directive/select.js | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index 97b4507fd871..eecc6ccaa8c6 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -449,12 +449,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, if (!multiple) { selectCtrl.writeValue = function writeNgOptionsValue(value) { - var selectedOption = options.selectValueMap[selectElement.val()]; + var selectedOption = selectElement[0].options[selectElement[0].selectedIndex]; var option = options.getOptionFromViewValue(value); // Make sure to remove the selected attribute from the previously selected option // Otherwise, screen readers might get confused - if (selectedOption) selectedOption.element.removeAttribute('selected'); + if (selectedOption) selectedOption.removeAttribute('selected'); if (option) { // Don't update the option when it is already selected. @@ -464,7 +464,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, if (selectElement[0].value !== option.selectValue) { selectCtrl.removeUnknownOption(); - selectCtrl.unselectEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; @@ -472,15 +471,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, option.element.setAttribute('selected', 'selected'); } else { - - if (value == null && providedEmptyOption) { - selectCtrl.removeUnknownOption(); - selectCtrl.selectEmptyOption(); - } else if (selectCtrl.unknownOption.parent().length) { - selectCtrl.updateUnknownOption(value); - } else { - selectCtrl.renderUnknownOption(value); - } + selectCtrl.selectUnknownOrEmptyOption(value); } }; diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 1f9d0cba0e7f..d8a19605f96d 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -86,7 +86,7 @@ var SelectController = self.unselectEmptyOption = function() { if (self.hasEmptyOption) { - self.emptyOption.removeAttr('selected'); + setOptionSelectedStatus(self.emptyOption, false); } }; @@ -128,14 +128,7 @@ var SelectController = var selectedOption = $element[0].options[$element[0].selectedIndex]; setOptionSelectedStatus(jqLite(selectedOption), true); } else { - if (value == null && self.emptyOption) { - self.removeUnknownOption(); - self.selectEmptyOption(); - } else if (self.unknownOption.parent().length) { - self.updateUnknownOption(value); - } else { - self.renderUnknownOption(value); - } + self.selectUnknownOrEmptyOption(value); } }; @@ -178,6 +171,16 @@ var SelectController = return !!optionsMap.get(value); }; + self.selectUnknownOrEmptyOption = function(value) { + if (value == null && self.emptyOption) { + self.removeUnknownOption(); + self.selectEmptyOption(); + } else if (self.unknownOption.parent().length) { + self.updateUnknownOption(value); + } else { + self.renderUnknownOption(value); + } + }; var renderScheduled = false; function scheduleRender() { From e4c2fe6d427cb1540977520f7e31a7e7a30acfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Apr 2017 13:19:53 +0200 Subject: [PATCH 107/719] refactor(*): remove workarounds for IE <9, update IE/Edge-related comments --- .github/ISSUE_TEMPLATE.md | 4 +- .../ngdoc/api/directive.template.html | 3 -- docs/content/guide/bootstrap.ngdoc | 4 +- docs/content/guide/ie.ngdoc | 8 ++-- docs/content/misc/faq.ngdoc | 6 +-- src/Angular.js | 10 ++-- src/ng/directive/ngOptions.js | 6 ++- src/ng/directive/select.js | 8 ++-- src/ngSanitize/sanitize.js | 16 ++----- test/AngularSpec.js | 1 + test/helpers/privateMocks.js | 4 +- test/helpers/testabilityPatch.js | 21 +------- test/jqLiteSpec.js | 2 +- test/minErrSpec.js | 1 + test/ng/compileSpec.js | 48 ++++++++----------- test/ng/directive/booleanAttrsSpec.js | 2 +- test/ng/locationSpec.js | 28 ++--------- test/ng/urlUtilsSpec.js | 2 +- 18 files changed, 57 insertions(+), 117 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 82e6b016d1cb..5e11e611ee8e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -29,8 +29,8 @@ https://plnkr.co or similar (you can use this template as a starting point: http **Angular version:** 1.x.y -**Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] +**Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] **Anything else:** - \ No newline at end of file + diff --git a/docs/config/templates/ngdoc/api/directive.template.html b/docs/config/templates/ngdoc/api/directive.template.html index 7e14ce0c6411..b30bdb0451f2 100644 --- a/docs/config/templates/ngdoc/api/directive.template.html +++ b/docs/config/templates/ngdoc/api/directive.template.html @@ -18,9 +18,6 @@

Usage