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' +
+ * '
';
+ $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 =
+ '