From b7a01a7cdf916971458512111e1cfa45bf63e405 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 5 Sep 2016 21:58:49 +0100 Subject: [PATCH 1/3] feat($compile): add `preAssignBindings` flag A new flag 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 enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See #14580 --- src/ng/compile.js | 64 +- test/ng/compileSpec.js | 10425 ++++++++++++++++++++------------------- 2 files changed, 5308 insertions(+), 5181 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 30f40539a9bc..850b608d1618 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1369,6 +1369,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; + /** + * @ngdoc method + * @name $compileProvider#preAssignBindings + * + * @param {boolean=} enabled update the preAssignBindings state if provided, otherwise just return the + * current preAssignBindings 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 Angular 1.5.x but will switch to false in Angular 1.6.x. + */ + var preAssignBindings = true; + this.preAssignBindings = function(enabled) { + if (isDefined(enabled)) { + preAssignBindings = enabled; + return this; + } + return preAssignBindings; + }; + var TTL = 10; /** @@ -2679,22 +2708,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } else { - controller.bindingInfo = {}; - } + if (preAssignBindings) { + if (controller.identifier && 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(); + 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); } diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 72015a3559ad..a0552427264a 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3845,6051 +3845,6142 @@ describe('$compile', function() { }); }); - describe('controller lifecycle hooks', function() { - - describe('$onInit', function() { + forEach([true, false], function(preAssignBindings) { + describe((preAssignBindings ? 'with' : 'without') + ' pre-assigned bindings', function() { + beforeEach(module(function($compileProvider) { + $compileProvider.preAssignBindings(preAssignBindings); + })); - it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() { + describe('controller lifecycle hooks', function() { - function check() { - expect(this.element.controller('d1').id).toEqual(1); - expect(this.element.controller('d2').id).toEqual(2); - } + describe('$onInit', function() { - function Controller1($element) { this.id = 1; this.element = $element; } - Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); + it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() { - function Controller2($element) { this.id = 2; this.element = $element; } - Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); + function check() { + expect(this.element.controller('d1').id).toEqual(1); + expect(this.element.controller('d2').id).toEqual(2); + } - angular.module('my', []) - .directive('d1', valueFn({ controller: Controller1 })) - .directive('d2', valueFn({ controller: Controller2 })); + function Controller1($element) { this.id = 1; this.element = $element; } + Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce(); - expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce(); - }); - }); + function Controller2($element) { this.id = 2; this.element = $element; } + Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); - 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', []) + .directive('d1', valueFn({ controller: Controller1 })) + .directive('d2', valueFn({ controller: Controller2 })); - 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) { + element = $compile('
')($rootScope); + expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce(); + expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce(); + }); }); - module('my'); - inject(function($compile, $rootScope, $exceptionHandler, $log) { - - // 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 second component's hook should still be called - expect($log.info.logs.pop()).toEqual(['onInit']); - }); - }); - }); - - - describe('$onDestroy', 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++; }; + 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', []) - .directive('d1', valueFn({ scope: true, controller: TestController })) - .directive('d2', valueFn({ scope: {}, controller: TestController })) - .directive('d3', valueFn({ controller: TestController })); + 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) { + module('my'); + inject(function($compile, $rootScope, $exceptionHandler, $log) { - element = $compile('
')($rootScope); + // Setup the directive with bindings that will keep updating the bound value forever + 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'); + // The first component's error should be logged + expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); - 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]); + // The second component's hook should still be called + expect($log.info.logs.pop()).toEqual(['onInit']); + }); + }); }); - }); - - - 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 })); - module('my'); - inject(function($compile, $rootScope) { + describe('$onDestroy', function() { - 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']); - }); - }); - }); + 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++; }; - describe('$postLink', function() { + angular.module('my', []) + .directive('d1', valueFn({ scope: true, controller: TestController })) + .directive('d2', valueFn({ scope: {}, controller: TestController })) + .directive('d3', valueFn({ controller: TestController })); - it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() { + module('my'); + inject(function($compile, $rootScope) { - var log = []; + element = $compile('
')($rootScope); - function Controller1() { } - Controller1.prototype.$postLink = function() { log.push('d1 view init'); }; + $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'); - function Controller2() { } - Controller2.prototype.$postLink = function() { log.push('d2 view init'); }; + 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]); + }); + }); - 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' - ]); - }); - }); - }); + 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'); }; - describe('$doCheck', function() { - it('should call `$doCheck`, if provided, for each digest cycle, after $onChanges and $onInit', function() { - var log = []; + angular.module('my', []) + .directive('parent', valueFn({ scope: true, controller: ParentController })) + .directive('child', valueFn({ scope: true, controller: ChildController })) + .directive('grandChild', valueFn({ scope: true, controller: GrandChildController })); - function TestController() { } - TestController.prototype.$doCheck = function() { log.push('$doCheck'); }; - TestController.prototype.$onChanges = function() { log.push('$onChanges'); }; - TestController.prototype.$onInit = function() { log.push('$onInit'); }; + module('my'); + inject(function($compile, $rootScope) { - angular.module('my', []) - .component('dcc', { - controller: TestController, - bindings: { 'prop1': '<' } + 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']); + }); }); + }); - module('my'); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - expect(log).toEqual([ - '$onChanges', - '$onInit', - '$doCheck' - ]); - // Clear log - log = []; + describe('$postLink', function() { - $rootScope.$apply(); - expect(log).toEqual([ - '$doCheck', - '$doCheck' - ]); + it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() { - // Clear log - log = []; + var log = []; - $rootScope.$apply('val = 2'); - expect(log).toEqual([ - '$doCheck', - '$onChanges', - '$doCheck' - ]); - }); - }); + function Controller1() { } + Controller1.prototype.$postLink = function() { log.push('d1 view init'); }; - it('should work if $doCheck is provided in the constructor', function() { - var log = []; + function Controller2() { } + Controller2.prototype.$postLink = function() { log.push('d2 view init'); }; - function TestController() { - this.$doCheck = function() { log.push('$doCheck'); }; - this.$onChanges = function() { log.push('$onChanges'); }; - this.$onInit = function() { log.push('$onInit'); }; - } + 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', []) - .component('dcc', { - controller: TestController, - bindings: { 'prop1': '<' } + 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([ - '$onChanges', - '$onInit', - '$doCheck' - ]); + describe('$doCheck', function() { + it('should call `$doCheck`, if provided, for each digest cycle, after $onChanges and $onInit', function() { + var log = []; - // Clear log - log = []; + function TestController() { } + TestController.prototype.$doCheck = function() { log.push('$doCheck'); }; + TestController.prototype.$onChanges = function() { log.push('$onChanges'); }; + TestController.prototype.$onInit = function() { log.push('$onInit'); }; - $rootScope.$apply(); - expect(log).toEqual([ - '$doCheck', - '$doCheck' - ]); + angular.module('my', []) + .component('dcc', { + controller: TestController, + bindings: { 'prop1': '<' } + }); - // Clear log - log = []; + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + '$onChanges', + '$onInit', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply(); + expect(log).toEqual([ + '$doCheck', + '$doCheck' + ]); + + // 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 = []; - describe('$onChanges', function() { + function TestController() { + this.$doCheck = function() { log.push('$doCheck'); }; + this.$onChanges = function() { log.push('$onChanges'); }; + this.$onInit = function() { log.push('$onInit'); }; + } - 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); }; + angular.module('my', []) + .component('dcc', { + controller: TestController, + bindings: { 'prop1': '<' } + }); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' } + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + '$onChanges', + '$onInit', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply(); + expect(log).toEqual([ + '$doCheck', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply('val = 2'); + expect(log).toEqual([ + '$doCheck', + '$onChanges', + '$doCheck' + ]); + }); }); + }); - 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: ''}) - } - ]); + describe('$onChanges', function() { - // Clear the initial changes from the 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); }; - // Update val to trigger the onChanges - $rootScope.$apply('val = 42'); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' } + }); - // 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'}) - } - ]); - }); - }); + 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: ''}) + } + ]); + // Clear the initial changes from 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 val to trigger the onChanges + $rootScope.$apply('val = 42'); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop1': '<' } + // 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'}) + } + ]); + }); }); - 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 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': '<' } + 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); - // 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 enfore - $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 enfore + $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) { - 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); }; - } + $rootScope.$apply('a = 7'); + element = $compile('')($rootScope); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: { 'prop': '<', attr: '@' } + 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); + }); }); - 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'}) + 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[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); - }); - }); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: { 'prop': '<', attr: '@' } + }); - 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]); }; + module('my'); + inject(function($compile, $rootScope) { + $rootScope.$apply('a = 7'); + element = $compile('')($rootScope); - angular.module('my', []) - .component('c1', { - controller: TestController1, - bindings: {'prop': '<'} - }) - .component('c2', { - controller: TestController2, - bindings: {'prop': '<'} + 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); + }); }); - module('my'); - inject(function($compile, $rootScope) { - - // Create a watcher to count the number of digest cycles - var watchCount = 0; - $rootScope.$watch(function() { watchCount++; }); - // Setup two sibling components with bindings that will change - element = $compile('
')($rootScope); + 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]); }; - // Clear out initial changes - log = []; + angular.module('my', []) + .component('c1', { + controller: TestController1, + bindings: {'prop': '<'} + }) + .component('c2', { + controller: TestController2, + bindings: {'prop': '<'} + }); - // Update val to trigger the onChanges - $rootScope.$apply('val1 = 42; val2 = 17'); + module('my'); + inject(function($compile, $rootScope) { - 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); - }); - }); + // Create a watcher to count the number of digest cycles + var watchCount = 0; + $rootScope.$watch(function() { watchCount++; }); + // Setup two sibling components with bindings that will change + element = $compile('
')($rootScope); - it('should cope with changes occuring inside `$onChanges()` hooks', function() { - var log = []; - function OuterController() { - this.prop1 = 0; - } - OuterController.prototype.$onChanges = function(change) { - log.push(['OuterController', change]); - // Make a change to the inner component - this.b = this.prop1 * 2; - }; + // Clear out initial changes + log = []; - function InnerController() { } - InnerController.prototype.$onChanges = function(change) { log.push(['InnerController', change]); }; + // Update val to trigger the onChanges + $rootScope.$apply('val1 = 42; val2 = 17'); - angular.module('my', []) - .component('outer', { - controller: OuterController, - bindings: {'prop1': '<'}, - template: '' - }) - .component('inner', { - controller: InnerController, - bindings: {'prop2': '<'} + 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); + }); }); - module('my'); - inject(function($compile, $rootScope) { - // Setup the directive with two bindings - element = $compile('')($rootScope); + it('should cope with changes occuring inside `$onChanges()` hooks', function() { + var log = []; + function OuterController() {} - // Clear out initial changes - log = []; + 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({currentValue: 42})}], - ['InnerController', {prop2: jasmine.objectContaining({currentValue: 84})}] - ]); - }); - }); + angular.module('my', []) + .component('outer', { + controller: OuterController, + bindings: {'prop1': '<'}, + template: '' + }) + .component('inner', { + controller: InnerController, + bindings: {'prop2': '<'} + }); + module('my'); + inject(function($compile, $rootScope) { - it('should throw an error if `$onChanges()` hooks are not stable', function() { - function TestController() {} - TestController.prototype.$onChanges = function(change) { - this.onChange(); - }; + // Setup the directive with two bindings + element = $compile('')($rootScope); - angular.module('my', []) - .component('c1', { - controller: TestController, - bindings: {'prop': '<', onChange: '&'} + // Clear out initial changes + log = []; + + // Update val to trigger the onChanges + $rootScope.$apply('a = 42'); + + expect(log).toEqual([ + ['OuterController', {prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 42})}], + ['InnerController', {prop2: jasmine.objectContaining({previousValue: NaN, currentValue: NaN})}], + ['InnerController', {prop2: jasmine.objectContaining({previousValue: NaN, currentValue: 84})}] + ]); + }); }); - module('my'); - inject(function($compile, $rootScope) { - // Setup the directive with bindings that will keep updating the bound value forever - element = $compile('')($rootScope); + it('should throw 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 - expect(function() { - $rootScope.$apply('a = 42'); - }).toThrowMinErr('$compile', 'infchng'); + angular.module('my', []) + .component('c1', { + controller: TestController, + bindings: {'prop': '<', onChange: '&'} + }); - dealoc(element); - element = $compile('')($rootScope); - $rootScope.$apply('b = 24'); - $rootScope.$apply('b = 48'); - }); - }); + module('my'); + inject(function($compile, $rootScope) { + // 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 + expect(function() { + $rootScope.$apply('a = 42'); + }).toThrowMinErr('$compile', 'infchng'); - 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'); + 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].toString()).toContain('[$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].toString()).toContain('[$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']); + }); }); - 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 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) { + + // 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; + describe('isolated locals', function() { + var componentScope, regularScope; - beforeEach(module(function() { - directive('myComponent', function() { - return { - scope: { - attr: '@', - attrAlias: '@attr', - ref: '=', - refAlias: '= ref', - reference: '=', - optref: '=?', - optrefAlias: '=? optref', - optreference: '=?', - colref: '=*', - colrefAlias: '=* colref', - 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}}' - }; - }); - }); + 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('
'); + inject(function($rootScope) { + compile('
'); - $rootScope.$apply(function() { - $rootScope.value = 'from-parent'; - }); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; + }); - expect(element.html()).toBe('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('
'); - - $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 regular child elements', function() { - inject(function($rootScope) { - compile('
value: {{value}}
'); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; + }); - $rootScope.$apply(function() { - $rootScope.value = 'from-parent'; + expect(element.html()).toBe('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}}
'); - 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(); + $rootScope.$apply(function() { + $rootScope.value = 'from-parent'; + }); - isolateScope.$apply(function(scope) { scope.reference = 64; }); - expect($rootScope.num).toBe(64); - })); + expect(element.html()).toBe('value: from-parent'); + }); + }); - 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 parent 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); - })); + isolateScope.$apply(function(scope) { scope.reference = 64; }); + expect($rootScope.num).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': '&', + 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(); - // Spidermonkey extension, may be obsolete in the future - 'watch': '=' - } + $rootScope.$apply(function(scope) { scope.num = 64; }); + expect(isolateScope.reference).toBe(64); })); - }); - 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': '&', - 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(); - })); + // 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 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 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 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 is not passed', inject(function($compile) { + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.optExpr).toBeUndefined(); + })); - 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 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 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: '

' + 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!'); })); - }); - 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 ignore optional "="-bound property if value is the emptry 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: '

' + 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!'); })); - }); - 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'); - }); - }); - describe('bind-once', function() { - - 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 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'); - 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}}' - }; + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('default'); }); }); - 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:foo;4:'); - expect(countWatches($rootScope)).toEqual(4); + it('should ignore optional "="-bound property if value is the emptry 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 = '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:foo;4:bar'); - }); - }); + describe('bind-once', function() { - 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}}' - }; - }); - }); + 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; + } - 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 + 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}}' + }; + }); + }); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + 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 = 'baz'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:bar;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.bar = 'baz'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:baz;3:;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); - 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' - }; + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); + }); }); - }); - 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 -> '=' + 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}}' + }; + }); + }); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:foo;4:'); - expect(countWatches($rootScope)).toEqual(4); + 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 = '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 = 'foo'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:;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.foo = 'baz'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:bar;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - 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' - }; + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:;4:'); + }); }); - }); - 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 -> {{ }} + 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' + }; + }); + }); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + 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 = 'baz'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:bar;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.bar = 'baz'; - $rootScope.$digest(); - expect(element.html()).toBe('1:foo;2:baz;3:;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); - 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!'; }; - } - }; + $rootScope.bar = 'baz'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); + }); }); - }); - - inject(function($rootScope) { - compile('
' + - '

{{greeting}}

' + - '
' + - '
'); - $rootScope.$digest(); - browserTrigger(element.find('button'), 'click'); - expect(element.find('p').text()).toBe('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, $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 -> {{ }} - describe('attribute', function() { - it('should copy simple attribute', inject(function() { - compile('
'); + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - expect(componentScope.attr).toEqual('some text'); - expect(componentScope.attrAlias).toEqual('some text'); - expect(componentScope.attrAlias).toEqual(componentScope.attr); - })); + $rootScope.foo = 'baz'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.html()).toBe('1:foo;2:bar;3:;4:'); + expect(countWatches($rootScope)).toEqual(3); - it('should copy an attribute with spaces', 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(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 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'); - })); + inject(function($rootScope) { + compile('
' + + '

{{greeting}}

' + + '
' + + '
'); + $rootScope.$digest(); + browserTrigger(element.find('button'), 'click'); + expect(element.find('p').text()).toBe('Hello!'); + }); + }); - it('should update when interpolated attribute updates', inject(function() { - compile('
'); + }); - $rootScope.name = 'igor'; - $rootScope.$apply(); - expect(componentScope.attr).toEqual('hello igor'); - expect(componentScope.attrAlias).toEqual('hello igor'); - })); - }); + describe('attribute', function() { + it('should copy simple attribute', inject(function() { + compile('
'); + expect(componentScope.attr).toEqual('some text'); + expect(componentScope.attrAlias).toEqual('some 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); + it('should copy an attribute with spaces', inject(function() { + compile('
'); - $rootScope.name = 'misko'; - $rootScope.$apply(); + expect(componentScope.attr).toEqual(' some text '); + expect(componentScope.attrAlias).toEqual(' some text '); + expect(componentScope.attrAlias).toEqual(componentScope.attr); + })); - expect($rootScope.name).toBe('misko'); - expect(componentScope.ref).toBe('misko'); - expect(componentScope.refAlias).toBe('misko'); + 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'); + })); - $rootScope.name = {}; - $rootScope.$apply(); - expect(componentScope.ref).toBe($rootScope.name); - expect(componentScope.refAlias).toBe($rootScope.name); - })); + it('should update when interpolated attribute updates', inject(function() { + compile('
'); + $rootScope.name = 'igor'; + $rootScope.$apply(); - it('should update local when both change', inject(function() { - compile('
'); - $rootScope.name = {mark:123}; - componentScope.ref = 'misko'; + expect(componentScope.attr).toEqual('hello igor'); + expect(componentScope.attrAlias).toEqual('hello igor'); + })); + }); - $rootScope.$apply(); - expect($rootScope.name).toEqual({mark:123}); - expect(componentScope.ref).toBe($rootScope.name); - expect(componentScope.refAlias).toBe($rootScope.name); - $rootScope.name = 'igor'; - componentScope.ref = {}; - $rootScope.$apply(); - expect($rootScope.name).toEqual('igor'); - expect(componentScope.ref).toBe($rootScope.name); - expect(componentScope.refAlias).toBe($rootScope.name); - })); + describe('object reference', function() { + it('should update local when origin changes', inject(function() { + compile('
'); + expect(componentScope.ref).toBeUndefined(); + expect(componentScope.refAlias).toBe(componentScope.ref); - it('should not break if local and origin both change to the same value', inject(function() { - $rootScope.name = 'aaa'; + $rootScope.name = 'misko'; + $rootScope.$apply(); - compile('
'); + expect($rootScope.name).toBe('misko'); + expect(componentScope.ref).toBe('misko'); + expect(componentScope.refAlias).toBe('misko'); - //change both sides to the same item within the same digest cycle - componentScope.ref = 'same'; - $rootScope.name = 'same'; - $rootScope.$apply(); + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.ref).toBe($rootScope.name); + expect(componentScope.refAlias).toBe($rootScope.name); + })); - //change origin back to its previous value - $rootScope.name = 'aaa'; - $rootScope.$apply(); - expect($rootScope.name).toBe('aaa'); - expect(componentScope.ref).toBe('aaa'); - })); + it('should update local when both change', inject(function() { + compile('
'); + $rootScope.name = {mark:123}; + componentScope.ref = 'misko'; - it('should complain on non assignable changes', inject(function() { - compile('
'); - $rootScope.name = 'world'; - $rootScope.$apply(); - expect(componentScope.ref).toBe('hello world'); + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.ref).toBe($rootScope.name); + expect(componentScope.refAlias).toBe($rootScope.name); - 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; + $rootScope.name = 'igor'; + componentScope.ref = {}; + $rootScope.$apply(); + expect($rootScope.name).toEqual('igor'); + expect(componentScope.ref).toBe($rootScope.name); + expect(componentScope.refAlias).toBe($rootScope.name); + })); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.ref).toBe('hello misko'); - })); + it('should not break if local and origin both change to the same value', inject(function() { + $rootScope.name = 'aaa'; - it('should complain if assigning to undefined', inject(function() { - compile('
'); - $rootScope.$apply(); - expect(componentScope.ref).toBeUndefined(); + compile('
'); - 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(); + //change both sides to the same item within the same digest cycle + componentScope.ref = 'same'; + $rootScope.name = 'same'; + $rootScope.$apply(); - $rootScope.$$phase = null; // reset since the exception was rethrown which prevented phase clearing - $rootScope.$apply(); - expect(componentScope.ref).toBeUndefined(); - })); + //change origin back to its previous value + $rootScope.name = 'aaa'; + $rootScope.$apply(); - // regression - it('should stabilize model', inject(function() { - compile('
'); + expect($rootScope.name).toBe('aaa'); + expect(componentScope.ref).toBe('aaa'); + })); - var lastRefValueInParent; - $rootScope.$watch('name', function(ref) { - lastRefValueInParent = ref; - }); + it('should complain on non assignable changes', inject(function() { + compile('
'); + $rootScope.name = 'world'; + $rootScope.$apply(); + expect(componentScope.ref).toBe('hello world'); - $rootScope.name = 'aaa'; - $rootScope.$apply(); + 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; - componentScope.reference = 'new'; - $rootScope.$apply(); - - expect(lastRefValueInParent).toBe('new'); - })); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.ref).toBe('hello misko'); + })); - describe('literal objects', function() { - it('should copy parent changes', inject(function() { - compile('
'); + it('should complain if assigning to undefined', inject(function() { + compile('
'); + $rootScope.$apply(); + expect(componentScope.ref).toBeUndefined(); - $rootScope.name = 'a'; - $rootScope.$apply(); - expect(componentScope.reference).toEqual({name: 'a'}); + 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(); - $rootScope.name = 'b'; - $rootScope.$apply(); - expect(componentScope.reference).toEqual({name: 'b'}); - })); + $rootScope.$$phase = null; // reset since the exception was rethrown which prevented phase clearing + $rootScope.$apply(); + expect(componentScope.ref).toBeUndefined(); + })); - it('should not change the component when parent does not change', inject(function() { - compile('
'); + // regression + it('should stabilize model', inject(function() { + compile('
'); - $rootScope.name = 'a'; - $rootScope.$apply(); - var lastComponentValue = componentScope.reference; - $rootScope.$apply(); - expect(componentScope.reference).toBe(lastComponentValue); - })); + var lastRefValueInParent; + $rootScope.$watch('name', function(ref) { + lastRefValueInParent = ref; + }); - it('should complain when the component changes', inject(function() { - compile('
'); + $rootScope.name = 'aaa'; + $rootScope.$apply(); - $rootScope.name = 'a'; - $rootScope.$apply(); - componentScope.reference = {name: 'b'}; - expect(function() { + componentScope.reference = 'new'; $rootScope.$apply(); - }).toThrowMinErr('$compile', 'nonassign', 'Expression \'{name: name}\' in attribute \'reference\' used with directive \'myComponent\' is non-assignable!'); - })); + expect(lastRefValueInParent).toBe('new'); + })); - it('should work for primitive literals', inject(function() { - test('1', 1); - test('null', null); - test('undefined', undefined); - test('\'someString\'', 'someString'); - test('true', true); + describe('literal objects', function() { + it('should copy parent changes', inject(function() { + compile('
'); - function test(literalString, literalValue) { - compile('
'); + $rootScope.name = 'a'; + $rootScope.$apply(); + expect(componentScope.reference).toEqual({name: 'a'}); - $rootScope.$apply(); - expect(componentScope.reference).toBe(literalValue); - dealoc(element); - } - })); + $rootScope.name = 'b'; + $rootScope.$apply(); + expect(componentScope.reference).toEqual({name: 'b'}); + })); - }); + it('should not change the component when parent does not change', inject(function() { + compile('
'); - }); + $rootScope.name = 'a'; + $rootScope.$apply(); + var lastComponentValue = componentScope.reference; + $rootScope.$apply(); + expect(componentScope.reference).toBe(lastComponentValue); + })); + it('should complain when the component changes', inject(function() { + compile('
'); - 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); + $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 = 'misko'; - $rootScope.$apply(); - expect(componentScope.optref).toBe($rootScope.name); - expect(componentScope.optrefAlias).toBe($rootScope.name); + })); - $rootScope.name = {}; - $rootScope.$apply(); - expect(componentScope.optref).toBe($rootScope.name); - expect(componentScope.optrefAlias).toBe($rootScope.name); - })); + 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 throw exception when reference does not exist', inject(function() { - compile('
'); + function test(literalString, literalValue) { + compile('
'); - expect(componentScope.optref).toBeUndefined(); - expect(componentScope.optrefAlias).toBeUndefined(); - expect(componentScope.optreference).toBeUndefined(); - })); - }); + $rootScope.$apply(); + expect(componentScope.reference).toBe(literalValue); + dealoc(element); + } + })); + }); - 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('
'); - expect(componentScope.colref).toEqual($rootScope.collection); - expect(componentScope.colrefAlias).toEqual(componentScope.colref); + 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); - $rootScope.query = 'Gab'; - $rootScope.$apply(); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); - expect(componentScope.colref).toEqual([$rootScope.collection[0]]); - expect(componentScope.colrefAlias).toEqual([$rootScope.collection[0]]); - })); + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + })); - it('should update origin scope when isolate scope changes', inject(function() { - $rootScope.collection = [{ - name: 'Gabriel', - value: 18 - }, { - name: 'Tony', - value: 91 - }]; + it('should not throw exception when reference does not exist', inject(function() { + compile('
'); - compile('
'); + expect(componentScope.optref).toBeUndefined(); + expect(componentScope.optrefAlias).toBeUndefined(); + expect(componentScope.optreference).toBeUndefined(); + })); + }); - var newItem = { - name: 'Pablo', - value: 10 - }; - componentScope.colref.push(newItem); - componentScope.$apply(); - expect($rootScope.collection[2]).toEqual(newItem); - })); - }); + 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('
'); - describe('one-way binding', function() { - it('should update isolate when the identity of origin changes', inject(function() { - compile('
'); + expect(componentScope.colref).toEqual($rootScope.collection); + expect(componentScope.colrefAlias).toEqual(componentScope.colref); - expect(componentScope.owRef).toBeUndefined(); - expect(componentScope.owRefAlias).toBe(componentScope.owRef); + $rootScope.query = 'Gab'; + $rootScope.$apply(); - $rootScope.obj = {value: 'initial'}; - $rootScope.$apply(); + expect(componentScope.colref).toEqual([$rootScope.collection[0]]); + expect(componentScope.colrefAlias).toEqual([$rootScope.collection[0]]); + })); - expect($rootScope.obj).toEqual({value: 'initial'}); - expect(componentScope.owRef).toEqual({value: 'initial'}); - expect(componentScope.owRefAlias).toBe(componentScope.owRef); + it('should update origin scope when isolate scope changes', inject(function() { + $rootScope.collection = [{ + name: 'Gabriel', + value: 18 + }, { + name: 'Tony', + value: 91 + }]; - // 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'); + compile('
'); - componentScope.owRef = {value: 'isolate1'}; - componentScope.$apply(); - expect($rootScope.obj.value).toBe('origin1'); + var newItem = { + name: 'Pablo', + value: 10 + }; + componentScope.colref.push(newItem); + componentScope.$apply(); - // 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($rootScope.collection[2]).toEqual(newItem); + })); + }); - // 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); - })); - it('should update isolate when both change', inject(function() { - compile('
'); + describe('one-way binding', function() { + it('should update isolate when the identity of origin changes', inject(function() { + compile('
'); - $rootScope.name = {mark:123}; - componentScope.owRef = 'misko'; + expect(componentScope.owRef).toBeUndefined(); + expect(componentScope.owRefAlias).toBe(componentScope.owRef); - $rootScope.$apply(); - expect($rootScope.name).toEqual({mark:123}); - expect(componentScope.owRef).toBe($rootScope.name); - expect(componentScope.owRefAlias).toBe($rootScope.name); + $rootScope.obj = {value: 'initial'}; + $rootScope.$apply(); - $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($rootScope.obj).toEqual({value: 'initial'}); + expect(componentScope.owRef).toEqual({value: 'initial'}); + expect(componentScope.owRefAlias).toBe(componentScope.owRef); - describe('initialization', function() { - var component, log; + // 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'); - beforeEach(function() { - log = []; - angular.module('owComponentTest', []) - .component('owComponent', { - bindings: { input: '<' }, - controller: function() { - component = this; - this.input = 'constructor'; - log.push('constructor'); + componentScope.owRef = {value: 'isolate1'}; + componentScope.$apply(); + expect($rootScope.obj.value).toBe('origin1'); - this.$onInit = function() { - this.input = '$onInit'; - log.push('$onInit'); - }; + // 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'); - this.$onChanges = function(changes) { - if (changes.input) { - log.push(['$onChanges', changes.input]); - } - }; - } - }); - }); + // 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); + })); - it('should not update isolate again after $onInit if outer has not changed', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer'; - compile(''); + it('should update isolate when both change', inject(function() { + compile('
'); - expect($rootScope.name).toEqual('outer'); - expect(component.input).toEqual('$onInit'); + $rootScope.name = {mark:123}; + componentScope.owRef = 'misko'; $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); - expect($rootScope.name).toEqual('outer'); - expect(component.input).toEqual('$onInit'); + $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(log).toEqual([ - 'constructor', - ['$onChanges', jasmine.objectContaining({ currentValue: 'outer' })], - '$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'); + }; - it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer1'; - compile(''); + this.$onChanges = function(changes) { + if (changes.input) { + log.push(['$onChanges', changes.input]); + } + }; + } + }); + }); - expect(component.input).toEqual('$onInit'); - $rootScope.$apply('name = "outer2"'); + it('should not update isolate again after $onInit if outer has not changed', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = 'outer'; + compile(''); - 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('outer'); + expect(component.input).toEqual('$onInit'); - 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'; - }; + $rootScope.$apply(); + + expect($rootScope.name).toEqual('outer'); + expect(component.input).toEqual('$onInit'); + + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: 'outer' })], + '$onInit' + ]); + }); }); - module('owComponentTest'); - inject(function() { - $rootScope.name = 'outer1'; - compile(''); - expect(component.input).toEqual('$onInit'); - $rootScope.$digest(); + 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' })] + ]); + }); + }); - 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(''); + + 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' })] + ]); + }); + }); }); - }); - }); - it('should not break when isolate and origin both change to the same value', inject(function() { - $rootScope.name = 'aaa'; - compile('
'); + it('should not break when isolate and origin both change to the same value', inject(function() { + $rootScope.name = 'aaa'; + compile('
'); - //change both sides to the same item within the same digest cycle - componentScope.owRef = 'same'; - $rootScope.name = 'same'; - $rootScope.$apply(); + //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(); + //change origin back to its previous value + $rootScope.name = 'aaa'; + $rootScope.$apply(); - expect($rootScope.name).toBe('aaa'); - expect(componentScope.owRef).toBe('aaa'); - })); + expect($rootScope.name).toBe('aaa'); + expect(componentScope.owRef).toBe('aaa'); + })); - it('should not update origin when identity of isolate changes', inject(function() { - $rootScope.name = {mark:123}; - compile('
'); + 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($rootScope.name).toEqual({mark:123}); + expect(componentScope.owRef).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}); - })); + componentScope.owRef = 'martin'; + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark: 123}); + expect(componentScope.owRef).toBe('martin'); + expect(componentScope.owRefAlias).toEqual({mark: 123}); + })); - it('should update origin when property of isolate object reference changes', inject(function() { - $rootScope.obj = {mark:123}; - compile('
'); + 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); + 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); - })); + componentScope.owRef.mark = 789; + $rootScope.$apply(); + expect($rootScope.obj).toEqual({mark: 789}); + expect(componentScope.owRef).toBe($rootScope.obj); + })); - it('should not throw on non assignable expressions in the parent', inject(function() { - compile('
'); + it('should not throw on non assignable expressions in the parent', inject(function() { + compile('
'); - $rootScope.name = 'world'; - $rootScope.$apply(); - expect(componentScope.owRef).toBe('hello world'); + $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'); + componentScope.owRef = 'ignore me'; + expect(componentScope.owRef).toBe('ignore me'); + expect($rootScope.name).toBe('world'); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.owRef).toBe('hello misko'); - })); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.owRef).toBe('hello misko'); + })); - it('should not throw when assigning to undefined', inject(function() { - compile('
'); + it('should not throw when assigning to undefined', inject(function() { + compile('
'); - expect(componentScope.owRef).toBeUndefined(); + expect(componentScope.owRef).toBeUndefined(); - componentScope.owRef = 'ignore me'; - expect(componentScope.owRef).toBe('ignore me'); + componentScope.owRef = 'ignore me'; + expect(componentScope.owRef).toBe('ignore me'); - $rootScope.$apply(); - expect(componentScope.owRef).toBe('ignore me'); - })); + $rootScope.$apply(); + expect(componentScope.owRef).toBe('ignore me'); + })); - it('should update isolate scope when "<"-bound NaN changes', inject(function() { - $rootScope.num = NaN; - compile('
'); + it('should update isolate scope when "<"-bound NaN changes', inject(function() { + $rootScope.num = NaN; + compile('
'); - var isolateScope = element.isolateScope(); - expect(isolateScope.owRef).toBeNaN(); + var isolateScope = element.isolateScope(); + expect(isolateScope.owRef).toBeNaN(); - $rootScope.num = 64; - $rootScope.$apply(); - expect(isolateScope.owRef).toBe(64); - })); + $rootScope.num = 64; + $rootScope.$apply(); + expect(isolateScope.owRef).toBe(64); + })); - describe('literal objects', function() { - it('should copy parent changes', inject(function() { - compile('
'); + describe('literal objects', function() { + it('should copy parent changes', inject(function() { + compile('
'); - $rootScope.name = 'a'; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'a'}); + $rootScope.name = 'a'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'a'}); - $rootScope.name = 'b'; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'b'}); - })); + $rootScope.name = 'b'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'b'}); + })); - it('should not change the isolated scope when origin does not change', inject(function() { - compile('
'); + it('should not change the isolated scope when origin does not change', inject(function() { + compile('
'); - $rootScope.name = 'a'; - $rootScope.$apply(); - var lastComponentValue = componentScope.owRef; - $rootScope.$apply(); - expect(componentScope.owRef).toBe(lastComponentValue); - })); + $rootScope.name = 'a'; + $rootScope.$apply(); + var lastComponentValue = componentScope.owRef; + $rootScope.$apply(); + expect(componentScope.owRef).toBe(lastComponentValue); + })); - it('should deep-watch array literals', inject(function() { - $rootScope.name = 'georgios'; - $rootScope.obj = {name: 'pete'}; - compile('
'); + it('should deep-watch array literals', inject(function() { + $rootScope.name = 'georgios'; + $rootScope.obj = {name: 'pete'}; + compile('
'); - expect(componentScope.owRef).toEqual([{name: 'georgios'}, {name: 'pete'}]); + 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'}]); - })); + $rootScope.name = 'lucas'; + $rootScope.obj = {name: 'martin'}; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual([{name: 'lucas'}, {name: 'martin'}]); + })); - it('should deep-watch object literals', inject(function() { - $rootScope.name = 'georgios'; - $rootScope.obj = {name: 'pete'}; - compile('
'); + it('should deep-watch object literals', inject(function() { + $rootScope.name = 'georgios'; + $rootScope.obj = {name: 'pete'}; + compile('
'); - expect(componentScope.owRef).toEqual({name: 'georgios', item: {name: 'pete'}}); + 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'}}); - })); + $rootScope.name = 'lucas'; + $rootScope.obj = {name: 'martin'}; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'lucas', item: {name: 'martin'}}); + })); - it('should not complain when the isolated scope changes', inject(function() { - compile('
'); + it('should not complain when the isolated scope changes', inject(function() { + compile('
'); - $rootScope.name = 'a'; - $rootScope.$apply(); - componentScope.owRef = {name: 'b'}; - componentScope.$apply(); + $rootScope.name = 'a'; + $rootScope.$apply(); + componentScope.owRef = {name: 'b'}; + componentScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'b'}); - expect($rootScope.name).toBe('a'); + expect(componentScope.owRef).toEqual({name: 'b'}); + expect($rootScope.name).toBe('a'); - $rootScope.name = 'c'; - $rootScope.$apply(); - expect(componentScope.owRef).toEqual({name: 'c'}); - })); + $rootScope.name = 'c'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'c'}); + })); - 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 work for primitive literals', inject(function() { + test('1', 1); + test('null', null); + test('undefined', undefined); + test('\'someString\'', 'someString'); + test('true', true); - function test(literalString, literalValue) { - compile('
'); + function test(literalString, literalValue) { + compile('
'); - expect(componentScope.owRef).toBe(literalValue); - dealoc(element); - } - })); + expect(componentScope.owRef).toBe(literalValue); + dealoc(element); + } + })); - describe('optional one-way binding', function() { - it('should update local when origin changes', inject(function() { - compile('
'); + describe('optional one-way binding', function() { + it('should update local when origin changes', inject(function() { + compile('
'); - expect(componentScope.owOptref).toBeUndefined(); - expect(componentScope.owOptrefAlias).toBe(componentScope.owOptref); + expect(componentScope.owOptref).toBeUndefined(); + expect(componentScope.owOptrefAlias).toBe(componentScope.owOptref); - $rootScope.name = 'misko'; - $rootScope.$apply(); - expect(componentScope.owOptref).toBe($rootScope.name); - expect(componentScope.owOptrefAlias).toBe($rootScope.name); + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.owOptref).toBe($rootScope.name); + expect(componentScope.owOptrefAlias).toBe($rootScope.name); - $rootScope.name = {}; - $rootScope.$apply(); - expect(componentScope.owOptref).toBe($rootScope.name); - expect(componentScope.owOptrefAlias).toBe($rootScope.name); - })); + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.owOptref).toBe($rootScope.name); + expect(componentScope.owOptrefAlias).toBe($rootScope.name); + })); - it('should not throw exception when reference does not exist', inject(function() { - compile('
'); + it('should not throw exception when reference does not exist', inject(function() { + compile('
'); - expect(componentScope.owOptref).toBeUndefined(); - expect(componentScope.owOptrefAlias).toBeUndefined(); - })); + expect(componentScope.owOptref).toBeUndefined(); + expect(componentScope.owOptrefAlias).toBeUndefined(); + })); + }); + }); }); - }); - }); - describe('executable expression', function() { - it('should allow expression execution with locals', inject(function() { - compile('
'); - $rootScope.count = 2; + describe('executable expression', function() { + it('should allow expression execution with locals', inject(function() { + compile('
'); + $rootScope.count = 2; - expect(typeof componentScope.expr).toBe('function'); - expect(typeof componentScope.exprAlias).toBe('function'); + expect(typeof componentScope.expr).toBe('function'); + expect(typeof componentScope.exprAlias).toBe('function'); - expect(componentScope.expr({offset: 1})).toEqual(3); - expect($rootScope.count).toEqual(3); + expect(componentScope.expr({offset: 1})).toEqual(3); + expect($rootScope.count).toEqual(3); - expect(componentScope.exprAlias({offset: 10})).toEqual(13); - expect($rootScope.count).toEqual(13); - })); - }); + expect(componentScope.exprAlias({offset: 10})).toEqual(13); + expect($rootScope.count).toEqual(13); + })); + }); - 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 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.ref.mode).toBe('='); - expect(componentScope.$$isolateBindings.ref.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.expr.mode).toBe('&'); - expect(componentScope.$$isolateBindings.expr.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 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.ref.mode).toBe('='); + expect(componentScope.$$isolateBindings.ref.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.expr.mode).toBe('&'); + expect(componentScope.$$isolateBindings.expr.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 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': 'isolate

', + scope: { + 'data': '=dirData', + 'oneway': '
')($rootScope); + expect(controllerCalled).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(); + + module(function($compileProvider) { + $compileProvider.directive('fooDir', valueFn({ + template: '

isolate

', + scope: { + 'data': '=dirData', + 'oneway': '
')($rootScope); - expect(controllerCalled).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(); - - module(function($compileProvider) { - $compileProvider.directive('fooDir', valueFn({ - template: '

isolate

', - scope: { - 'data': '=dirData', - 'oneway': '
')($rootScope); - expect(Controller.prototype.$onInit).toHaveBeenCalled(); - expect(controllerCalled).toBe(true); - }); - }); + }; + element = $compile('
')($rootScope); + expect(Controller.prototype.$onInit).toHaveBeenCalled(); + expect(controllerCalled).toBe(true); + }); + }); - 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' - })); - }); + 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: '); + 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'); - }); - }); + $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': 'isolate

'); + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { 'foo': 'bar', 'baz': 'biz' - }); - expect(this.str).toBe('Hello, world!'); - expect(this.fn()).toBe('called!'); - 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 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'); - }); - - 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); - 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'/); - }); - }); - - - it('should throw noident when missing controllerAs directive property', function() { - module(function($compileProvider) { - $compileProvider.directive('noIdent', valueFn({ - templateUrl: 'test.html', - scope: { - 'data': '=dirData', - 'oneway': '')($rootScope); - }).toThrowMinErr('$compile', 'noident', - 'Cannot bind to controller without identifier for directive \'noIdent\'.'); - }); - }); - - - it('should throw noident when missing controller identifier', function() { - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() {}); - $compileProvider.directive('noIdent', valueFn({ - templateUrl: 'test.html', - scope: { - 'data': '=dirData', - 'oneway': '')($rootScope); - }).toThrowMinErr('$compile', 'noident', - 'Cannot bind to controller without identifier for directive \'noIdent\'.'); - }); - }); - - - it('should bind to controller via object notation (isolate scope)', function() { - var controllerCalled = false; - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', 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!'); - controllerCalled = true; - }); - $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); - }); - }); - - - it('should bind to controller via object notation (new scope)', function() { - var controllerCalled = false; - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() { - expect(this.data).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.data).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.str).toBe('Hello, world!'); - expect(this.fn()).toBe('called!'); - controllerCalled = true; - }); - $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); - }); - }); - - - 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); - }); - }); - - - 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'; - } + }; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); }); - - $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(); - - 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); - - childCtrl.fromParent2 = 'modified'; - $rootScope.$digest(); - expect(parentCtrl.value2).toBe('modified'); - expect(childCtrl.value2).toBe('child2'); - }); - } - ); - - - 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 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.'); }); - - $compileProvider.directive('child', valueFn({ - scope: {}, - controller: 'ChildCtrl as ctrl', - bindToController: { - fromParent1: '@', - fromParent2: '=', - fromParent3: '&', - fromParent4: '<' - }, - template: '' - })); }); - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '' + - '' + - '
')($rootScope); - $rootScope.$digest(); - var parentCtrl = element.controller('ngController'); - var childCtrl = element.find('child').controller('child'); + 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})); - 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); + // We need to test with the exceptionHandler not rethrowing... + $exceptionHandlerProvider.mode('log'); + }); - childCtrl.fromParent2 = 'modified'; - $rootScope.$digest(); + inject(function($exceptionHandler, $compile, $rootScope) { + $compile('
')($rootScope); + expect($exceptionHandler.errors.length).toBe(1); + expect($exceptionHandler.errors[0]).toMatch(/\$compile.*badrestrict.*'true'/); - expect(parentCtrl.value2).toBe('modified'); - expect(childCtrl.value2).toBe('child2'); - }); - } - ); + $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.*'{}'/); - 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 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' + $compile('
')($rootScope); + expect($exceptionHandler.errors.length).toBe(4); + expect($exceptionHandler.errors[3]).toMatch(/\$compile.*badrestrict.*'42'/); + }); }); - 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: { - '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 (isolate 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' + it('should throw noident when missing controllerAs directive property', function() { + module(function($compileProvider) { + $compileProvider.directive('noIdent', valueFn({ + templateUrl: 'test.html', + scope: { + 'data': '=dirData', + 'oneway': '')($rootScope); + }).toThrowMinErr('$compile', 'noident', + 'Cannot bind to controller without identifier for directive \'noIdent\'.'); + }); }); - 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('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.prop = this.prop || 'default'; - this.getProp = function() { - return self.prop; - }; - }, - controllerAs: 'ctrl', - template: '

' - })); + it('should throw noident when missing controller identifier', function() { + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register('myCtrl', function() {}); + $compileProvider.directive('noIdent', valueFn({ + templateUrl: 'test.html', + scope: { + 'data': '=dirData', + 'oneway': '')($rootScope); + }).toThrowMinErr('$compile', 'noident', + 'Cannot bind to controller without identifier for directive \'noIdent\'.'); + }); }); - 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('when creating isolate scope', function() { - module(function($compileProvider) { - $compileProvider.directive('testDir', valueFn({ - scope: {}, - bindToController: { - prop: '@' - }, - controller: function() { - var self = this; - this.prop = this.prop || 'default'; - this.getProp = function() { - return self.prop; + it('should bind to controller via object notation (isolate scope)', function() { + var controllerCalled = false; + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register('myCtrl', function() { + this.check = 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!'); }; - }, - 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'); - }); - }); - }); - - }); - - - describe('controller', 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 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('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'); + controllerCalled = true; + if (preAssignBindings) { + this.check(); + } else { + this.$onInit = this.check; } - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('done'); - expect(element.data('$nestedController')).toBe(expectedController); - }); - }); - - - 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('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); - }); - }); - - - 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; - } - }; + }); + $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); + }); }); - }); - - 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); - } - }; + it('should bind to controller via object notation (new scope)', function() { + var controllerCalled = false; + module(function($compileProvider, $controllerProvider) { + $controllerProvider.register('myCtrl', function() { + this.check = function() { + expect(this.data).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' + }); + expect(this.data).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' + }); + expect(this.str).toBe('Hello, world!'); + expect(this.fn()).toBe('called!'); + }; + controllerCalled = true; + if (preAssignBindings) { + this.check(); + } else { + this.$onInit = this.check; + } + }); + $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); + }); }); - }); - 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); - } - }; + 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); + }); }); - }); - 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) {} - }; + 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); + }); }); - }); - 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); - } - }; + 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); + }); }); - }); - 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 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'; + } + }); - it('should bind the required controllers to the directive controller, if provided as an object and bindToController is truthy', function() { - var parentController, siblingController; + $compileProvider.directive('child', valueFn({ + scope: true, + controller: 'ChildCtrl as ctrl', + bindToController: { + fromParent1: '@', + fromParent2: '=', + fromParent3: '&', + fromParent4: '<' + }, + template: '' + })); + }); - 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(); + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); - 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 - }; - }); + var parentCtrl = element.controller('ngController'); + var childCtrl = element.find('child').controller('child'); - 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)); - }); - }); + 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(); - 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: '?' + expect(parentCtrl.value2).toBe('modified'); + expect(childCtrl.value2).toBe('child2'); + }); } - }) - .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}; - }); + ); - 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 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 not bind required controllers if bindToController is falsy', function() { - var parentController, siblingController; + $compileProvider.directive('child', valueFn({ + scope: {}, + controller: 'ChildCtrl as ctrl', + bindToController: { + fromParent1: '@', + fromParent2: '=', + fromParent3: '&', + fromParent4: '<' + }, + template: '' + })); + }); - 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(); + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); - 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 - }; - }); + var parentCtrl = element.controller('ngController'); + var childCtrl = element.find('child').controller('child'); - module('my'); - inject(function($compile, $rootScope, meDirective) { - element = $compile('')($rootScope); - expect(MeController.prototype.$onInit).toHaveBeenCalled(); - expect(parentController).toBeUndefined(); - expect(siblingController).toBeUndefined(); - }); - }); + 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 required controllers to controller that has an explicit constructor return value', function() { - var parentController, siblingController, meController; + childCtrl.fromParent2 = 'modified'; + $rootScope.$digest(); - function ParentController() { this.name = 'Parent'; } - function SiblingController() { this.name = 'Sibling'; } - function MeController() { - meController = { - name: 'Me', - $onInit: function() { - parentController = this.container; - siblingController = this.friend; + expect(parentCtrl.value2).toBe('modified'); + expect(childCtrl.value2).toBe('child2'); + }); } - }; - 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 - }; - }); + ); - 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)); - }); - }); + 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 bind required controllers to controllers that return an explicit constructor return value', function() { - var parentController, containerController, siblingController, friendController, meController; - 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; - } + it('should re-install controllerAs and bindings for returned value from controller (new scope)', function() { + var controllerCalled = false; + var myCtrl; - 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 + 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: { + '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(); + }); }); - module('my'); - inject(function($compile, $rootScope, meDirective) { - element = $compile('')($rootScope); - expect(containerController).toEqual(parentController); - expect(friendController).toEqual(siblingController); - }); - }); - it('should require controller of an isolate directive from a non-isolate directive on the ' + - 'same element', function() { - var IsolateController = function() {}; - var isolateDirControllerInNonIsolateDirective; + it('should re-install controllerAs and bindings for returned value from controller (isolate scope)', function() { + var controllerCalled = false; + var myCtrl; - module(function() { - directive('isolate', function() { - return { - scope: {}, - controller: IsolateController - }; - }); - directive('nonIsolate', function() { - return { - require: 'isolate', - link: function(_, __, ___, isolateDirController) { - isolateDirControllerInNonIsolateDirective = isolateDirController; - } + 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(); + }); }); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); + 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 (preAssignBindings) { + 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'); - expect(isolateDirControllerInNonIsolateDirective).toBeDefined(); - expect(isolateDirControllerInNonIsolateDirective instanceof IsolateController).toBe(true); - }); - }); + $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 (preAssignBindings) { + 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 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: '' - }; + $rootScope.$digest(); + expect(scope.ctrl.getProp()).toBe('default'); + }); + }); }); - }); - 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 (template)', function() { - var normalScope; - var isolateScope; + describe('require', function() { - 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; - } - }; + 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'); + }); }); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - - expect(normalScope).toBe($rootScope); - expect(normalScope.name).toEqual(undefined); - expect(isolateScope.name).toEqual('WORKS'); - $rootScope.$digest(); - expect(element.text()).toEqual('WORKS'); - }); - }); + 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('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; - } - }; + 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); + }); }); - }); - - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('main.html', '{{name}}'); - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(normalScope).toBe($rootScope); - expect(normalScope.name).toEqual(undefined); - expect(isolateScope.name).toEqual('WORKS'); - expect(element.text()).toEqual('WORKS'); - }); - }); + 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 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')); - } - }; + 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); + }); }); - }); - - 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('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 require controller of a non-isolate directive from an isolate directive on the ' + - 'same element', function() { - var NonIsolateController = function() {}; - var nonIsolateDirControllerInIsolateDirective; - module(function() { - directive('isolate', function() { - return { - scope: {}, - require: 'nonIsolate', - link: function(_, __, ___, nonIsolateDirController) { - nonIsolateDirControllerInIsolateDirective = nonIsolateDirController; - } - }; - }); - directive('nonIsolate', function() { - return { - controller: NonIsolateController - }; - }); - }); + it('should correctly assign controller return values for multiple directives', function() { + var directiveController, otherDirectiveController; + module(function() { - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); + directive('myDirective', function(log) { + return { + scope: true, + controller: function($scope) { + directiveController = { + foo: 'bar' + }; + return directiveController; + } + }; + }); - expect(nonIsolateDirControllerInIsolateDirective).toBeDefined(); - expect(nonIsolateDirControllerInIsolateDirective instanceof NonIsolateController).toBe(true); - }); - }); + directive('myOtherDirective', function(log) { + return { + controller: function($scope) { + otherDirectiveController = { + baz: 'luh' + }; + return otherDirectiveController; + } + }; + }); + }); - 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(log, $compile, $rootScope) { + element = $compile('')($rootScope); + expect(element.data('$myDirectiveController')).toBe(directiveController); + expect(element.data('$myOtherDirectiveController')).toBe(otherDirectiveController); + }); }); - }); - 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 support controller alias', function() { - module(function($controllerProvider) { - $controllerProvider.register('MainCtrl', function() { - this.name = 'lucas'; + 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'); + }); }); - directive('main', function() { - return { - templateUrl: 'main.html', - scope: {}, - controller: 'MainCtrl as mainCtrl' - }; + + + 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'); + }); }); - }); - inject(function($templateCache, $compile, $rootScope) { - $templateCache.put('main.html', '{{mainCtrl.name}}'); - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toBe('lucas'); - }); - }); - - 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); - } - }; + 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!'); + }); }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:main'); - }); - }); - 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); - } - }; + 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'); + }); }); - }); - inject(function(log, $compile, $rootScope) { - expect(function() { - $compile('
')($rootScope); - }).toThrowMinErr('$compile', 'ctreq', 'Controller \'main\', required by directive \'dep\', can\'t be found!'); - }); - }); - 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); - } - }; + 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'); + }); }); - }); - inject(function(log, $compile, $rootScope) { - $compile('
')($rootScope); - expect(log).toEqual('dep:null'); - }); - }); + 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 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); - } + 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; }; - }); - }); - inject(function(log, $compile, $rootScope) { - $compile('
')($rootScope); - expect(log).toEqual('dep:null'); - }); - }); + 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 + }; + }); - 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); - } + 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}; + }); + + 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() { + parentController = this.container; + siblingController = this.friend; }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:false'); - }); - }); + 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 + }; + }); - 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); - } - }; + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(parentController).toBeUndefined(); + expect(siblingController).toBeUndefined(); + }); }); - }); - 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 bind required controllers to controller that has an explicit constructor return value', function() { + var parentController, siblingController, meController; - 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); + 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 + }; + }); + + 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)); }); }); - 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'); - module(function() { - directive('myDirectiveSync', valueFn({ - template: '
Hello!
', - controller: syncCtrlSpy - })); - directive('myDirectiveAsync', valueFn({ - templateUrl: 'myDirectiveAsync.html', - controller: asyncCtrlSpy, - compile: function() { - return function() { + it('should bind required controllers to controllers that return an explicit constructor return value', function() { + var parentController, containerController, siblingController, friendController, meController; + + 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; + } - inject(function($templateCache, $compile, $rootScope) { - expect(syncCtrlSpy).not.toHaveBeenCalled(); - expect(asyncCtrlSpy).not.toHaveBeenCalled(); + 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 + }; + }); - $templateCache.put('myDirectiveAsync.html', '
Hello!
'); - element = $compile('
' + - '' + - '' + - '
')($rootScope); - expect(syncCtrlSpy).not.toHaveBeenCalled(); - expect(asyncCtrlSpy).not.toHaveBeenCalled(); + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(containerController).toEqual(parentController); + expect(friendController).toEqual(siblingController); + }); + }); - $rootScope.$apply(); + it('should require controller of an isolate directive from a non-isolate directive on the ' + + 'same element', function() { + var IsolateController = function() {}; + var isolateDirControllerInNonIsolateDirective; - //expect(syncCtrlSpy).toHaveBeenCalledOnce(); - expect(asyncCtrlSpy).toHaveBeenCalledOnce(); - }); - }); + module(function() { + directive('isolate', function() { + return { + scope: {}, + controller: IsolateController + }; + }); + directive('nonIsolate', function() { + return { + require: 'isolate', + link: function(_, __, ___, isolateDirController) { + isolateDirControllerInNonIsolateDirective = isolateDirController; + } + }; + }); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + expect(isolateDirControllerInNonIsolateDirective).toBeDefined(); + expect(isolateDirControllerInNonIsolateDirective instanceof IsolateController).toBe(true); + }); + }); - it('should instantiate controllers in the parent->child order when transluction, 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('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'); } - }; + 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: '' + }; + }); + }); + + 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); + }); }); - }); - inject(function($templateCache, log, $compile, $rootScope) { - $templateCache.put('parentDirective.html', '
parentTemplateText;
'); - $templateCache.put('childDirective.html', 'childTemplateText;'); - element = $compile('
childContentText;
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('parentController; childController'); - expect(element.text()).toBe('childTemplateText;childContentText;'); - }); - }); + 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 instantiate the controller after the isolate scope bindings are initialized (with template)', function() { - module(function() { - var Ctrl = function($scope, log) { - log('myFoo=' + $scope.myFoo); - }; + expect(normalScope).toBe($rootScope); + expect(normalScope.name).toEqual(undefined); + expect(isolateScope.name).toEqual('WORKS'); + $rootScope.$digest(); + expect(element.text()).toEqual('WORKS'); + }); + }); - directive('myDirective', function() { - return { - scope: { - myFoo: '=' - }, - template: '

