From cc788d5853a96ae00d5abd7a52c79eb135c9b0f7 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] fix($compile): remove the preAssignBindingsEnabled flag 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); - }); - }); - } - }); + } });