Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit aa6adc7

Browse files
authored
feat(ngRef): add directive to publish controller, or element into scope
Thanks to @drpicox for the original implementation: PR #14080 Closes #16511
1 parent a7de87d commit aa6adc7

File tree

7 files changed

+905
-0
lines changed

7 files changed

+905
-0
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ var angularFiles = {
7474
'src/ng/directive/ngNonBindable.js',
7575
'src/ng/directive/ngOptions.js',
7676
'src/ng/directive/ngPluralize.js',
77+
'src/ng/directive/ngRef.js',
7778
'src/ng/directive/ngRepeat.js',
7879
'src/ng/directive/ngShowHide.js',
7980
'src/ng/directive/ngStyle.js',

docs/content/error/ngRef/noctrl.ngdoc

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@ngdoc error
2+
@name ngRef:noctrl
3+
@fullName A controller for the value of `ngRefRead` could not be found on the element.
4+
@description
5+
6+
This error occurs when the {@link ng.ngRef ngRef directive} specifies
7+
a value in `ngRefRead` that cannot be resolved to a directive / component controller.
8+
9+
Causes for this error can be:
10+
11+
1. Your `ngRefRead` value has a typo.
12+
2. You have a typo in the *registered* directive / component name.
13+
3. The directive / component does not have a controller.
14+
15+
Note that `ngRefRead` takes the name of the component / directive, not the name of controller, and
16+
also not the combination of directive and 'Controller'. For example, for a directive called 'myDirective',
17+
the correct declaration is `<div ng-ref="$ctrl.ref" ng-ref-read="myDirective">`.
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@ngdoc error
2+
@name ngRef:nonassign
3+
@fullName Non-Assignable Expression
4+
@description
5+
6+
This error occurs when ngRef defines an expression that is not-assignable.
7+
8+
In order for ngRef to work, it must be possible to write the reference into the path defined with the expression.
9+
10+
For example, the following expressions are non-assignable:
11+
12+
```
13+
<my-directive ng-ref="{}"></my-directive>
14+
15+
<my-directive ng-ref="myFn()"></my-directive>
16+
17+
<!-- missing attribute value is also invalid -->
18+
<my-directive ng-ref></my-directive>
19+
20+
```
21+
22+
To resolve this error, use a path expression that is assignable:
23+
24+
```
25+
<my-directive ng-ref="$ctrl.reference"></my-directive>
26+
27+
```

src/AngularPublic.js

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ngInitDirective,
2929
ngNonBindableDirective,
3030
ngPluralizeDirective,
31+
ngRefDirective,
3132
ngRepeatDirective,
3233
ngShowDirective,
3334
ngStyleDirective,
@@ -194,6 +195,7 @@ function publishExternalAPI(angular) {
194195
ngInit: ngInitDirective,
195196
ngNonBindable: ngNonBindableDirective,
196197
ngPluralize: ngPluralizeDirective,
198+
ngRef: ngRefDirective,
197199
ngRepeat: ngRepeatDirective,
198200
ngShow: ngShowDirective,
199201
ngStyle: ngStyleDirective,

src/ng/directive/ngRef.js

+296
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
'use strict';
2+
3+
/**
4+
* @ngdoc directive
5+
* @name ngRef
6+
* @restrict A
7+
*
8+
* @description
9+
* The `ngRef` attribute tells AngularJS to assign the controller of a component (or a directive)
10+
* to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM
11+
* element to the scope.
12+
*
13+
* If the element with `ngRef` is destroyed `null` is assigned to the property.
14+
*
15+
* Note that if you want to assign from a child into the parent scope, you must initialize the
16+
* target property on the parent scope, otherwise `ngRef` will assign on the child scope.
17+
* This commonly happens when assigning elements or components wrapped in {@link ngIf} or
18+
* {@link ngRepeat}. See the second example below.
19+
*
20+
*
21+
* @element ANY
22+
* @param {string} ngRef property name - A valid AngularJS expression identifier to which the
23+
* controller or jqlite-wrapped DOM element will be bound.
24+
* @param {string=} ngRefRead read value - The name of a directive (or component) on this element,
25+
* or the special string `$element`. If a name is provided, `ngRef` will
26+
* assign the matching controller. If `$element` is provided, the element
27+
* itself is assigned (even if a controller is available).
28+
*
29+
*
30+
* @example
31+
* ### Simple toggle
32+
* This example shows how the controller of the component toggle
33+
* is reused in the template through the scope to use its logic.
34+
* <example name="ng-ref-component" module="myApp">
35+
* <file name="index.html">
36+
* <my-toggle ng-ref="myToggle"></my-toggle>
37+
* <button ng-click="myToggle.toggle()">Toggle</button>
38+
* <div ng-show="myToggle.isOpen()">
39+
* You are using a component in the same template to show it.
40+
* </div>
41+
* </file>
42+
* <file name="index.js">
43+
* angular.module('myApp', [])
44+
* .component('myToggle', {
45+
* controller: function ToggleController() {
46+
* var opened = false;
47+
* this.isOpen = function() { return opened; };
48+
* this.toggle = function() { opened = !opened; };
49+
* }
50+
* });
51+
* </file>
52+
* <file name="protractor.js" type="protractor">
53+
* it('should publish the toggle into the scope', function() {
54+
* var toggle = element(by.buttonText('Toggle'));
55+
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false);
56+
* toggle.click();
57+
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true);
58+
* });
59+
* </file>
60+
* </example>
61+
*
62+
* @example
63+
* ### ngRef inside scopes
64+
* This example shows how `ngRef` works with child scopes. The `ngRepeat`-ed `myWrapper` components
65+
* are assigned to the scope of `myRoot`, because the `toggles` property has been initialized.
66+
* The repeated `myToggle` components are published to the child scopes created by `ngRepeat`.
67+
* `ngIf` behaves similarly - the assignment of `myToggle` happens in the `ngIf` child scope,
68+
* because the target property has not been initialized on the `myRoot` component controller.
69+
*
70+
* <example name="ng-ref-scopes" module="myApp">
71+
* <file name="index.html">
72+
* <my-root></my-root>
73+
* </file>
74+
* <file name="index.js">
75+
* angular.module('myApp', [])
76+
* .component('myRoot', {
77+
* templateUrl: 'root.html',
78+
* controller: function() {
79+
* this.wrappers = []; // initialize the array so that the wrappers are assigned into the parent scope
80+
* }
81+
* })
82+
* .component('myToggle', {
83+
* template: '<strong>myToggle</strong><button ng-click="$ctrl.toggle()" ng-transclude></button>',
84+
* transclude: true,
85+
* controller: function ToggleController() {
86+
* var opened = false;
87+
* this.isOpen = function() { return opened; };
88+
* this.toggle = function() { opened = !opened; };
89+
* }
90+
* })
91+
* .component('myWrapper', {
92+
* transclude: true,
93+
* template: '<strong>myWrapper</strong>' +
94+
* '<div>ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}</div>' +
95+
* '<my-toggle ng-ref="$ctrl.ngRepeatToggle"><ng-transclude></ng-transclude></my-toggle>'
96+
* });
97+
* </file>
98+
* <file name="root.html">
99+
* <strong>myRoot</strong>
100+
* <my-toggle ng-ref="$ctrl.outerToggle">Outer Toggle</my-toggle>
101+
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
102+
* <div><em>wrappers assigned to root</em><br>
103+
* <div ng-repeat="wrapper in $ctrl.wrappers">
104+
* wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}}
105+
* </div>
106+
*
107+
* <ul>
108+
* <li ng-repeat="(index, value) in [1,2,3]">
109+
* <strong>ngRepeat</strong>
110+
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
111+
* <my-wrapper ng-ref="$ctrl.wrappers[index]">ngRepeat Toggle {{$index + 1}}</my-wrapper>
112+
* </li>
113+
* </ul>
114+
*
115+
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's
116+
* assigned to the child scope created by ngIf.
117+
* </div>
118+
* <div ng-if="true">
119+
<strong>ngIf</strong>
120+
* <my-toggle ng-ref="ngIfToggle">ngIf Toggle</my-toggle>
121+
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}</div>
122+
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
123+
* </div>
124+
* </file>
125+
* <file name="styles.css">
126+
* ul {
127+
* list-style: none;
128+
* padding-left: 0;
129+
* }
130+
*
131+
* li[ng-repeat] {
132+
* background: lightgreen;
133+
* padding: 8px;
134+
* margin: 8px;
135+
* }
136+
*
137+
* [ng-if] {
138+
* background: lightgrey;
139+
* padding: 8px;
140+
* }
141+
*
142+
* my-root {
143+
* background: lightgoldenrodyellow;
144+
* padding: 8px;
145+
* display: block;
146+
* }
147+
*
148+
* my-wrapper {
149+
* background: lightsalmon;
150+
* padding: 8px;
151+
* display: block;
152+
* }
153+
*
154+
* my-toggle {
155+
* background: lightblue;
156+
* padding: 8px;
157+
* display: block;
158+
* }
159+
* </file>
160+
* <file name="protractor.js" type="protractor">
161+
* var OuterToggle = function() {
162+
* this.toggle = function() {
163+
* element(by.buttonText('Outer Toggle')).click();
164+
* };
165+
* this.isOpen = function() {
166+
* return element.all(by.binding('outerToggle.isOpen()')).first().getText();
167+
* };
168+
* };
169+
* var NgRepeatToggle = function(i) {
170+
* var parent = element.all(by.repeater('(index, value) in [1,2,3]')).get(i - 1);
171+
* this.toggle = function() {
172+
* element(by.buttonText('ngRepeat Toggle ' + i)).click();
173+
* };
174+
* this.isOpen = function() {
175+
* return parent.element(by.binding('ngRepeatToggle.isOpen() | json')).getText();
176+
* };
177+
* this.isOuterOpen = function() {
178+
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
179+
* };
180+
* };
181+
* var NgRepeatToggles = function() {
182+
* var toggles = [1,2,3].map(function(i) { return new NgRepeatToggle(i); });
183+
* this.forEach = function(fn) {
184+
* toggles.forEach(fn);
185+
* };
186+
* this.isOuterOpen = function(i) {
187+
* return toggles[i - 1].isOuterOpen();
188+
* };
189+
* };
190+
* var NgIfToggle = function() {
191+
* var parent = element(by.css('[ng-if]'));
192+
* this.toggle = function() {
193+
* element(by.buttonText('ngIf Toggle')).click();
194+
* };
195+
* this.isOpen = function() {
196+
* return by.binding('ngIfToggle.isOpen() | json').getText();
197+
* };
198+
* this.isOuterOpen = function() {
199+
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
200+
* };
201+
* };
202+
*
203+
* it('should toggle the outer toggle', function() {
204+
* var outerToggle = new OuterToggle();
205+
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
206+
* outerToggle.toggle();
207+
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
208+
* });
209+
*
210+
* it('should toggle all outer toggles', function() {
211+
* var outerToggle = new OuterToggle();
212+
* var repeatToggles = new NgRepeatToggles();
213+
* var ifToggle = new NgIfToggle();
214+
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
215+
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): false');
216+
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): false');
217+
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): false');
218+
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
219+
* outerToggle.toggle();
220+
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
221+
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): true');
222+
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): true');
223+
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): true');
224+
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): true');
225+
* });
226+
*
227+
* it('should toggle each repeat iteration separately', function() {
228+
* var repeatToggles = new NgRepeatToggles();
229+
*
230+
* repeatToggles.forEach(function(repeatToggle) {
231+
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): false');
232+
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
233+
* repeatToggle.toggle();
234+
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): true');
235+
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
236+
* });
237+
* });
238+
* </file>
239+
* </example>
240+
*
241+
*/
242+
243+
var ngRefMinErr = minErr('ngRef');
244+
245+
var ngRefDirective = ['$parse', function($parse) {
246+
return {
247+
priority: -1, // Needed for compatibility with element transclusion on the same element
248+
restrict: 'A',
249+
compile: function(tElement, tAttrs) {
250+
// Get the expected controller name, converts <data-some-thing> into "someThing"
251+
var controllerName = directiveNormalize(nodeName_(tElement));
252+
253+
// Get the expression for value binding
254+
var getter = $parse(tAttrs.ngRef);
255+
var setter = getter.assign || function() {
256+
throw ngRefMinErr('nonassign', 'Expression in ngRef="{0}" is non-assignable!', tAttrs.ngRef);
257+
};
258+
259+
return function(scope, element, attrs) {
260+
var refValue;
261+
262+
if (attrs.hasOwnProperty('ngRefRead')) {
263+
if (attrs.ngRefRead === '$element') {
264+
refValue = element;
265+
} else {
266+
refValue = element.data('$' + attrs.ngRefRead + 'Controller');
267+
268+
if (!refValue) {
269+
throw ngRefMinErr(
270+
'noctrl',
271+
'The controller for ngRefRead="{0}" could not be found on ngRef="{1}"',
272+
attrs.ngRefRead,
273+
tAttrs.ngRef
274+
);
275+
}
276+
}
277+
} else {
278+
refValue = element.data('$' + controllerName + 'Controller');
279+
}
280+
281+
refValue = refValue || element;
282+
283+
setter(scope, refValue);
284+
285+
// when the element is removed, remove it (nullify it)
286+
element.on('$destroy', function() {
287+
// only remove it if value has not changed,
288+
// because animations (and other procedures) may duplicate elements
289+
if (getter(scope) === refValue) {
290+
setter(scope, null);
291+
}
292+
});
293+
};
294+
}
295+
};
296+
}];

test/helpers/matchers.js

+1
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ beforeEach(function() {
313313

314314
function generateCompare(isNot) {
315315
return function(actual, namespace, code, content) {
316+
316317
var matcher = new MinErrMatcher(isNot, namespace, code, content, {
317318
inputType: 'error',
318319
expectedAction: 'equal',

0 commit comments

Comments
 (0)