Hello

', - controller: Ctrl - }; + + 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(); + + expect(normalScope).toBe($rootScope); + expect(normalScope.name).toEqual(undefined); + expect(isolateScope.name).toEqual('WORKS'); + expect(element.text()).toEqual('WORKS'); + }); }); - }); - inject(function($templateCache, $compile, $rootScope, log) { - $rootScope.foo = 'bar'; - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('myFoo=bar'); - }); - }); + 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')); + } + }; + }); + }); - 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); - }; + inject(function($compile, $rootScope, log) { + element = $compile('
' + + '
' + + '' + + '
')($rootScope); - directive('myDirective', function() { - return { - scope: { - myFoo: '=' - }, - templateUrl: 'hello.html', - controller: Ctrl - }; + $rootScope.$digest(); + expect(log).toEqual('inside=isolate; ' + + 'outside replaced=non-isolate; ' + // outside + 'outside replaced=isolate; ' + // replaced + 'sibling=non-isolate'); + }); }); - }); - inject(function($templateCache, $compile, $rootScope, log) { - $templateCache.put('hello.html', '

Hello

'); - $rootScope.foo = 'bar'; - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('myFoo=bar'); - }); - }); + it('should require controller of a non-isolate directive from an isolate directive on the ' + + 'same element', function() { + var NonIsolateController = function() {}; + var nonIsolateDirControllerInIsolateDirective; + module(function() { + directive('isolate', function() { + return { + scope: {}, + require: 'nonIsolate', + link: function(_, __, ___, nonIsolateDirController) { + nonIsolateDirControllerInIsolateDirective = nonIsolateDirController; + } + }; + }); + directive('nonIsolate', function() { + return { + controller: NonIsolateController + }; + }); + }); - it('should instantiate controllers in the parent->child->baby order when nested transluction, 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 + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); - 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'); } - }; + expect(nonIsolateDirControllerInIsolateDirective).toBeDefined(); + expect(nonIsolateDirControllerInIsolateDirective instanceof NonIsolateController).toBe(true); + }); }); - directive('babyDirective', function() { - return { - require: '^childDirective', - templateUrl: 'babyDirective.html', - controller: function(log) { log('babyController'); } - }; + + + 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:'); + }); }); - }); - inject(function($templateCache, log, $compile, $rootScope) { - $templateCache.put('parentDirective.html', '
parentTemplateText;
'); - $templateCache.put('childDirective.html', 'childTemplateText;'); - $templateCache.put('babyDirective.html', 'babyTemplateText;'); - element = $compile('
' + - '
' + - 'childContentText;' + - '
babyContent;
' + - '
' + - '
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('parentController; childController; babyController'); - expect(element.text()).toBe('childContentText;babyTemplateText;'); - }); - }); + 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 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() { + it('should require controller on parent element',function() { + module(function() { + directive('main', function(log) { return { - pre: function(scope, template, attr, ctrl) {}, - post: function() {} + 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'); + }); }); - }); - inject(function($templateCache, $compile, $rootScope, log) { - $templateCache.put('hello.html', '

Hello

'); - element = $compile('
')($rootScope); - $rootScope.$apply(); + 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!'); + }); + }); - expect(log).toEqual('instance'); - expect(element.text()).toBe('Hello'); - }); - }); + 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 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() { + 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 { - pre: function(scope, template, attr, ctrl) {}, - post: function() {} + require: '^?main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller); + } }; - } - }; + }); + }); + inject(function(log, $compile, $rootScope) { + $compile('
')($rootScope); + expect(log).toEqual('dep:null'); + }); }); - }); - inject(function($templateCache, $compile, $rootScope, log) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('instance'); - expect(element.text()).toBe('Hello'); - }); - }); + 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 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 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'); + }); + }); - describe('transclude', function() { + 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'); + }); + } + ); - describe('content transclusion', function() { + it('should instantiate the controller just once when template/templateUrl', function() { + var syncCtrlSpy = jasmine.createSpy('sync controller'), + asyncCtrlSpy = jasmine.createSpy('async controller'); - 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}};
' - }; + module(function() { + directive('myDirectiveSync', valueFn({ + template: '
Hello!
', + controller: syncCtrlSpy + })); + directive('myDirectiveAsync', valueFn({ + templateUrl: 'myDirectiveAsync.html', + controller: asyncCtrlSpy, + compile: function() { + return function() { + }; + } + })); }); - }); - 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($templateCache, $compile, $rootScope) { + expect(syncCtrlSpy).not.toHaveBeenCalled(); + expect(asyncCtrlSpy).not.toHaveBeenCalled(); - 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(); + $templateCache.put('myDirectiveAsync.html', '
Hello!
'); + element = $compile('
' + + '' + + '' + + '
')($rootScope); + expect(syncCtrlSpy).not.toHaveBeenCalled(); + expect(asyncCtrlSpy).not.toHaveBeenCalled(); - expect(element.text()).toEqual('book-'); + $rootScope.$apply(); - $httpBackend.flush(); - $rootScope.$apply(); - expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); + //expect(syncCtrlSpy).toHaveBeenCalledOnce(); + expect(asyncCtrlSpy).toHaveBeenCalledOnce(); + }); }); - }); - 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'); - }); - }); + it('should instantiate controllers in the parent->child order when transluction, 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('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 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:
childContentText;
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('parentController; childController'); + expect(element.text()).toBe('childTemplateText;childContentText;'); + }); }); - }); - //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', function() { - module(function() { - directive('isolate', valueFn({ - scope: {}, - replace: true, - template: '
{{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'); - }); - }); + 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'; - //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'); + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('myFoo=bar'); + }); }); - }); - 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 (with debug info)', function() { - if (jQuery) { - // jQuery 2.x doesn't expose the cache storage. - return; - } + 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); + }; + directive('myDirective', function() { + return { + scope: { + myFoo: '=' + }, + templateUrl: 'hello.html', + controller: Ctrl + }; + }); + }); - module(function($compileProvider) { - $compileProvider.debugInfoEnabled(true); - }); + inject(function($templateCache, $compile, $rootScope, log) { + $templateCache.put('hello.html', '

Hello

'); + $rootScope.foo = 'bar'; - inject(function($compile, $rootScope) { - var cacheSize = jqLiteCacheSize(); + element = $compile('
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('myFoo=bar'); + }); + }); - element = $compile('
{{x}}
')($rootScope); - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - $rootScope.$apply('xs = [0,1]'); - expect(jqLiteCacheSize()).toEqual(cacheSize + 2); + it('should instantiate controllers in the parent->child->baby order when nested transluction, 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 - $rootScope.$apply('xs = [0]'); - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + 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'); } + }; + }); + }); - $rootScope.$apply('xs = []'); - expect(jqLiteCacheSize()).toEqual(cacheSize + 1); + inject(function($templateCache, log, $compile, $rootScope) { + $templateCache.put('parentDirective.html', '
parentTemplateText;
'); + $templateCache.put('childDirective.html', 'childTemplateText;'); + $templateCache.put('babyDirective.html', 'babyTemplateText;'); - element.remove(); - expect(jqLiteCacheSize()).toEqual(cacheSize + 0); + element = $compile('
' + + '
' + + 'childContentText;' + + '
babyContent;
' + + '
' + + '
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('parentController; childController; babyController'); + expect(element.text()).toBe('childContentText;babyTemplateText;'); + }); }); - }); - 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 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 { + pre: function(scope, template, attr, ctrl) {}, + post: function() {} + }; + } + }; + }); + }); + inject(function($templateCache, $compile, $rootScope, log) { + $templateCache.put('hello.html', '

Hello

'); - module(function($compileProvider) { - $compileProvider.debugInfoEnabled(false); + element = $compile('
')($rootScope); + $rootScope.$apply(); + + expect(log).toEqual('instance'); + expect(element.text()).toBe('Hello'); + }); }); - inject(function($compile, $rootScope) { - var cacheSize = jqLiteCacheSize(); - element = $compile('
{{x}}
')($rootScope); - expect(jqLiteCacheSize()).toEqual(cacheSize); + 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() {} + }; + } + }; + }); + }); - $rootScope.$apply('xs = [0,1]'); - expect(jqLiteCacheSize()).toEqual(cacheSize); + inject(function($templateCache, $compile, $rootScope, log) { + element = $compile('
')($rootScope); + $rootScope.$apply(); - $rootScope.$apply('xs = [0]'); - expect(jqLiteCacheSize()).toEqual(cacheSize); + expect(log).toEqual('instance'); + expect(element.text()).toBe('Hello'); + }); + }); - $rootScope.$apply('xs = []'); - expect(jqLiteCacheSize()).toEqual(cacheSize); - element.remove(); - expect(jqLiteCacheSize()).toEqual(cacheSize); + 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 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; - } + describe('transclude', function() { - module(function($compileProvider) { - $compileProvider.debugInfoEnabled(true); - }); + describe('content transclusion', function() { - inject(function($compile, $rootScope) { - var cacheSize = jqLiteCacheSize(); - element = $compile('
{{x}}
')($rootScope); + 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 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(); - $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); + expect(element.text()).toEqual('book-'); - $rootScope.$apply('val = true'); - // Now we have two concrete transcluded elements plus some comments so two more cache items - expect(jqLiteCacheSize()).toEqual(cacheSize + 3); + $httpBackend.flush(); + $rootScope.$apply(); + expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); + }); + }); - $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); - }); - }); + 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'); + }); + }); - 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; - } - var linkFn = jasmine.createSpy('linkFn'); + 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'); }); }); - $compileProvider.directive('isolateRed', function() { - return { - restrict: 'A', - scope: {}, - template: '
' - }; + + + //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'); + }); }); - $compileProvider.directive('red', function() { - return { - restrict: 'A', - templateUrl: 'red.html', - scope: {}, - link: linkFn - }; + + 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.'); + }); }); - }); - 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(); - }); - }); - if (jQuery) { - describe('cleaning up after a replaced element', function() { - var $compile, xs; - beforeEach(inject(function(_$compile_) { - $compile = _$compile_; - xs = [0, 1]; - })); + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }); - function testCleanup() { - var privateData, firstRepeatedElem; + inject(function($compile, $rootScope) { + var cacheSize = jqLiteCacheSize(); - element = $compile('
{{x}}
')($rootScope); + element = $compile('
{{x}}
')($rootScope); + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - $rootScope.$apply('xs = [' + xs + ']'); - firstRepeatedElem = element.children('.ng-scope').eq(0); + $rootScope.$apply('xs = [0,1]'); + expect(jqLiteCacheSize()).toEqual(cacheSize + 2); - 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(); + $rootScope.$apply('xs = [0]'); + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - //Ensure the angular $destroy event is still sent - var destroyCount = 0; - element.find('div').on('$destroy', function() { destroyCount++; }); + $rootScope.$apply('xs = []'); + expect(jqLiteCacheSize()).toEqual(cacheSize + 1); - $rootScope.$apply('xs = null'); + element.remove(); + expect(jqLiteCacheSize()).toEqual(cacheSize + 0); + }); + }); - 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 Angular', 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 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; + } - testCleanup(); - // The ng-repeat template is removed/cleaned (the +1) - // and each clone of the ng-repeat template is also removed (xs.length) - expect(cleanedCount).toBe(xs.length + 1); + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(false); + }); - // Restore the previous jQuery.cleanData. - jQuery.cleanData = currentCleanData; - }); - }); - } + inject(function($compile, $rootScope) { + var cacheSize = jqLiteCacheSize(); + element = $compile('
{{x}}
')($rootScope); + expect(jqLiteCacheSize()).toEqual(cacheSize); - 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'); - }); - }); + $rootScope.$apply('xs = [0,1]'); + expect(jqLiteCacheSize()).toEqual(cacheSize); + $rootScope.$apply('xs = [0]'); + expect(jqLiteCacheSize()).toEqual(cacheSize); - it('should clear contents of the ng-translude 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!
'); - }); - }); + $rootScope.$apply('xs = []'); + expect(jqLiteCacheSize()).toEqual(cacheSize); - it('should NOT clear contents of the ng-translude element before appending transcluded content' + - ' if transcluded content does NOT exist', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
old stuff!
' - }; + element.remove(); + expect(jqLiteCacheSize()).toEqual(cacheSize); + }); }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(sortedHtml(element.html())).toEqual('
old stuff!
'); - }); - }); - 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 not leak if two "element" transclusions are on the same element (with debug info)', function() { + if (jQuery) { + // jQuery 2.x doesn't expose the cache storage. + return; + } + module(function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }); - it('should allow cloning of the fallback via ngRepeat', function() { - module(function() { - directive('trans', function() { - return { - transclude: true, - template: '
{{i}}
' - }; + 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); + }); }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toEqual('012'); - }); - }); + 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; + } + + var linkFn = jasmine.createSpy('linkFn'); + + 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 + }; + }); + }); - it('should not link the fallback content if transcluded content is provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + 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); - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; + $templateCache.removeAll(); + var destroyedScope = $rootScope.$new(); + destroyedScope.$destroy(); + var clone = template(destroyedScope, noop); + $rootScope.$digest(); + $timeout.flush(); + expect(linkFn).not.toHaveBeenCalled(); + clone.remove(); + }); }); - 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(); - }); - }); + if (jQuery) { + describe('cleaning up after a replaced element', function() { + var $compile, xs; + beforeEach(inject(function(_$compile_) { + $compile = _$compile_; + xs = [0, 1]; + })); - it('should compile and link the fallback content if no transcluded content is provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + function testCleanup() { + var privateData, firstRepeatedElem; - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; - }); + element = $compile('
{{x}}
')($rootScope); - 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(); - }); - }); + $rootScope.$apply('xs = [' + xs + ']'); + firstRepeatedElem = element.children('.ng-scope').eq(0); - it('should compile and link the fallback content if an optional transclusion slot is not provided', function() { - var linkSpy = jasmine.createSpy('postlink'); + 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(); - module(function() { - directive('inner', function() { - return { - restrict: 'E', - template: 'old stuff! ', - link: linkSpy - }; - }); + //Ensure the angular $destroy event is still sent + var destroyCount = 0; + element.find('div').on('$destroy', function() { destroyCount++; }); - 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(); - }); - }); + $rootScope.$apply('xs = null'); - 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('
'); - }); - }); + expect(destroyCount).toBe(2); + expect(firstRepeatedElem.data('$scope')).not.toBeDefined(); + privateData = jQuery._data(firstRepeatedElem[0]); + expect(privateData && privateData.events).not.toBeDefined(); + } - it('should throw on an ng-transclude element inside no transclusion directive', function() { - inject(function($rootScope, $compile) { - // we need to do this because different browsers print empty attributes differently - try { - $compile('
')($rootScope); - } catch (e) { - expect(e.message).toMatch(new RegExp( - '^\\[ngTransclude:orphan\\] ' + - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found\\. ' + - 'Element:
' + - '
' + - '
this one should get replaced with content
' + - '
' + - '
', - transclude: true - })); + 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'); + }); + }); - $compileProvider.directive('noTransBar', valueFn({ - template: '
' + - // This ng-transclude is invalid. It should throw an error. - '
' + - '
', - transclude: false - })); - }); + it('should clear contents of the ng-translude 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!
'); + }); + }); - 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:
'); - }); - }); + it('should NOT clear contents of the ng-translude 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!
'); + }); + }); - it('should not pass transclusion into a templateUrl directive', function() { + 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
'); + }); + }); - module(function($compileProvider) { - $compileProvider.directive('transFoo', valueFn({ - template: '
' + - '
' + - '
this one should get replaced with content
' + - '
' + - '
', - transclude: 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'); + }); + }); - })); - $compileProvider.directive('noTransBar', valueFn({ - templateUrl: 'noTransBar.html', - transclude: false + it('should not link the fallback content if transcluded content is provided', function() { + var linkSpy = jasmine.createSpy('postlink'); - })); - }); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('noTransBar.html', - '
' + - // This ng-transclude is invalid. It should throw an error. - '
' + - '
'); + 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(); + }); + }); - expect(function() { - element = $compile('
content
')($rootScope); - $rootScope.$apply(); - }).toThrowMinErr('ngTransclude', 'orphan', - 'Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:
'); - }); - }); + it('should compile and link the fallback content if no transcluded content is provided', function() { + var linkSpy = jasmine.createSpy('postlink'); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); - 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); - }); - }; - } - })); - }); + 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(); + }); + }); - inject(function($compile, $rootScope, $templateCache) { - $templateCache.put('foo.html', '
whatever
'); + it('should compile and link the fallback content if an optional transclusion slot is not provided', function() { + var linkSpy = jasmine.createSpy('postlink'); - compile('
transcluded content
'); - $rootScope.$apply(); + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); - expect(trim(element.text())).toBe('transcluded content'); - }); - }); + 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 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('
'); + }); + }); - 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() + ')'); - } + it('should throw on an ng-transclude element inside no transclusion directive', function() { + inject(function($rootScope, $compile) { + // we need to do this because different browsers print empty attributes differently + try { + $compile('
')($rootScope); + } catch (e) { + expect(e.message).toMatch(new RegExp( + '^\\[ngTransclude:orphan\\] ' + + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found\\. ' + + 'Element:
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 not pass transclusion into a template directive when the directive didn\'t request transclusion', function() { - 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', '
'); + module(function($compileProvider) { - element = $compile('
unicorn!
')($rootScope); - $rootScope.$apply(); - expect(log).toEqual('pre(); post(unicorn!)'); - }); - }); + $compileProvider.directive('transFoo', valueFn({ + template: '
' + + '
' + + '
this one should get replaced with content
' + + '
' + + '
', + transclude: true + })); - 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!)'); - }); - }); + $compileProvider.directive('noTransBar', valueFn({ + template: '
' + + // This ng-transclude is invalid. It should throw an error. + '
' + + '
', + transclude: false + })); + }); - 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($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(log, $rootScope, $compile, $templateCache) { - $templateCache.put('trans.html', '
'); - 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); - } + it('should not pass transclusion into a templateUrl directive', function() { - function cloneAttach(clone) { - el.append(clone); - } - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - var children = element.children(), i; - expect(transcludeCtrl).toBeDefined(); + module(function($compileProvider) { - expect(element.data('$transcludeController')).toBe(transcludeCtrl); - for (i = 0; i < cloneCount; i++) { - expect(children.eq(i).data('$transcludeController')).toBeUndefined(); - } - }); - }); + $compileProvider.directive('transFoo', valueFn({ + template: '
' + + '
' + + '
this one should get replaced with content
' + + '
' + + '
', + transclude: true - 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); - }); - }); + })); + + $compileProvider.directive('noTransBar', valueFn({ + templateUrl: 'noTransBar.html', + transclude: false + + })); + }); + + 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.$apply(); + }).toThrowMinErr('ngTransclude', 'orphan', + 'Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:
'); + }); + }); + + + 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 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, $rootScope, $templateCache) { + $templateCache.put('foo.html', '
whatever
'); + + compile('
transcluded content
'); + $rootScope.$apply(); + + expect(trim(element.text())).toBe('transcluded content'); + }); + }); + + + 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($compile) { - element = $compile('
{{$id}}
')($rootScope); - $rootScope.$apply(); - expect(element.text()).toBe('' + $rootScope.$id); - }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('pre(); post(unicorn!)'); + }); + }); - }); - 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); + 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() + ')'); + } + } + }; }); - } - })); - directive('child', valueFn({ - require: '^transclude', - link: function(scope, element, attr, ctrl) { - capturedChildCtrl = ctrl; - } - })); - }); - inject(function($compile) { - element = $compile('
')($rootScope); - expect(capturedChildCtrl).toBeTruthy(); - }); - }); + }); + inject(function(log, $rootScope, $compile, $templateCache) { + $templateCache.put('trans.html', '
'); + element = $compile('
unicorn!
')($rootScope); + $rootScope.$apply(); + expect(log).toEqual('pre(); post(unicorn!)'); + }); + }); - // 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: {} - })); + + 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!)'); + }); }); - 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; + 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', '
'); - expect(element.text()).toBe('102030'); - expect(newWatcherCount).toBe(3); + 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/9413 - describe('passing a parent bound transclude function to the link ' + - 'function returned from `$compile`', function() { + function cloneAttach(clone) { + el.append(clone); + } + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + var children = element.children(), i; + expect(transcludeCtrl).toBeDefined(); - 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 - }); - }; + expect(element.data('$transcludeController')).toBe(transcludeCtrl); + for (i = 0; i < cloneCount; i++) { + expect(children.eq(i).data('$transcludeController')).toBeUndefined(); } - }; + }); }); - directive('toggle', valueFn({ - scope: {t: '=toggle'}, - transclude: true, - template: '
' - })); - })); - it('should preserve the bound scope', function() { + 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); + }); + }); - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
' + - '
' + - 'SuccessError' + - '
' + - '
')($rootScope); + 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); + }); - $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 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); + }); + } + })); + directive('child', valueFn({ + require: '^transclude', + link: function(scope, element, attr, ctrl) { + capturedChildCtrl = ctrl; + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + expect(capturedChildCtrl).toBeTruthy(); + }); + }); - $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'); - }); - }); + // 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 }}')); - it('should preserve the bound scope when using recursive transclusion', function() { + var initialWatcherCount = $rootScope.$countWatchers(); + $compile(element)($rootScope); + $rootScope.$apply('value = 0'); + var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount; - directive('recursiveTransclude', valueFn({ - transclude: true, - template: '
' - })); + expect(element.text()).toBe('102030'); + expect(newWatcherCount).toBe(3); + }); + } + ); - 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/9413 + describe('passing a parent bound transclude function to the link ' + + 'function returned from `$compile`', function() { - $rootScope.$apply('t = true'); - expect($rootScope.$countChildScopes()).toBe(4); - expect(element.text()).toBe('Success'); + 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 = false'); - expect($rootScope.$countChildScopes()).toBe(1); - expect(element.text()).toBe(''); + it('should preserve the bound scope', function() { - $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(''); + + $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'); + }); + }); - // see issue https://github.com/angular/angular.js/issues/9095 - describe('removing a transcluded element', function() { + it('should preserve the bound scope when using recursive transclusion', function() { - beforeEach(module(function() { - directive('toggle', function() { - return { - 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'); + }); + }); }); - })); - it('should not leak the transclude scope when the transcluded content is an element transclusion directive', - inject(function($compile, $rootScope) { + // see issue https://github.com/angular/angular.js/issues/9095 + describe('removing a transcluded element', function() { - 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); - })); + 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); - - $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( + '
' + + '
{{ 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); + })); - it('should not leak the transclude scope if the transcluded contains only comments', - 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); - })); + 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 text nodes', - inject(function($compile, $rootScope) { + element = $compile( + '
' + + '
{{ msg }}
' + + '
{{ msg }}
' + + '
' + )($rootScope); - 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); - })); + $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 mark as destroyed all sub scopes of the scope being destroyed', - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
{{ msg }}
' + - '
' - )($rootScope); + it('should not leak the transclude scope if the transcluded contains only comments', + inject(function($compile, $rootScope) { - $rootScope.$apply('t = true'); - var childScopes = getChildScopes($rootScope); + element = $compile( + '
' + + '' + + '
' + )($rootScope); - $rootScope.$apply('t = false'); - for (var i = 0; i < childScopes.length; ++i) { - expect(childScopes[i].$$destroyed).toBe(true); - } - })); - }); + $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 text nodes', + inject(function($compile, $rootScope) { - describe('nested transcludes', function() { + element = $compile( + '
' + + 'some text' + + '
' + )($rootScope); - beforeEach(module(function($compileProvider) { + $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); + })); - $compileProvider.directive('noop', valueFn({})); + it('should mark as destroyed all sub scopes of the scope being destroyed', + inject(function($compile, $rootScope) { - $compileProvider.directive('sync', valueFn({ - template: '
', - transclude: true - })); + element = $compile( + '
' + + '
{{ msg }}
' + + '
' + )($rootScope); - $compileProvider.directive('async', valueFn({ - templateUrl: 'async', - transclude: true - })); + $rootScope.$apply('t = true'); + var childScopes = getChildScopes($rootScope); - $compileProvider.directive('syncSync', valueFn({ - template: '
', - transclude: true - })); + $rootScope.$apply('t = false'); + for (var i = 0; i < childScopes.length; ++i) { + expect(childScopes[i].$$destroyed).toBe(true); + } + })); + }); - $compileProvider.directive('syncAsync', valueFn({ - template: '
', - transclude: true - })); - $compileProvider.directive('asyncSync', valueFn({ - templateUrl: 'asyncSync', - transclude: true - })); + describe('nested transcludes', function() { - $compileProvider.directive('asyncAsync', valueFn({ - templateUrl: 'asyncAsync', - transclude: true - })); + beforeEach(module(function($compileProvider) { - })); + $compileProvider.directive('noop', valueFn({})); - beforeEach(inject(function($templateCache) { - $templateCache.put('async', '
'); - $templateCache.put('asyncSync', '
'); - $templateCache.put('asyncAsync', '
'); - })); + $compileProvider.directive('sync', valueFn({ + template: '
', + transclude: true + })); + $compileProvider.directive('async', valueFn({ + templateUrl: 'async', + transclude: true + })); - 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'); - })); + $compileProvider.directive('syncSync', valueFn({ + template: '
', + transclude: true + })); - 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'); - })); + $compileProvider.directive('syncAsync', valueFn({ + template: '
', + transclude: true + })); - 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'); - })); + $compileProvider.directive('asyncSync', valueFn({ + templateUrl: 'asyncSync', + transclude: true + })); - 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'); - })); + $compileProvider.directive('asyncAsync', valueFn({ + templateUrl: 'asyncAsync', + transclude: true + })); + })); - it('should not leak memory with nested transclusion', function() { - inject(function($compile, $rootScope) { - var size, initialSize = jqLiteCacheSize(); + beforeEach(inject(function($templateCache) { + $templateCache.put('async', '
'); + $templateCache.put('asyncSync', '
'); + $templateCache.put('asyncAsync', '
'); + })); - element = jqLite('
  • {{n}} => EvenOdd
