feat(ngRef): add directive to publish controller, or element into scope

Thanks to @drpicox for the original implementation: PR #14080 

Closes #16511
This commit is contained in:
Martin Staffa
2018-06-05 15:00:12 +02:00
committed by GitHub
parent a7de87d3e8
commit aa6adc77ae
7 changed files with 905 additions and 0 deletions
+1
View File
@@ -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',
+17
View File
@@ -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 `<div ng-ref="$ctrl.ref" ng-ref-read="myDirective">`.
+27
View File
@@ -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:
```
<my-directive ng-ref="{}"></my-directive>
<my-directive ng-ref="myFn()"></my-directive>
<!-- missing attribute value is also invalid -->
<my-directive ng-ref></my-directive>
```
To resolve this error, use a path expression that is assignable:
```
<my-directive ng-ref="$ctrl.reference"></my-directive>
```
+2
View File
@@ -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,
+296
View File
@@ -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.
* <example name="ng-ref-component" module="myApp">
* <file name="index.html">
* <my-toggle ng-ref="myToggle"></my-toggle>
* <button ng-click="myToggle.toggle()">Toggle</button>
* <div ng-show="myToggle.isOpen()">
* You are using a component in the same template to show it.
* </div>
* </file>
* <file name="index.js">
* angular.module('myApp', [])
* .component('myToggle', {
* controller: function ToggleController() {
* var opened = false;
* this.isOpen = function() { return opened; };
* this.toggle = function() { opened = !opened; };
* }
* });
* </file>
* <file name="protractor.js" type="protractor">
* 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);
* });
* </file>
* </example>
*
* @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.
*
* <example name="ng-ref-scopes" module="myApp">
* <file name="index.html">
* <my-root></my-root>
* </file>
* <file name="index.js">
* 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: '<strong>myToggle</strong><button ng-click="$ctrl.toggle()" ng-transclude></button>',
* transclude: true,
* controller: function ToggleController() {
* var opened = false;
* this.isOpen = function() { return opened; };
* this.toggle = function() { opened = !opened; };
* }
* })
* .component('myWrapper', {
* transclude: true,
* template: '<strong>myWrapper</strong>' +
* '<div>ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}</div>' +
* '<my-toggle ng-ref="$ctrl.ngRepeatToggle"><ng-transclude></ng-transclude></my-toggle>'
* });
* </file>
* <file name="root.html">
* <strong>myRoot</strong>
* <my-toggle ng-ref="$ctrl.outerToggle">Outer Toggle</my-toggle>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* <div><em>wrappers assigned to root</em><br>
* <div ng-repeat="wrapper in $ctrl.wrappers">
* wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}}
* </div>
*
* <ul>
* <li ng-repeat="(index, value) in [1,2,3]">
* <strong>ngRepeat</strong>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* <my-wrapper ng-ref="$ctrl.wrappers[index]">ngRepeat Toggle {{$index + 1}}</my-wrapper>
* </li>
* </ul>
*
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's
* assigned to the child scope created by ngIf.
* </div>
* <div ng-if="true">
<strong>ngIf</strong>
* <my-toggle ng-ref="ngIfToggle">ngIf Toggle</my-toggle>
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}</div>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* </div>
* </file>
* <file name="styles.css">
* 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;
* }
* </file>
* <file name="protractor.js" type="protractor">
* 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');
* });
* });
* </file>
* </example>
*
*/
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 <data-some-thing> 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);
}
});
};
}
};
}];
+1
View File
@@ -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',
+561
View File
@@ -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('<my-component ng-ref="myComponentRef"></my-component>')($rootScope);
expect($rootScope.$ctrl).toBe('undamaged');
expect($rootScope.myComponentRef).toBe(myComponentController);
});
it('should throw if the expression is not assignable', function() {
expect(function() {
$compile('<my-component ng-ref="\'hello\'"></my-component>')($rootScope);
}).toThrowMinErr('ngRef', 'nonassign', 'Expression in ngRef="\'hello\'" is non-assignable!');
});
it('should work with non:normalized entity name', function() {
$compile('<my:component ng-ref="myComponent1"></my:component>')($rootScope);
expect($rootScope.myComponent1).toBe(myComponentController);
});
it('should work with data-non-normalized entity name', function() {
$compile('<data-my-component ng-ref="myComponent2"></data-my-component>')($rootScope);
expect($rootScope.myComponent2).toBe(myComponentController);
});
it('should work with x-non-normalized entity name', function() {
$compile('<x-my-component ng-ref="myComponent3"></x-my-component>')($rootScope);
expect($rootScope.myComponent3).toBe(myComponentController);
});
it('should work with data-non-normalized attribute name', function() {
$compile('<my-component data-ng-ref="myComponent1"></my-component>')($rootScope);
expect($rootScope.myComponent1).toBe(myComponentController);
});
it('should work with x-non-normalized attribute name', function() {
$compile('<my-component x-ng-ref="myComponent2"></my-component>')($rootScope);
expect($rootScope.myComponent2).toBe(myComponentController);
});
it('should not bind the controller of an attribute directive', function() {
$compile('<my-component attribute-directive-1 ng-ref="myComponentRef"></my-component>')($rootScope);
expect($rootScope.myComponentRef).toBe(myComponentController);
});
it('should not leak to parent scopes', function() {
var template =
'<div ng-if="true">' +
'<my-component ng-ref="myComponent"></my-component>' +
'</div>';
$compile(template)($rootScope);
expect($rootScope.myComponent).toBe(undefined);
});
it('should nullify the variable once the component is destroyed', function() {
var template = '<div><my-component ng-ref="myComponent"></my-component></div>';
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 = '<my-component ng-ref="myComponent"></my-component>';
$rootScope.$ctrl = {};
var parent = $compile('<div></div>')($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('<my-component ng-ref="obj.myComponent"></my-component>')($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('<span ng-ref="mySpan">my text</span>')($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('<div><span ng-ref="mySpan">my text</span></div>')($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('<my-directive ng-ref="myDirective"></my-directive>')($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('<my-component ng-ref="myEl" ng-ref-read="$element"></my-component>')($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('<my-directive ng-ref="myEl" ng-ref-read="$element"></my-directive>')($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('<element-directive' +
'attribute-directive-1' +
'attribute-directive-2' +
'ng-ref="myController"' +
'ng-ref-read="$element"></element-directive>')($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('<element-directive ' +
'ng-ref="myController"' +
'ng-ref-read="attribute"></element-directive>')($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('<div my-directive ng-ref="myEl"></div>')($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('<div class="my-directive" ng-ref="myEl"></div>')($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: '<ng-transclude></ng-transclude>',
controller: function() {
this.text = 'SUCCESS';
}
});
});
inject(function($compile, $rootScope) {
var template = '<my-component ng-ref="myComponent">{{myComponent.text}}</my-component>';
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 =
'<div>' +
'<my-component ng-ref="myComponent">' +
'{{myComponent.text}}' +
'</my-component>' +
'</div>';
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: '<ng-transclude></ng-transclude>',
transclude: true,
controller: function($scope) {
this.text = 'SUCCESS';
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-component ng-if="present" ng-ref="myComponent" >' +
'{{myComponent.text}}' +
'</my-component>' +
'</div>';
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 =
'<div>' +
'<my-transcluding-component ng-ref="myComponent">' +
'{{myComponent.text}}' +
'</my-transcluding-component>' +
'</div>';
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 =
'<div>' +
'<my-directive ng-ref="myDirective">' +
'{{myDirective.text}}' +
'</my-directive>' +
'</div>';
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 = '<div><http-component ng-ref="controller"></http-component></div>';
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 = '<div><my-component ng-repeat="(key, el) in elements" ng-ref="controllers[key]"></my-component></div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect($rootScope.controllers).toEqual(controllers);
$rootScope.$apply('elements = []');
expect($rootScope.controllers).toEqual([null, null, null, null, null]);
});
});
});