diff --git a/angularFiles.js b/angularFiles.js index 0233722adfc4..01d9dfd3f0f3 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -74,6 +74,7 @@ var angularFiles = { 'src/ng/directive/ngNonBindable.js', 'src/ng/directive/ngOptions.js', 'src/ng/directive/ngPluralize.js', + 'src/ng/directive/ngRef.js', 'src/ng/directive/ngRepeat.js', 'src/ng/directive/ngShowHide.js', 'src/ng/directive/ngStyle.js', diff --git a/docs/content/error/ngRef/noctrl.ngdoc b/docs/content/error/ngRef/noctrl.ngdoc new file mode 100644 index 000000000000..29d19a9ae134 --- /dev/null +++ b/docs/content/error/ngRef/noctrl.ngdoc @@ -0,0 +1,17 @@ +@ngdoc error +@name ngRef:noctrl +@fullName A controller for the value of `ngRefRead` could not be found on the element. +@description + +This error occurs when the {@link ng.ngRef ngRef directive} specifies +a value in `ngRefRead` that cannot be resolved to a directive / component controller. + +Causes for this error can be: + +1. Your `ngRefRead` value has a typo. +2. You have a typo in the *registered* directive / component name. +3. The directive / component does not have a controller. + +Note that `ngRefRead` takes the name of the component / directive, not the name of controller, and +also not the combination of directive and 'Controller'. For example, for a directive called 'myDirective', +the correct declaration is `
`. diff --git a/docs/content/error/ngRef/nonassign.ngdoc b/docs/content/error/ngRef/nonassign.ngdoc new file mode 100644 index 000000000000..9c1c52ee35b7 --- /dev/null +++ b/docs/content/error/ngRef/nonassign.ngdoc @@ -0,0 +1,27 @@ +@ngdoc error +@name ngRef:nonassign +@fullName Non-Assignable Expression +@description + +This error occurs when ngRef defines an expression that is not-assignable. + +In order for ngRef to work, it must be possible to write the reference into the path defined with the expression. + +For example, the following expressions are non-assignable: + +``` + + + + + + + +``` + +To resolve this error, use a path expression that is assignable: + +``` + + +``` diff --git a/src/AngularPublic.js b/src/AngularPublic.js index c18889911a50..dca14bdd6ffd 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -28,6 +28,7 @@ ngInitDirective, ngNonBindableDirective, ngPluralizeDirective, + ngRefDirective, ngRepeatDirective, ngShowDirective, ngStyleDirective, @@ -194,6 +195,7 @@ function publishExternalAPI(angular) { ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, ngPluralize: ngPluralizeDirective, + ngRef: ngRefDirective, ngRepeat: ngRepeatDirective, ngShow: ngShowDirective, ngStyle: ngStyleDirective, diff --git a/src/ng/directive/ngRef.js b/src/ng/directive/ngRef.js new file mode 100644 index 000000000000..4b3c7a746ba4 --- /dev/null +++ b/src/ng/directive/ngRef.js @@ -0,0 +1,296 @@ +'use strict'; + +/** + * @ngdoc directive + * @name ngRef + * @restrict A + * + * @description + * The `ngRef` attribute tells AngularJS to assign the controller of a component (or a directive) + * to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM + * element to the scope. + * + * If the element with `ngRef` is destroyed `null` is assigned to the property. + * + * Note that if you want to assign from a child into the parent scope, you must initialize the + * target property on the parent scope, otherwise `ngRef` will assign on the child scope. + * This commonly happens when assigning elements or components wrapped in {@link ngIf} or + * {@link ngRepeat}. See the second example below. + * + * + * @element ANY + * @param {string} ngRef property name - A valid AngularJS expression identifier to which the + * controller or jqlite-wrapped DOM element will be bound. + * @param {string=} ngRefRead read value - The name of a directive (or component) on this element, + * or the special string `$element`. If a name is provided, `ngRef` will + * assign the matching controller. If `$element` is provided, the element + * itself is assigned (even if a controller is available). + * + * + * @example + * ### Simple toggle + * This example shows how the controller of the component toggle + * is reused in the template through the scope to use its logic. + * + * + * + * + *
+ * You are using a component in the same template to show it. + *
+ *
+ * + * angular.module('myApp', []) + * .component('myToggle', { + * controller: function ToggleController() { + * var opened = false; + * this.isOpen = function() { return opened; }; + * this.toggle = function() { opened = !opened; }; + * } + * }); + * + * + * it('should publish the toggle into the scope', function() { + * var toggle = element(by.buttonText('Toggle')); + * expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false); + * toggle.click(); + * expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true); + * }); + * + *
+ * + * @example + * ### ngRef inside scopes + * This example shows how `ngRef` works with child scopes. The `ngRepeat`-ed `myWrapper` components + * are assigned to the scope of `myRoot`, because the `toggles` property has been initialized. + * The repeated `myToggle` components are published to the child scopes created by `ngRepeat`. + * `ngIf` behaves similarly - the assignment of `myToggle` happens in the `ngIf` child scope, + * because the target property has not been initialized on the `myRoot` component controller. + * + * + * + * + * + * + * angular.module('myApp', []) + * .component('myRoot', { + * templateUrl: 'root.html', + * controller: function() { + * this.wrappers = []; // initialize the array so that the wrappers are assigned into the parent scope + * } + * }) + * .component('myToggle', { + * template: 'myToggle', + * transclude: true, + * controller: function ToggleController() { + * var opened = false; + * this.isOpen = function() { return opened; }; + * this.toggle = function() { opened = !opened; }; + * } + * }) + * .component('myWrapper', { + * transclude: true, + * template: 'myWrapper' + + * '
ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}
' + + * '' + * }); + *
+ * + * myRoot + * Outer Toggle + *
outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}
+ *
wrappers assigned to root
+ *
+ * wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}} + *
+ * + *
    + *
  • + * ngRepeat + *
    outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}
    + * ngRepeat Toggle {{$index + 1}} + *
  • + *
+ * + *
ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's + * assigned to the child scope created by ngIf. + *
+ *
+ ngIf + * ngIf Toggle + *
ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}
+ *
outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}
+ *
+ * + * + * ul { + * list-style: none; + * padding-left: 0; + * } + * + * li[ng-repeat] { + * background: lightgreen; + * padding: 8px; + * margin: 8px; + * } + * + * [ng-if] { + * background: lightgrey; + * padding: 8px; + * } + * + * my-root { + * background: lightgoldenrodyellow; + * padding: 8px; + * display: block; + * } + * + * my-wrapper { + * background: lightsalmon; + * padding: 8px; + * display: block; + * } + * + * my-toggle { + * background: lightblue; + * padding: 8px; + * display: block; + * } + * + * + * var OuterToggle = function() { + * this.toggle = function() { + * element(by.buttonText('Outer Toggle')).click(); + * }; + * this.isOpen = function() { + * return element.all(by.binding('outerToggle.isOpen()')).first().getText(); + * }; + * }; + * var NgRepeatToggle = function(i) { + * var parent = element.all(by.repeater('(index, value) in [1,2,3]')).get(i - 1); + * this.toggle = function() { + * element(by.buttonText('ngRepeat Toggle ' + i)).click(); + * }; + * this.isOpen = function() { + * return parent.element(by.binding('ngRepeatToggle.isOpen() | json')).getText(); + * }; + * this.isOuterOpen = function() { + * return parent.element(by.binding('outerToggle.isOpen() | json')).getText(); + * }; + * }; + * var NgRepeatToggles = function() { + * var toggles = [1,2,3].map(function(i) { return new NgRepeatToggle(i); }); + * this.forEach = function(fn) { + * toggles.forEach(fn); + * }; + * this.isOuterOpen = function(i) { + * return toggles[i - 1].isOuterOpen(); + * }; + * }; + * var NgIfToggle = function() { + * var parent = element(by.css('[ng-if]')); + * this.toggle = function() { + * element(by.buttonText('ngIf Toggle')).click(); + * }; + * this.isOpen = function() { + * return by.binding('ngIfToggle.isOpen() | json').getText(); + * }; + * this.isOuterOpen = function() { + * return parent.element(by.binding('outerToggle.isOpen() | json')).getText(); + * }; + * }; + * + * it('should toggle the outer toggle', function() { + * var outerToggle = new OuterToggle(); + * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false'); + * outerToggle.toggle(); + * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true'); + * }); + * + * it('should toggle all outer toggles', function() { + * var outerToggle = new OuterToggle(); + * var repeatToggles = new NgRepeatToggles(); + * var ifToggle = new NgIfToggle(); + * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false'); + * expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): false'); + * expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): false'); + * expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): false'); + * expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false'); + * outerToggle.toggle(); + * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true'); + * expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): true'); + * expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): true'); + * expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): true'); + * expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): true'); + * }); + * + * it('should toggle each repeat iteration separately', function() { + * var repeatToggles = new NgRepeatToggles(); + * + * repeatToggles.forEach(function(repeatToggle) { + * expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): false'); + * expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false'); + * repeatToggle.toggle(); + * expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): true'); + * expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false'); + * }); + * }); + * + * + * + */ + +var ngRefMinErr = minErr('ngRef'); + +var ngRefDirective = ['$parse', function($parse) { + return { + priority: -1, // Needed for compatibility with element transclusion on the same element + restrict: 'A', + compile: function(tElement, tAttrs) { + // Get the expected controller name, converts into "someThing" + var controllerName = directiveNormalize(nodeName_(tElement)); + + // Get the expression for value binding + var getter = $parse(tAttrs.ngRef); + var setter = getter.assign || function() { + throw ngRefMinErr('nonassign', 'Expression in ngRef="{0}" is non-assignable!', tAttrs.ngRef); + }; + + return function(scope, element, attrs) { + var refValue; + + if (attrs.hasOwnProperty('ngRefRead')) { + if (attrs.ngRefRead === '$element') { + refValue = element; + } else { + refValue = element.data('$' + attrs.ngRefRead + 'Controller'); + + if (!refValue) { + throw ngRefMinErr( + 'noctrl', + 'The controller for ngRefRead="{0}" could not be found on ngRef="{1}"', + attrs.ngRefRead, + tAttrs.ngRef + ); + } + } + } else { + refValue = element.data('$' + controllerName + 'Controller'); + } + + refValue = refValue || element; + + setter(scope, refValue); + + // when the element is removed, remove it (nullify it) + element.on('$destroy', function() { + // only remove it if value has not changed, + // because animations (and other procedures) may duplicate elements + if (getter(scope) === refValue) { + setter(scope, null); + } + }); + }; + } + }; +}]; diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js index ac297609e579..5010b212f6d2 100644 --- a/test/helpers/matchers.js +++ b/test/helpers/matchers.js @@ -313,6 +313,7 @@ beforeEach(function() { function generateCompare(isNot) { return function(actual, namespace, code, content) { + var matcher = new MinErrMatcher(isNot, namespace, code, content, { inputType: 'error', expectedAction: 'equal', diff --git a/test/ng/directive/ngRefSpec.js b/test/ng/directive/ngRefSpec.js new file mode 100644 index 000000000000..ef62fae99cad --- /dev/null +++ b/test/ng/directive/ngRefSpec.js @@ -0,0 +1,561 @@ +'use strict'; + +describe('ngRef', function() { + + beforeEach(function() { + jasmine.addMatchers({ + toEqualJq: function(util) { + return { + compare: function(actual, expected) { + // Jquery <= 2.2 objects add a context property that is irrelevant for equality + if (actual && actual.hasOwnProperty('context')) { + delete actual.context; + } + + if (expected && expected.hasOwnProperty('context')) { + delete expected.context; + } + + return { + pass: util.equals(actual, expected) + }; + } + }; + } + }); + }); + + describe('on a component', function() { + + var myComponentController, attributeDirectiveController, $rootScope, $compile; + + beforeEach(module(function($compileProvider) { + $compileProvider.component('myComponent', { + template: 'foo', + controller: function() { + myComponentController = this; + } + }); + + $compileProvider.directive('attributeDirective', function() { + return { + restrict: 'A', + controller: function() { + attributeDirectiveController = this; + } + }; + }); + + })); + + beforeEach(inject(function(_$compile_, _$rootScope_) { + $rootScope = _$rootScope_; + $compile = _$compile_; + })); + + it('should bind in the current scope the controller of a component', function() { + $rootScope.$ctrl = 'undamaged'; + + $compile('')($rootScope); + expect($rootScope.$ctrl).toBe('undamaged'); + expect($rootScope.myComponentRef).toBe(myComponentController); + }); + + it('should throw if the expression is not assignable', function() { + expect(function() { + $compile('')($rootScope); + }).toThrowMinErr('ngRef', 'nonassign', 'Expression in ngRef="\'hello\'" is non-assignable!'); + }); + + it('should work with non:normalized entity name', function() { + $compile('')($rootScope); + expect($rootScope.myComponent1).toBe(myComponentController); + }); + + it('should work with data-non-normalized entity name', function() { + $compile('')($rootScope); + expect($rootScope.myComponent2).toBe(myComponentController); + }); + + it('should work with x-non-normalized entity name', function() { + $compile('')($rootScope); + expect($rootScope.myComponent3).toBe(myComponentController); + }); + + it('should work with data-non-normalized attribute name', function() { + $compile('')($rootScope); + expect($rootScope.myComponent1).toBe(myComponentController); + }); + + it('should work with x-non-normalized attribute name', function() { + $compile('')($rootScope); + expect($rootScope.myComponent2).toBe(myComponentController); + }); + + it('should not bind the controller of an attribute directive', function() { + $compile('')($rootScope); + expect($rootScope.myComponentRef).toBe(myComponentController); + }); + + it('should not leak to parent scopes', function() { + var template = + '
' + + '' + + '
'; + $compile(template)($rootScope); + expect($rootScope.myComponent).toBe(undefined); + }); + + it('should nullify the variable once the component is destroyed', function() { + var template = '
'; + + var element = $compile(template)($rootScope); + expect($rootScope.myComponent).toBe(myComponentController); + + var componentElement = element.children(); + var isolateScope = componentElement.isolateScope(); + componentElement.remove(); + isolateScope.$destroy(); + expect($rootScope.myComponent).toBe(null); + }); + + it('should be compatible with entering/leaving components', inject(function($animate) { + var template = ''; + $rootScope.$ctrl = {}; + var parent = $compile('
')($rootScope); + + var leaving = $compile(template)($rootScope); + var leavingController = myComponentController; + + $animate.enter(leaving, parent); + expect($rootScope.myComponent).toBe(leavingController); + + var entering = $compile(template)($rootScope); + var enteringController = myComponentController; + + $animate.enter(entering, parent); + $animate.leave(leaving, parent); + expect($rootScope.myComponent).toBe(enteringController); + })); + + it('should allow binding to a nested property', function() { + $rootScope.obj = {}; + + $compile('')($rootScope); + expect($rootScope.obj.myComponent).toBe(myComponentController); + }); + + }); + + it('should bind the jqlite wrapped DOM element if there is no component', inject(function($compile, $rootScope) { + + var el = $compile('my text')($rootScope); + + expect($rootScope.mySpan).toEqualJq(el); + expect($rootScope.mySpan[0].textContent).toBe('my text'); + })); + + it('should nullify the expression value if the DOM element is destroyed', inject(function($compile, $rootScope) { + var element = $compile('
my text
')($rootScope); + element.children().remove(); + expect($rootScope.mySpan).toBe(null); + })); + + it('should bind the controller of an element directive', function() { + var myDirectiveController; + + module(function($compileProvider) { + $compileProvider.directive('myDirective', function() { + return { + controller: function() { + myDirectiveController = this; + } + }; + }); + }); + + inject(function($compile, $rootScope) { + $compile('')($rootScope); + + expect($rootScope.myDirective).toBe(myDirectiveController); + }); + }); + + describe('ngRefRead', function() { + + it('should bind the element instead of the controller of a component if ngRefRead="$element" is set', function() { + + module(function($compileProvider) { + + $compileProvider.component('myComponent', { + template: 'my text', + controller: function() {} + }); + }); + + inject(function($compile, $rootScope) { + + var el = $compile('')($rootScope); + expect($rootScope.myEl).toEqualJq(el); + expect($rootScope.myEl[0].textContent).toBe('my text'); + }); + }); + + + it('should bind the element instead an element-directive controller if ngRefRead="$element" is set', function() { + + module(function($compileProvider) { + $compileProvider.directive('myDirective', function() { + return { + restrict: 'E', + template: 'my text', + controller: function() {} + }; + }); + }); + + inject(function($compile, $rootScope) { + var el = $compile('')($rootScope); + + expect($rootScope.myEl).toEqualJq(el); + expect($rootScope.myEl[0].textContent).toBe('my text'); + }); + }); + + + it('should bind an attribute-directive controller if ngRefRead="controllerName" is set', function() { + var attrDirective1Controller; + + module(function($compileProvider) { + $compileProvider.directive('elementDirective', function() { + return { + restrict: 'E', + template: 'my text', + controller: function() {} + }; + }); + + $compileProvider.directive('attributeDirective1', function() { + return { + restrict: 'A', + controller: function() { + attrDirective1Controller = this; + } + }; + }); + + $compileProvider.directive('attributeDirective2', function() { + return { + restrict: 'A', + controller: function() {} + }; + }); + + }); + + inject(function($compile, $rootScope) { + var el = $compile('')($rootScope); + + expect($rootScope.myController).toBe(attrDirective1Controller); + }); + }); + + it('should throw if no controller is found for the ngRefRead value', function() { + + module(function($compileProvider) { + $compileProvider.directive('elementDirective', function() { + return { + restrict: 'E', + template: 'my text', + controller: function() {} + }; + }); + }); + + inject(function($compile, $rootScope) { + + expect(function() { + $compile('')($rootScope); + }).toThrowMinErr('ngRef', 'noctrl', 'The controller for ngRefRead="attribute" could not be found on ngRef="myController"'); + + }); + }); + + }); + + + it('should bind the jqlite element if the controller is on an attribute-directive', function() { + var myDirectiveController; + + module(function($compileProvider) { + $compileProvider.directive('myDirective', function() { + return { + restrict: 'A', + template: 'my text', + controller: function() { + myDirectiveController = this; + } + }; + }); + }); + + inject(function($compile, $rootScope) { + var el = $compile('
')($rootScope); + + expect(myDirectiveController).toBeDefined(); + expect($rootScope.myEl).toEqualJq(el); + expect($rootScope.myEl[0].textContent).toBe('my text'); + }); + }); + + + it('should bind the jqlite element if the controller is on an class-directive', function() { + var myDirectiveController; + + module(function($compileProvider) { + $compileProvider.directive('myDirective', function() { + return { + restrict: 'C', + template: 'my text', + controller: function() { + myDirectiveController = this; + } + }; + }); + }); + + inject(function($compile, $rootScope) { + var el = $compile('
')($rootScope); + + expect(myDirectiveController).toBeDefined(); + expect($rootScope.myEl).toEqualJq(el); + expect($rootScope.myEl[0].textContent).toBe('my text'); + }); + }); + + describe('transclusion', function() { + + it('should work with simple transclusion', function() { + module(function($compileProvider) { + $compileProvider + .component('myComponent', { + transclude: true, + template: '', + controller: function() { + this.text = 'SUCCESS'; + } + }); + }); + + inject(function($compile, $rootScope) { + var template = '{{myComponent.text}}'; + var element = $compile(template)($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('SUCCESS'); + dealoc(element); + }); + }); + + it('should be compatible with element transclude components', function() { + + module(function($compileProvider) { + $compileProvider + .component('myComponent', { + transclude: 'element', + controller: function($animate, $element, $transclude) { + this.text = 'SUCCESS'; + this.$postLink = function() { + $transclude(function(clone, newScope) { + $animate.enter(clone, $element.parent(), $element); + }); + }; + } + }); + }); + + inject(function($compile, $rootScope) { + var template = + '
' + + '' + + '{{myComponent.text}}' + + '' + + '
'; + var element = $compile(template)($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('SUCCESS'); + dealoc(element); + }); + }); + + it('should be compatible with ngIf and transclusion on same element', function() { + module(function($compileProvider) { + $compileProvider.component('myComponent', { + template: '', + transclude: true, + controller: function($scope) { + this.text = 'SUCCESS'; + } + }); + }); + + inject(function($compile, $rootScope) { + var template = + '
' + + '' + + '{{myComponent.text}}' + + '' + + '
'; + var element = $compile(template)($rootScope); + + $rootScope.$apply('present = false'); + expect(element.text()).toBe(''); + $rootScope.$apply('present = true'); + expect(element.text()).toBe('SUCCESS'); + $rootScope.$apply('present = false'); + expect(element.text()).toBe(''); + $rootScope.$apply('present = true'); + expect(element.text()).toBe('SUCCESS'); + dealoc(element); + }); + }); + + it('should be compatible with element transclude & destroy components', function() { + var myComponentController; + module(function($compileProvider) { + $compileProvider + .component('myTranscludingComponent', { + transclude: 'element', + controller: function($animate, $element, $transclude) { + myComponentController = this; + + var currentClone, currentScope; + this.transclude = function(text) { + this.text = text; + $transclude(function(clone, newScope) { + currentClone = clone; + currentScope = newScope; + $animate.enter(clone, $element.parent(), $element); + }); + }; + this.destroy = function() { + currentClone.remove(); + currentScope.$destroy(); + }; + } + }); + }); + + inject(function($compile, $rootScope) { + var template = + '
' + + '' + + '{{myComponent.text}}' + + '' + + '
'; + var element = $compile(template)($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe(''); + + myComponentController.transclude('transcludedOk'); + $rootScope.$apply(); + expect(element.text()).toBe('transcludedOk'); + + myComponentController.destroy(); + $rootScope.$apply(); + expect(element.text()).toBe(''); + }); + }); + + it('should be compatible with element transclude directives', function() { + module(function($compileProvider) { + $compileProvider + .directive('myDirective', function($animate) { + return { + transclude: 'element', + controller: function() { + this.text = 'SUCCESS'; + }, + link: function(scope, element, attrs, ctrl, $transclude) { + $transclude(function(clone, newScope) { + $animate.enter(clone, element.parent(), element); + }); + } + }; + }); + }); + + inject(function($compile, $rootScope) { + var template = + '
' + + '' + + '{{myDirective.text}}' + + '' + + '
'; + var element = $compile(template)($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe('SUCCESS'); + dealoc(element); + }); + }); + + }); + + it('should work with components with templates via $http', function() { + module(function($compileProvider) { + $compileProvider.component('httpComponent', { + templateUrl: 'template.html', + controller: function() { + this.me = true; + } + }); + }); + + inject(function($compile, $httpBackend, $rootScope) { + var template = '
'; + var element = $compile(template)($rootScope); + $httpBackend.expect('GET', 'template.html').respond('ok'); + $rootScope.$apply(); + expect($rootScope.controller).toBeUndefined(); + $httpBackend.flush(); + expect($rootScope.controller.me).toBe(true); + dealoc(element); + }); + }); + + + it('should work with ngRepeat-ed components', function() { + var controllers = []; + + module(function($compileProvider) { + $compileProvider.component('myComponent', { + template: 'foo', + controller: function() { + controllers.push(this); + } + }); + }); + + + inject(function($compile, $rootScope) { + $rootScope.elements = [0,1,2,3,4]; + $rootScope.controllers = []; // Initialize the array because ngRepeat creates a child scope + + var template = '
'; + var element = $compile(template)($rootScope); + $rootScope.$apply(); + + expect($rootScope.controllers).toEqual(controllers); + + $rootScope.$apply('elements = []'); + + expect($rootScope.controllers).toEqual([null, null, null, null, null]); + }); + }); + +});