'); - $compile(element)($rootScope.$new()); - $rootScope.nums = [0,1,2]; - $rootScope.$apply(); - size = jqLiteCacheSize(); + 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'); + })); - $rootScope.nums = [3,4,5]; - $rootScope.$apply(); - expect(jqLiteCacheSize()).toEqual(size); + 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'); + })); - element.remove(); - expect(jqLiteCacheSize()).toEqual(initialSize); - }); - }); - }); + 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'); + })); - describe('nested isolated scope transcludes', function() { - beforeEach(module(function($compileProvider) { - $compileProvider.directive('trans', valueFn({ - restrict: 'E', - template: '
', - transclude: true - })); + it('should not leak memory with nested transclusion', function() { + inject(function($compile, $rootScope) { + var size, initialSize = jqLiteCacheSize(); - $compileProvider.directive('transAsync', valueFn({ - restrict: 'E', - templateUrl: 'transAsync', - transclude: true - })); + element = jqLite('
  • {{n}} => EvenOdd
'); + $compile(element)($rootScope.$new()); - $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: {} - })); - })); + $rootScope.nums = [0,1,2]; + $rootScope.$apply(); + size = jqLiteCacheSize(); - beforeEach(inject(function($templateCache) { - $templateCache.put('transAsync', '
'); - $templateCache.put('isoAsync', ''); - })); + $rootScope.nums = [3,4,5]; + $rootScope.$apply(); + expect(jqLiteCacheSize()).toEqual(size); + + element.remove(); + expect(jqLiteCacheSize()).toEqual(initialSize); + }); + }); + }); - it('should pass the outer scope to the transclude on the isolated template sync-sync', inject(function($compile, $rootScope) { + describe('nested isolated scope transcludes', function() { + beforeEach(module(function($compileProvider) { - $rootScope.val = 'transcluded content'; - element = $compile('')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + $compileProvider.directive('trans', valueFn({ + restrict: 'E', + template: '
', + transclude: true + })); - it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) { + $compileProvider.directive('transAsync', valueFn({ + restrict: 'E', + templateUrl: 'transAsync', + transclude: true + })); - $rootScope.val = 'transcluded content'; - element = $compile('')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('transcluded content'); - })); + $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: {} + })); + })); - it('should pass the outer scope to the transclude on the isolated template async-async', inject(function($compile, $rootScope) { + beforeEach(inject(function($templateCache) { + $templateCache.put('transAsync', '
'); + $templateCache.put('isoAsync', ''); + })); - $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 sync-sync', inject(function($compile, $rootScope) { - describe('multiple siblings receiving transclusion', function() { + $rootScope.val = 'transcluded content'; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - it('should only receive transclude from parent', function() { + it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) { - module(function($compileProvider) { + $rootScope.val = 'transcluded content'; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); - $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 + 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'); })); }); - inject(function($compile, $rootScope) { - var element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('myExample 0!'); - dealoc(element); + describe('multiple siblings receiving transclusion', function() { - element = $compile('

')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('myExample 1!has children'); - dealoc(element); + it('should only receive transclude from parent', function() { + + 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 + + })); + + }); + + 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); + }); + }); }); }); - }); - }); - 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

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

template.html

'); + $compile('
'); + expect(function() { + $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 trasclusion', 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') - }; + it('should terminate compilation only for element trasclusion', 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'); + }); }); - }); - 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); - }); - } - }; + 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' + ]); + }); }); - directive('normalDir', function(log) { - return { - controller: function() { - log('controller:normalDir'); - } - }; + + 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(); + }); }); - }); - inject(function($compile, $rootScope, log) { - element = $compile('
')($rootScope); - expect(log).toEqual([ - 'controller:elementTrans', - 'controller:normalDir', - 'controller:normalDir', - 'controller:normalDir' - ]); - }); - }); - 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(); - }); - }); + 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 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; + function cloneAttach(clone) { + el.after(clone); + } + } + })); + }); + inject(function($compile) { + element = $compile('
')($rootScope); + var children = element.children(), i; for (i = 0; i < cloneCount; i++) { - $transclude(cloneAttach); + expect(children.eq(i).data('$transcludeController')).toBe(transcludeCtrl); } + }); + }); - 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); + }); + } + })); + 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 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); + 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('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(); + 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 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)); - } - }; + 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; + }); }); - 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($compile, $rootScope) { + element = $compile('
')($rootScope); + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect(element.text()).toBe('bar'); }); }); - 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 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; + 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'); + }); }); - }); - inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect(element.text()).toBe('bar'); - }); - }); - - 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 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 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(); + expect(element.text()).toBe('-->|x|'); + })); - expect(element.text()).toBe('-->|x|'); - })); + 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) { - 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(transclude).toEqual(jasmine.any(Function)); - expect(transclude).toEqual(jasmine.any(Function)); + transclude(function(child) { element.append(child); }); + } + }; + }); - transclude(function(child) { element.append(child); }); - } - }; - }); + directive('trans', function($timeout) { + return { + transclude: true, + link: function(scope, element, attrs, ctrl, transclude) { - directive('trans', function($timeout) { - return { - transclude: true, - link: function(scope, element, attrs, ctrl, transclude) { + // We use timeout here to simulate how ng-if works + $timeout(function() { + transclude(function(child) { element.append(child); }); + }); + } + }; + }); - // We use timeout here to simulate how ng-if works - $timeout(function() { - transclude(function(child) { element.append(child); }); - }); - } - }; - }); + directive('replaceWithTemplate', function() { + return { + templateUrl: 'template.html', + replace: true + }; + }); + }); - directive('replaceWithTemplate', function() { - return { - templateUrl: 'template.html', - replace: true - }; - }); - }); + inject(function($compile, $rootScope, $templateCache, $timeout) { - inject(function($compile, $rootScope, $templateCache, $timeout) { + $templateCache.put('template.html', '
Content To Be Transcluded
'); - $templateCache.put('template.html', '
Content To Be Transcluded
'); + expect(function() { + element = $compile('
')($rootScope); + $timeout.flush(); + }).not.toThrow(); - expect(function() { - element = $compile('
')($rootScope); - $timeout.flush(); - }).not.toThrow(); + expect(element.text()).toEqual('Content To Be Transcluded'); + }); - expect(element.text()).toEqual('Content To Be Transcluded'); - }); + }); - }); + it('should lazily compile the contents of directives that are transcluded', function() { + var innerCompilationCount = 0, transclude; - it('should lazily compile the contents of directives that are transcluded', function() { - var innerCompilationCount = 0, transclude; + module(function() { + directive('trans', valueFn({ + transclude: true, + controller: function($transclude) { + transclude = $transclude; + } + })); - module(function() { - directive('trans', valueFn({ - transclude: true, - controller: function($transclude) { - transclude = $transclude; - } - })); + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + 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'); + }); + }); - 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'); - }); - }); + it('should lazily compile the contents of directives that are transcluded with a template', function() { + var innerCompilationCount = 0, transclude; - it('should lazily compile the contents of directives that are transcluded with a template', function() { - var innerCompilationCount = 0, transclude; + module(function() { + directive('trans', valueFn({ + transclude: true, + template: '
Baz
', + controller: function($transclude) { + transclude = $transclude; + } + })); - module(function() { - directive('trans', valueFn({ - transclude: true, - template: '
Baz
', - controller: function($transclude) { - transclude = $transclude; - } - })); + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + 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'); + }); + }); - 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'); - }); - }); + it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() { + var innerCompilationCount = 0, transclude; - it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() { - var innerCompilationCount = 0, transclude; + module(function() { + directive('trans', valueFn({ + transclude: true, + templateUrl: 'baz.html', + controller: function($transclude) { + transclude = $transclude; + } + })); - module(function() { - directive('trans', valueFn({ - transclude: true, - templateUrl: 'baz.html', - controller: function($transclude) { - transclude = $transclude; - } - })); + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.expectGET('baz.html').respond('
Baz
'); + element = $compile('')($rootScope); + $httpBackend.flush(); - inject(function($compile, $rootScope, $httpBackend) { - $httpBackend.expectGET('baz.html').respond('
Baz
'); - element = $compile('')($rootScope); - $httpBackend.flush(); + expect(innerCompilationCount).toBe(0); + transclude(function(child) { element.append(child); }); + expect(innerCompilationCount).toBe(1); + expect(element.text()).toBe('BazFooBar'); + }); + }); - expect(innerCompilationCount).toBe(0); - transclude(function(child) { element.append(child); }); - expect(innerCompilationCount).toBe(1); - expect(element.text()).toBe('BazFooBar'); - }); - }); + it('should lazily compile the contents of directives that are transclude element', function() { + var innerCompilationCount = 0, transclude; - it('should lazily compile the contents of directives that are transclude element', function() { - var innerCompilationCount = 0, transclude; + module(function() { + directive('trans', valueFn({ + transclude: 'element', + controller: function($transclude) { + transclude = $transclude; + } + })); - module(function() { - directive('trans', valueFn({ - transclude: 'element', - controller: function($transclude) { - transclude = $transclude; - } - })); + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + 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'); + }); + }); - 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'); - }); - }); + it('should lazily compile transcluded directives with ngIf on them', function() { + var innerCompilationCount = 0, outerCompilationCount = 0, transclude; - it('should lazily compile transcluded directives with ngIf on them', function() { - var innerCompilationCount = 0, outerCompilationCount = 0, transclude; + module(function() { + directive('outer', valueFn({ + transclude: true, + compile: function() { + outerCompilationCount += 1; + }, + controller: function($transclude) { + transclude = $transclude; + } + })); - module(function() { - directive('outer', valueFn({ - transclude: true, - compile: function() { - outerCompilationCount += 1; - }, - controller: function($transclude) { - transclude = $transclude; - } - })); + directive('inner', valueFn({ + template: 'FooBar', + compile: function() { + innerCompilationCount += 1; + } + })); + }); - directive('inner', valueFn({ - template: 'FooBar', - compile: function() { - innerCompilationCount += 1; - } - })); - }); + 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'); + }); + }); - inject(function($compile, $rootScope) { - $rootScope.shouldCompile = false; + it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() { + var innerCompilationCount = 0; - 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 + })); - it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() { - var innerCompilationCount = 0; + directive('outer', valueFn({ + templateUrl: 'inner.html', + replace: true + })); - module(function() { - directive('outer', valueFn({ - transclude: true - })); + directive('inner', valueFn({ + compile: function() { + innerCompilationCount += 1; + } + })); + }); - directive('outer', valueFn({ - templateUrl: 'inner.html', - replace: true - })); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.expectGET('inner.html').respond(''); + element = $compile('')($rootScope); + $httpBackend.flush(); - directive('inner', valueFn({ - compile: function() { - innerCompilationCount += 1; - } - })); + expect(innerCompilationCount).toBe(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() { it('should only include elements without a matching transclusion element in default transclusion slot', function() { module(function() { From 4b5d5b9581010f9492f3e500d5c412580e84f645 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 6 Sep 2016 14:23:24 +0100 Subject: [PATCH 2/3] feat($compile): add `preAssignBindingsEnabled` option A new option 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 enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See #14580 --- src/ng/compile.js | 16 +++--- test/ng/compileSpec.js | 128 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 121 insertions(+), 23 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 850b608d1618..ec3e828db87e 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1371,10 +1371,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method - * @name $compileProvider#preAssignBindings + * @name $compileProvider#preAssignBindingsEnabled * - * @param {boolean=} enabled update the preAssignBindings state if provided, otherwise just return the - * current preAssignBindings state + * @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 @@ -1389,13 +1389,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * The default value is true in Angular 1.5.x but will switch to false in Angular 1.6.x. */ - var preAssignBindings = true; - this.preAssignBindings = function(enabled) { + var preAssignBindingsEnabled = true; + this.preAssignBindingsEnabled = function(enabled) { if (isDefined(enabled)) { - preAssignBindings = enabled; + preAssignBindingsEnabled = enabled; return this; } - return preAssignBindings; + return preAssignBindingsEnabled; }; @@ -2708,7 +2708,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { if (controller.identifier && bindings) { controller.bindingInfo = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index a0552427264a..16d437ac4ae3 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3845,10 +3845,10 @@ describe('$compile', function() { }); }); - forEach([true, false], function(preAssignBindings) { - describe((preAssignBindings ? 'with' : 'without') + ' pre-assigned bindings', function() { + forEach([true, false], function(preAssignBindingsEnabled) { + describe((preAssignBindingsEnabled ? 'with' : 'without') + ' pre-assigned bindings', function() { beforeEach(module(function($compileProvider) { - $compileProvider.preAssignBindings(preAssignBindings); + $compileProvider.preAssignBindingsEnabled(preAssignBindingsEnabled); })); describe('controller lifecycle hooks', function() { @@ -5716,7 +5716,7 @@ describe('$compile', function() { expect(this.str).toBe('Hello, world!'); expect(this.fn()).toBe('called!'); }; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -5742,6 +5742,104 @@ describe('$compile', function() { }); + 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': '
')($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; @@ -5846,7 +5944,7 @@ describe('$compile', function() { expect(this.str).toBe('Hello, world!'); expect(this.fn()).toBe('called!'); }; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -5993,7 +6091,7 @@ describe('$compile', function() { expect(this.fn()).toBe('called!'); }; controllerCalled = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6045,7 +6143,7 @@ describe('$compile', function() { expect(this.fn()).toBe('called!'); }; controllerCalled = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6100,7 +6198,7 @@ describe('$compile', function() { expect(this.fn()).toBe('called!'); }; controller1Called = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6123,7 +6221,7 @@ describe('$compile', function() { expect(this.fn()).toBe('second called!'); }; controller2Called = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6177,7 +6275,7 @@ describe('$compile', function() { expect(this.fn()).toBe('called!'); }; controller1Called = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6200,7 +6298,7 @@ describe('$compile', function() { expect(this.fn()).toBe('second called!'); }; controller2Called = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6254,7 +6352,7 @@ describe('$compile', function() { expect(this.fn()).toBe('called!'); }; controller1Called = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6278,7 +6376,7 @@ describe('$compile', function() { expect(this.fn()).toBe('second called!'); }; controller2Called = true; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.check(); } else { this.$onInit = this.check; @@ -6598,7 +6696,7 @@ describe('$compile', function() { this.initProp = function() { this.prop = this.prop || 'default'; }; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.initProp(); } else { this.$onInit = this.initProp; @@ -6636,7 +6734,7 @@ describe('$compile', function() { this.getProp = function() { return self.prop; }; - if (preAssignBindings) { + if (preAssignBindingsEnabled) { this.initProp(); } else { this.$onInit = this.initProp; From 1a06ea7613e980431f962d7ac441b87500a35758 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 7 Sep 2016 08:42:19 +0100 Subject: [PATCH 3/3] test($compile): add tests for provider settings See https://github.com/angular/angular.js/pull/15095#issuecomment-244970426 --- test/ng/compileSpec.js | 54 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 16d437ac4ae3..9519585938d2 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -151,6 +151,60 @@ describe('$compile', function() { describe('configuration', function() { + it('should allow aHrefSanitizationWhitelist to be configured', function() { + module(function($compileProvider) { + expect($compileProvider.aHrefSanitizationWhitelist()).toEqual(/^\s*(https?|ftp|mailto|tel|file):/); // the default + $compileProvider.aHrefSanitizationWhitelist(/other/); + expect($compileProvider.aHrefSanitizationWhitelist()).toEqual(/other/); + }); + inject(); + }); + + it('should allow debugInfoEnabled to be configured', function() { + module(function($compileProvider) { + expect($compileProvider.debugInfoEnabled()).toBe(true); // the default + $compileProvider.debugInfoEnabled(false); + expect($compileProvider.debugInfoEnabled()).toBe(false); + }); + inject(); + }); + + it('should allow preAssignBindingsEnabled to be configured', function() { + module(function($compileProvider) { + expect($compileProvider.preAssignBindingsEnabled()).toBe(true); // the default + $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 + $compileProvider.onChangesTtl(2); + expect($compileProvider.onChangesTtl()).toBe(2); + }); + inject(); + }); + + it('should allow commentDirectivesEnabled to be configured', function() { + module(function($compileProvider) { + expect($compileProvider.commentDirectivesEnabled()).toBe(true); // the default + $compileProvider.commentDirectivesEnabled(false); + expect($compileProvider.commentDirectivesEnabled()).toBe(false); + }); + inject(); + }); + + it('should allow cssClassDirectivesEnabled to be configured', function() { + module(function($compileProvider) { + expect($compileProvider.cssClassDirectivesEnabled()).toBe(true); // the default + $compileProvider.cssClassDirectivesEnabled(false); + expect($compileProvider.cssClassDirectivesEnabled()).toBe(false); + }); + inject(); + }); + it('should register a directive', function() { module(function() { directive('div', function(log) {