From ea8672fef804bf80d5f26618f1769fcecaf9bf0d Mon Sep 17 00:00:00 2001 From: David Rodenas Pico Date: Thu, 18 Feb 2016 18:11:59 +0100 Subject: [PATCH 1/5] feat(ngAs): new as directive to attach children component controllers to the current controller --- angularFiles.js | 1 + src/AngularPublic.js | 2 + src/ng/directive/ngAs.js | 148 +++++++++++++++++ test/ng/directive/ngAsSpec.js | 299 ++++++++++++++++++++++++++++++++++ 4 files changed, 450 insertions(+) create mode 100644 src/ng/directive/ngAs.js create mode 100644 test/ng/directive/ngAsSpec.js diff --git a/angularFiles.js b/angularFiles.js index 311a39139322..f1aac32da4ba 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -58,6 +58,7 @@ var angularFiles = { 'src/ng/directive/attrs.js', 'src/ng/directive/form.js', 'src/ng/directive/input.js', + 'src/ng/directive/ngAs.js', 'src/ng/directive/ngBind.js', 'src/ng/directive/ngChange.js', 'src/ng/directive/ngClass.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index c18889911a50..d897f812b3f5 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -12,6 +12,7 @@ scriptDirective, selectDirective, optionDirective, + ngAsDirective, ngBindDirective, ngBindHtmlDirective, ngBindTemplateDirective, @@ -179,6 +180,7 @@ function publishExternalAPI(angular) { script: scriptDirective, select: selectDirective, option: optionDirective, + ngAs: ngAsDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, diff --git a/src/ng/directive/ngAs.js b/src/ng/directive/ngAs.js new file mode 100644 index 000000000000..18b3e3dd77a0 --- /dev/null +++ b/src/ng/directive/ngAs.js @@ -0,0 +1,148 @@ +'use strict'; + +/** + * @ngdoc directive + * @name ngAs + * @restrict A + * + * @description + * The `ngAs` attribute tells Angular to assign element component controller + * to a given property. + * + * Using this directive you can use the controller of existing components + * in your template (children components). + * + * If the children component is destroyed + * a `null` is assigned to the property. + * + * Note that this is the reverse of `require:`: + * with `require:` is the children who references the parent + * but with `ngAs`is the parent who references the children. + * It is very useful when you want to reuse the same component + * in different situations, + * and they do not need to know which exact parent they have. + * + * + * @element ANY + * @param {expression} ngAs {@link guide/expression Expression} to assign the controller. + * + * + * @example + * ### Use inside the scope + * 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 children component to show it.
+ *
+ * + * angular.module('ngAsExample', []) + * .component('toggle', { + * controller: function() { + * 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 + * ### Parent interacts with child via member + * This example shows how the parent controller can have access + * to children component controllers. + * + * + * + * + * + * angular.module('ngAsVoteExample', []) + * .component('voteTaker', { + * controller: function() { + * this.vote = null; + * this.agree = function() { this.vote = 'agree'; }; + * this.disagree = function() { this.vote = 'disagree'; }; + * this.clear = function() { this.vote = null; }; + * }, + * template: + * '' + + * '' + * }) + * .component('competition', { + * controller: function() { + * this.redVoteTaker = null; + * this.blueVoteTaker = null; + * this.match = function() { + * return this.redVoteTaker.vote === this.blueVoteTaker.vote && this.redVoteTaker.vote; + * }; + * this.next = function() { + * this.sentence++; + * this.redVoteTaker.clear(); + * this.blueVoteTaker.clear(); + * }; + * }, + * template: + * '

Red team:

' + + * '

Blue team:

' + + * '

There is a match!

' + + * '' + * }); + *
+ * + * var agrees = element.all(by.buttonText('Agree')); + * var matchMessage = element(by.css('[ng-show]')); + * var next = element(by.buttonText('Next')); + * + * it('should show match message if both agree', function() { + * expect(matchMessage.isDisplayed()).toBeFalsy(); + * agrees.click(); + * expect(matchMessage.isDisplayed()).toBeTruthy(); + * }); + * + * it('should hide match message after next is clicked', function() { + * agrees.click(); + * next.click(); + * expect(matchMessage.isDisplayed()).toBeFalsy(); + * }); + * + *
+ */ +var ngAsDirective = ['$parse', function($parse) { + return { + priority: -1, + restrict: 'A', + compile: function(tElement, tAttrs) { + // gets the expected controller name, converts into "someThing" + var controllerName = directiveNormalize(nodeName_(tElement)); + + // get the setter for the as attribute + var getter = $parse(tAttrs.ngAs); + var setter = getter.assign; + + return function(scope, element) { + // gets the controller of the current element (see jqLiteController for details) + var controller = element.data('$' + controllerName + 'Controller'); + setter(scope, controller); + + // when the element is removed, remove it from the scope assignment (nullify it) + element.on('$destroy', function() { + // only remove it if controller has not changed, + // because it can happen that animations (and other procedures) may duplicate elements + if (getter(scope) === controller) { + setter(scope, null); + } + }); + }; + } + }; +}]; diff --git a/test/ng/directive/ngAsSpec.js b/test/ng/directive/ngAsSpec.js new file mode 100644 index 000000000000..6e51bd65cda6 --- /dev/null +++ b/test/ng/directive/ngAsSpec.js @@ -0,0 +1,299 @@ +'use strict'; + +describe('ngAs', function() { + + describe('given a component', function() { + + var myComponentController, $rootScope, $compile; + + beforeEach(module(function($compileProvider) { + $compileProvider.component('myComponent', { + template: 'foo', + controller: function() { + myComponentController = this; + } + }); + })); + + beforeEach(inject(function(_$compile_, _$rootScope_) { + $rootScope = _$rootScope_; + $compile = _$compile_; + })); + + it('should bind in the current scope the controller of a component', function() { + var $ctrl = {undamaged: true}; + $rootScope.$ctrl = $ctrl; + + $compile('')($rootScope); + expect($rootScope.$ctrl).toBe($ctrl); + expect($rootScope.$ctrl.undamaged).toBe(true); + expect($ctrl.myComponent).toBe(myComponentController); + }); + + it('should be parametrized with any variable', function() { + $compile('')($rootScope); + expect($rootScope.bar.myComponent).toBe(myComponentController); + }); + + it('should work with non:normalized entity name', function() { + $compile('')($rootScope); + expect($rootScope.$ctrl.myComponent1).toBe(myComponentController); + }); + + it('should work with data-non-normalized entity name', function() { + $compile('')($rootScope); + expect($rootScope.$ctrl.myComponent2).toBe(myComponentController); + }); + + it('should work with x-non-normalized entity name', function() { + $compile('')($rootScope); + expect($rootScope.$ctrl.myComponent3).toBe(myComponentController); + }); + + it('should work with data-non-normalized attribute name', function() { + $compile('')($rootScope); + expect($rootScope.$ctrl.myComponent1).toBe(myComponentController); + }); + + it('should work with x-non-normalized attribute name', function() { + $compile('')($rootScope); + expect($rootScope.$ctrl.myComponent2).toBe(myComponentController); + }); + + it('should nullify the variable once the component is destroyed', function() { + var template = + '
' + + '' + + '
'; + $rootScope.$ctrl = {}; + $compile(template)($rootScope); + $rootScope.$apply('nullified = false'); + expect($rootScope.$ctrl.myComponent).toBe(myComponentController); + + $rootScope.$apply('nullified = true'); + expect($rootScope.$ctrl.myComponent).toBe(null); + }); + + it('should nullify the variable once the component is destroyed externally', function() { + var template = ''; + var element = $compile(template)($rootScope); + var isolateScope = element.isolateScope(); + expect($rootScope.$ctrl.myComponent).toBe(myComponentController); + + element.remove(); + isolateScope.$destroy(); + expect($rootScope.$ctrl.myComponent).toBe(null); + }); + + it('should nullify be compatible with $element transclusion', function() { + var template = ''; + $rootScope.$ctrl = {}; + $compile(template)($rootScope); + + $rootScope.$apply('nullified = true'); + expect($rootScope.$ctrl.myComponent).toBeUndefined(); + $rootScope.$apply('nullified = false'); + expect($rootScope.$ctrl.myComponent).toBe(myComponentController); + $rootScope.$apply('nullified = true'); + expect($rootScope.$ctrl.myComponent).toBe(null); + }); + + it('should be compatible with entering/leaving components', inject(function($animate) { + var template = ''; + $rootScope.$ctrl = {}; + var parent = $compile('
')($rootScope); + + var leavingScope = $rootScope.$new(); + var leaving = $compile(template)(leavingScope); + var leavingController = myComponentController; + + $animate.enter(leaving, parent); + expect($rootScope.$ctrl.myComponent).toBe(leavingController); + + var enteringScope = $rootScope.$new(); + var entering = $compile(template)($rootScope); + var enteringController = myComponentController; + + $animate.enter(entering, parent); + $animate.leave(leaving, parent); + leavingScope.$destroy(); + expect($rootScope.$ctrl.myComponent).toBe(enteringController); + })); + + }); + + it('should be compatible with directives on entities with controller', function() { + var myDirectiveController; + + module(function($compileProvider) { + $compileProvider.directive('myDirective', function() { + return { + controller: function() { + myDirectiveController = this; + } + }; + }); + }); + + inject(function($compile, $rootScope) { + $compile('')($rootScope); + + expect($rootScope.bar.myDirective).toBe(myDirectiveController); + }); + }); + + it('should work with transclussion', 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'; + $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 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 = + '
' + + '' + + '{{$ctrl.myComponent.text}}' + + '' + + '
'; + $rootScope.$ctrl = {}; + var element = $compile(template)($rootScope); + $rootScope.$apply(); + expect(element.text()).toBe(''); + expect($rootScope.$ctrl.myComponent).toBeUndefined(); + + myComponentController.transclude('transcludedOk'); + $rootScope.$apply(); + expect(element.text()).toBe('transcludedOk'); + expect($rootScope.$ctrl.myComponent).toBe(myComponentController); + + myComponentController.destroy(); + $rootScope.$apply(); + expect($rootScope.$ctrl.myComponent).toBe(null); + }); + }); + + it('should be compatible with element transclude directives', function() { + module(function($compileProvider) { + $compileProvider + .directive('myDirective', function() { + return { + transclude: 'element', + controller: function($animate, $element, $transclude) { + this.text = 'SUCCESS'; + $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); + }); + }); + +}); From 384a4e776b655cd4bce072c84c8d604e68a8bd97 Mon Sep 17 00:00:00 2001 From: David Rodenas Pico Date: Thu, 13 Oct 2016 10:24:31 +0200 Subject: [PATCH 2/5] some doc fixes --- src/ng/directive/ngAs.js | 227 +++++++++++++++++++++++++++------------ 1 file changed, 160 insertions(+), 67 deletions(-) diff --git a/src/ng/directive/ngAs.js b/src/ng/directive/ngAs.js index 18b3e3dd77a0..4429465de4b6 100644 --- a/src/ng/directive/ngAs.js +++ b/src/ng/directive/ngAs.js @@ -6,18 +6,20 @@ * @restrict A * * @description - * The `ngAs` attribute tells Angular to assign element component controller - * to a given property. + * The `ngAs` attribute tells Angular to assign the element component controller + * to the given property. * * Using this directive you can use the controller of existing components * in your template (children components). * - * If the children component is destroyed - * a `null` is assigned to the property. + * If the component is destroyed `null` is assigned to the property. * - * Note that this is the reverse of `require:`: - * with `require:` is the children who references the parent - * but with `ngAs`is the parent who references the children. + * Note that this is the reverse of `require`: + * * with `require` a component can access to the controllers + * of parent directives or directives in the same element, + * directives outside the component `template:` or `templateUrl` + * * with `ngAs`: a component can access to the controllers + * of components inside its `template` or `templateUrl` * It is very useful when you want to reuse the same component * in different situations, * and they do not need to know which exact parent they have. @@ -31,21 +33,27 @@ * ### Use inside the scope * 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 children component to show it.
+ *
+ * You are using a component in the same template to show it. + *
*
- * - * angular.module('ngAsExample', []) - * .component('toggle', { - * controller: function() { - * var opened = false; - * this.isOpen = function() { return opened; }; - * this.toggle = function() { opened = !opened; }; - * } - * }); + * + * angular.module('myApp', []); + * + * + * function ToggleController() { + * var opened = false; + * this.isOpen = function() { return opened; }; + * this.toggle = function() { opened = !opened; }; + * } + * + * angular.module('myApp').component('myToggle', { + * controller: ToggleController + * }); * * * it('should publish the toggle into the scope', function() { @@ -61,58 +69,143 @@ * ### Parent interacts with child via member * This example shows how the parent controller can have access * to children component controllers. - * - * - * + * + * + * angular.module('myApp', []); + * + * + * function ContestController() { + * var results = ['y','n','y','y']; + * + * this.$onInit = function() { + * this.restart(); + * }; + * + * this.hasQuestion = function() { + * return this.currentQuestion <= results.length; + * }; + * + * this.nextQuestion = function() { + * var answer = results[this.currentQuestion - 1]; + * this.currentQuestion = this.currentQuestion + 1; + * + * this.redScore += score(this.redVoteTaker.vote, answer); + * this.redVoteTaker.clear(); + * + * this.blueScore += score(this.blueVoteTaker.vote, answer); + * this.blueVoteTaker.clear(); + * }; + * + * this.restart = function() { + * this.currentQuestion = 1; + * this.redScore = 0; + * this.blueScore = 0; + * }; + * + * function score(vote, expected) { + * if (vote === expected) { + * return +1; + * } else if (vote === null) { + * return 0; + * } else { + * return -1; + * } + * } + * } + * + * angular.module('myApp').component('myContest', { + * controller: ContestController, + * templateUrl: 'contest.html' + * }); * - * - * angular.module('ngAsVoteExample', []) - * .component('voteTaker', { - * controller: function() { - * this.vote = null; - * this.agree = function() { this.vote = 'agree'; }; - * this.disagree = function() { this.vote = 'disagree'; }; - * this.clear = function() { this.vote = null; }; - * }, - * template: - * '' + - * '' - * }) - * .component('competition', { - * controller: function() { - * this.redVoteTaker = null; - * this.blueVoteTaker = null; - * this.match = function() { - * return this.redVoteTaker.vote === this.blueVoteTaker.vote && this.redVoteTaker.vote; - * }; - * this.next = function() { - * this.sentence++; - * this.redVoteTaker.clear(); - * this.blueVoteTaker.clear(); - * }; - * }, - * template: - * '

Red team:

' + - * '

Blue team:

' + - * '

There is a match!

' + - * '' - * }); + * + *
+ *

Question {{$ctrl.currentQuestion}}?

+ *

Red team:

+ *

Blue team:

+ *

+ *
+ *
+ *

+ * Red Wins! + * Blue Wins! + * There is a tie! + *

+ *

Red score: {{$ctrl.redScore}}

+ *

Blue score: {{$ctrl.blueScore}}

+ *

+ *
+ *
+ * + * function VoteTakerController() { + * this.vote = null; + * + * this.yes = function() { + * this.vote = 'y'; + * }; + * this.no = function() { + * this.vote = 'n'; + * }; + * this.clear = function() { + * this.vote = null; + * }; + * } + * + * angular.module('myApp').component('myVoteTaker', { + * controller: VoteTakerController, + * templateUrl: 'voteTaker.html' + * }); + * + * + * + * + * + * + * * * - * var agrees = element.all(by.buttonText('Agree')); - * var matchMessage = element(by.css('[ng-show]')); - * var next = element(by.buttonText('Next')); - * - * it('should show match message if both agree', function() { - * expect(matchMessage.isDisplayed()).toBeFalsy(); - * agrees.click(); - * expect(matchMessage.isDisplayed()).toBeTruthy(); - * }); + * function VoteTaker(team) { + * var voteTaker = element(by.css('[ng-as="$ctrl.' + team + 'VoteTaker"]')); + * var yes = voteTaker.element(by.buttonText('Yes')); + * var no = voteTaker.element(by.buttonText('No')); + * + * this.yes = function() { + * yes.click(); + * }; + * this.no = function() { + * no.click(); + * }; + * } + * + * function Contest() { + * var redScore = element(by.binding('$ctrl.redScore')); + * var blueScore = element(by.binding('$ctrl.blueScore')); + * var nextQuestion = element(by.buttonText('Next Question')); + * + * this.redVoteTaker = new VoteTaker('red'); + * this.blueVoteTaker = new VoteTaker('blue'); + * + * this.getRedScore = function() { + * return redScore.getText(); + * }; + * + * this.getBlueScore = function() { + * return blueScore.getText(); + * }; + * + * this.nextQuestion = function() { + * nextQuestion.click(); + * }; + * } * - * it('should hide match message after next is clicked', function() { - * agrees.click(); - * next.click(); - * expect(matchMessage.isDisplayed()).toBeFalsy(); + * it('should compute score red always yes, blue always pass', function() { + * var contest = new Contest(); + * for (var i = 0; i < 4; i++) { + * contest.redVoteTaker.yes(); + * contest.nextQuestion(); + * } + * expect(contest.getRedScore()).toEqual('Red score: 2'); + * expect(contest.getBlueScore()).toEqual('Blue score: 0'); * }); * *
From 7cca4abf4b5a98c2ee8d6e9f4fd0272e837ee5fd Mon Sep 17 00:00:00 2001 From: David Rodenas Pico Date: Mon, 13 Mar 2017 10:53:28 +0100 Subject: [PATCH 3/5] feat(ngRef): 1. bind controllers to scope --- angularFiles.js | 2 +- docs/content/error/ngRef/badident.ngdoc | 37 +++ src/AngularPublic.js | 4 +- src/ng/directive/ngAs.js | 241 ------------------ src/ng/directive/ngRef.js | 215 ++++++++++++++++ .../directive/{ngAsSpec.js => ngRefSpec.js} | 187 ++++++++------ 6 files changed, 369 insertions(+), 317 deletions(-) create mode 100644 docs/content/error/ngRef/badident.ngdoc delete mode 100644 src/ng/directive/ngAs.js create mode 100644 src/ng/directive/ngRef.js rename test/ng/directive/{ngAsSpec.js => ngRefSpec.js} (57%) diff --git a/angularFiles.js b/angularFiles.js index f1aac32da4ba..6565d4254695 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -58,7 +58,6 @@ var angularFiles = { 'src/ng/directive/attrs.js', 'src/ng/directive/form.js', 'src/ng/directive/input.js', - 'src/ng/directive/ngAs.js', 'src/ng/directive/ngBind.js', 'src/ng/directive/ngChange.js', 'src/ng/directive/ngClass.js', @@ -75,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/badident.ngdoc b/docs/content/error/ngRef/badident.ngdoc new file mode 100644 index 000000000000..b411ff57df14 --- /dev/null +++ b/docs/content/error/ngRef/badident.ngdoc @@ -0,0 +1,37 @@ +@ngdoc error +@name ngRef:badident +@fullName Invalid identifier expression +@description + +Occurs when an invalid identifier is specified in an {@link ng.directive:ngRef ngRef} expression. + +The {@link ng.directive:ngRef ngRef} directive's symbol syntax is used to assign a reference for the component controller in scope. + +If the expression is not a simple identifier (such that you could declare it with `var {name}`, or if the expression is a reserved name, +this error is thrown. + +Reserved names include: + + - `null` + - `this` + - `undefined` + - `$parent` + - `$id` + - `$root` + +Invalid expressions might look like this: + +```html + + + + + +``` + +Valid expressions might look like this: + +```html + + +``` diff --git a/src/AngularPublic.js b/src/AngularPublic.js index d897f812b3f5..dca14bdd6ffd 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -12,7 +12,6 @@ scriptDirective, selectDirective, optionDirective, - ngAsDirective, ngBindDirective, ngBindHtmlDirective, ngBindTemplateDirective, @@ -29,6 +28,7 @@ ngInitDirective, ngNonBindableDirective, ngPluralizeDirective, + ngRefDirective, ngRepeatDirective, ngShowDirective, ngStyleDirective, @@ -180,7 +180,6 @@ function publishExternalAPI(angular) { script: scriptDirective, select: selectDirective, option: optionDirective, - ngAs: ngAsDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, @@ -196,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/ngAs.js b/src/ng/directive/ngAs.js deleted file mode 100644 index 4429465de4b6..000000000000 --- a/src/ng/directive/ngAs.js +++ /dev/null @@ -1,241 +0,0 @@ -'use strict'; - -/** - * @ngdoc directive - * @name ngAs - * @restrict A - * - * @description - * The `ngAs` attribute tells Angular to assign the element component controller - * to the given property. - * - * Using this directive you can use the controller of existing components - * in your template (children components). - * - * If the component is destroyed `null` is assigned to the property. - * - * Note that this is the reverse of `require`: - * * with `require` a component can access to the controllers - * of parent directives or directives in the same element, - * directives outside the component `template:` or `templateUrl` - * * with `ngAs`: a component can access to the controllers - * of components inside its `template` or `templateUrl` - * It is very useful when you want to reuse the same component - * in different situations, - * and they do not need to know which exact parent they have. - * - * - * @element ANY - * @param {expression} ngAs {@link guide/expression Expression} to assign the controller. - * - * - * @example - * ### Use inside the scope - * 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', []); - * - * - * function ToggleController() { - * var opened = false; - * this.isOpen = function() { return opened; }; - * this.toggle = function() { opened = !opened; }; - * } - * - * angular.module('myApp').component('myToggle', { - * controller: ToggleController - * }); - * - * - * 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 - * ### Parent interacts with child via member - * This example shows how the parent controller can have access - * to children component controllers. - * - * - * angular.module('myApp', []); - * - * - * function ContestController() { - * var results = ['y','n','y','y']; - * - * this.$onInit = function() { - * this.restart(); - * }; - * - * this.hasQuestion = function() { - * return this.currentQuestion <= results.length; - * }; - * - * this.nextQuestion = function() { - * var answer = results[this.currentQuestion - 1]; - * this.currentQuestion = this.currentQuestion + 1; - * - * this.redScore += score(this.redVoteTaker.vote, answer); - * this.redVoteTaker.clear(); - * - * this.blueScore += score(this.blueVoteTaker.vote, answer); - * this.blueVoteTaker.clear(); - * }; - * - * this.restart = function() { - * this.currentQuestion = 1; - * this.redScore = 0; - * this.blueScore = 0; - * }; - * - * function score(vote, expected) { - * if (vote === expected) { - * return +1; - * } else if (vote === null) { - * return 0; - * } else { - * return -1; - * } - * } - * } - * - * angular.module('myApp').component('myContest', { - * controller: ContestController, - * templateUrl: 'contest.html' - * }); - * - * - *
- *

Question {{$ctrl.currentQuestion}}?

- *

Red team:

- *

Blue team:

- *

- *
- *
- *

- * Red Wins! - * Blue Wins! - * There is a tie! - *

- *

Red score: {{$ctrl.redScore}}

- *

Blue score: {{$ctrl.blueScore}}

- *

- *
- *
- * - * function VoteTakerController() { - * this.vote = null; - * - * this.yes = function() { - * this.vote = 'y'; - * }; - * this.no = function() { - * this.vote = 'n'; - * }; - * this.clear = function() { - * this.vote = null; - * }; - * } - * - * angular.module('myApp').component('myVoteTaker', { - * controller: VoteTakerController, - * templateUrl: 'voteTaker.html' - * }); - * - * - * - * - * - * - * - * - * - * function VoteTaker(team) { - * var voteTaker = element(by.css('[ng-as="$ctrl.' + team + 'VoteTaker"]')); - * var yes = voteTaker.element(by.buttonText('Yes')); - * var no = voteTaker.element(by.buttonText('No')); - * - * this.yes = function() { - * yes.click(); - * }; - * this.no = function() { - * no.click(); - * }; - * } - * - * function Contest() { - * var redScore = element(by.binding('$ctrl.redScore')); - * var blueScore = element(by.binding('$ctrl.blueScore')); - * var nextQuestion = element(by.buttonText('Next Question')); - * - * this.redVoteTaker = new VoteTaker('red'); - * this.blueVoteTaker = new VoteTaker('blue'); - * - * this.getRedScore = function() { - * return redScore.getText(); - * }; - * - * this.getBlueScore = function() { - * return blueScore.getText(); - * }; - * - * this.nextQuestion = function() { - * nextQuestion.click(); - * }; - * } - * - * it('should compute score red always yes, blue always pass', function() { - * var contest = new Contest(); - * for (var i = 0; i < 4; i++) { - * contest.redVoteTaker.yes(); - * contest.nextQuestion(); - * } - * expect(contest.getRedScore()).toEqual('Red score: 2'); - * expect(contest.getBlueScore()).toEqual('Blue score: 0'); - * }); - * - *
- */ -var ngAsDirective = ['$parse', function($parse) { - return { - priority: -1, - restrict: 'A', - compile: function(tElement, tAttrs) { - // gets the expected controller name, converts into "someThing" - var controllerName = directiveNormalize(nodeName_(tElement)); - - // get the setter for the as attribute - var getter = $parse(tAttrs.ngAs); - var setter = getter.assign; - - return function(scope, element) { - // gets the controller of the current element (see jqLiteController for details) - var controller = element.data('$' + controllerName + 'Controller'); - setter(scope, controller); - - // when the element is removed, remove it from the scope assignment (nullify it) - element.on('$destroy', function() { - // only remove it if controller has not changed, - // because it can happen that animations (and other procedures) may duplicate elements - if (getter(scope) === controller) { - setter(scope, null); - } - }); - }; - } - }; -}]; diff --git a/src/ng/directive/ngRef.js b/src/ng/directive/ngRef.js new file mode 100644 index 000000000000..f08c9c178b3d --- /dev/null +++ b/src/ng/directive/ngRef.js @@ -0,0 +1,215 @@ +'use strict'; + +/** + * @ngdoc directive + * @name ngRef + * @restrict A + * + * @description + * The `ngRef` attribute tells AngularJS to assign the element component controller + * to the given property in the current scope. + * + * If the component is destroyed `null` is assigned to the property. + * + * + * @element ANY + * @param {string} ngRef property name - this must be a valid AngularJS expression identifier + * + * + * @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', []); + * + * + * function ToggleController() { + * var opened = false; + * this.isOpen = function() { return opened; }; + * this.toggle = function() { opened = !opened; }; + * } + * + * angular.module('myApp').component('myToggle', { + * controller: ToggleController + * }); + * + * + * 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 new scopes limits + * + * + *

Outer Toggle

+ * Outer Toggle + *
outerToggle.isOpen(): {{outerToggle.isOpen() | json}}
+ * + *

ngRepeat toggle

+ *
    + *
  • + * ngRepeat Toggle {{i}} + *
    ngRepeatToggle.isOpen(): {{ngRepeatToggle.isOpen() | json}}
    + *
    outerToggle.isOpen(): {{outerToggle.isOpen() | json}}
    + *
  • + *
+ *
ngRepeat.isOpen(): {{ngRepeatToggle.isOpen() | json}}
+ * + *

ngIf toggle

+ *
+ * ngIf Toggle + *
ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}
+ *
outerToggle.isOpen(): {{outerToggle.isOpen() | json}}
+ *
+ *
ngIf.isOpen(): {{ngIf.isOpen() | json}}
+ *
+ * + * angular.module('myApp', []); + * + * + * function ToggleController() { + * var opened = false; + * this.isOpen = function() { return opened; }; + * this.toggle = function() { opened = !opened; }; + * } + * + * angular.module('myApp').component('myToggle', { + * template: '', + * transclude: true, + * controller: ToggleController + * }); + * + * + * 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('i 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 ngRefDirective = function() { + var ngRefMinErr = minErr('ngRef'); + + return { + priority: -1, + restrict: 'A', + compile: function(tElement, tAttrs) { + // gets the expected controller name, converts into "someThing" + var controllerName = directiveNormalize(nodeName_(tElement)); + + // get the symbol name where to set the reference in the scope + var symbolName = tAttrs.ngRef; + + if (symbolName && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(symbolName) || + /^(null|undefined|this|\$parent|\$root|\$id)$/.test(symbolName))) { + throw ngRefMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.', + symbolName); + } + + return function(scope, element) { + // gets the controller of the current element (see jqLiteController for details) + var controller = element.data('$' + controllerName + 'Controller'); + scope[symbolName] = controller; + + // when the element is removed, remove it from the scope assignment (nullify it) + element.on('$destroy', function() { + // only remove it if controller has not changed, + // because it can happen that animations (and other procedures) may duplicate elements + if (scope[symbolName] === controller) { + scope[symbolName] = null; + } + }); + }; + } + }; +}; diff --git a/test/ng/directive/ngAsSpec.js b/test/ng/directive/ngRefSpec.js similarity index 57% rename from test/ng/directive/ngAsSpec.js rename to test/ng/directive/ngRefSpec.js index 6e51bd65cda6..23c20551e5fd 100644 --- a/test/ng/directive/ngAsSpec.js +++ b/test/ng/directive/ngRefSpec.js @@ -1,6 +1,6 @@ 'use strict'; -describe('ngAs', function() { +describe('ngRef', function() { describe('given a component', function() { @@ -15,112 +15,124 @@ describe('ngAs', function() { }); })); + beforeEach(module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + })); + beforeEach(inject(function(_$compile_, _$rootScope_) { $rootScope = _$rootScope_; $compile = _$compile_; })); - it('should bind in the current scope the controller of a component', function() { - var $ctrl = {undamaged: true}; - $rootScope.$ctrl = $ctrl; + afterEach(inject(function($exceptionHandler) { + if ($exceptionHandler.errors.length) { + dump(jasmine.getEnv().currentSpec.getFullName()); + dump('$exceptionHandler has errors'); + dump($exceptionHandler.errors); + expect($exceptionHandler.errors).toBe([]); + } + })); - $compile('')($rootScope); - expect($rootScope.$ctrl).toBe($ctrl); - expect($rootScope.$ctrl.undamaged).toBe(true); - expect($ctrl.myComponent).toBe(myComponentController); - }); + it('should bind in the current scope the controller of a component', function() { + $rootScope.$ctrl = 'undamaged'; - it('should be parametrized with any variable', function() { - $compile('')($rootScope); - expect($rootScope.bar.myComponent).toBe(myComponentController); + $compile('')($rootScope); + expect($rootScope.$ctrl).toBe('undamaged'); + expect($rootScope.myComponent).toBe(myComponentController); }); it('should work with non:normalized entity name', function() { - $compile('')($rootScope); - expect($rootScope.$ctrl.myComponent1).toBe(myComponentController); + $compile('')($rootScope); + expect($rootScope.myComponent1).toBe(myComponentController); }); it('should work with data-non-normalized entity name', function() { - $compile('')($rootScope); - expect($rootScope.$ctrl.myComponent2).toBe(myComponentController); + $compile('')($rootScope); + expect($rootScope.myComponent2).toBe(myComponentController); }); it('should work with x-non-normalized entity name', function() { - $compile('')($rootScope); - expect($rootScope.$ctrl.myComponent3).toBe(myComponentController); + $compile('')($rootScope); + expect($rootScope.myComponent3).toBe(myComponentController); }); it('should work with data-non-normalized attribute name', function() { - $compile('')($rootScope); - expect($rootScope.$ctrl.myComponent1).toBe(myComponentController); + $compile('')($rootScope); + expect($rootScope.myComponent1).toBe(myComponentController); }); it('should work with x-non-normalized attribute name', function() { - $compile('')($rootScope); - expect($rootScope.$ctrl.myComponent2).toBe(myComponentController); + $compile('')($rootScope); + expect($rootScope.myComponent2).toBe(myComponentController); }); - it('should nullify the variable once the component is destroyed', function() { + it('should not leak to parent scopes', function() { var template = - '
' + - '' + + '
' + + '' + '
'; - $rootScope.$ctrl = {}; $compile(template)($rootScope); - $rootScope.$apply('nullified = false'); - expect($rootScope.$ctrl.myComponent).toBe(myComponentController); - - $rootScope.$apply('nullified = true'); - expect($rootScope.$ctrl.myComponent).toBe(null); + expect($rootScope.myComponent).toBe(undefined); }); - it('should nullify the variable once the component is destroyed externally', function() { - var template = ''; + it('should nullify the variable once the component is destroyed', function() { + var template = '
'; + var element = $compile(template)($rootScope); - var isolateScope = element.isolateScope(); - expect($rootScope.$ctrl.myComponent).toBe(myComponentController); + expect($rootScope.myComponent).toBe(myComponentController); - element.remove(); + var componentElement = element.children(); + var isolateScope = componentElement.isolateScope(); + componentElement.remove(); isolateScope.$destroy(); - expect($rootScope.$ctrl.myComponent).toBe(null); - }); - - it('should nullify be compatible with $element transclusion', function() { - var template = ''; - $rootScope.$ctrl = {}; - $compile(template)($rootScope); - - $rootScope.$apply('nullified = true'); - expect($rootScope.$ctrl.myComponent).toBeUndefined(); - $rootScope.$apply('nullified = false'); - expect($rootScope.$ctrl.myComponent).toBe(myComponentController); - $rootScope.$apply('nullified = true'); - expect($rootScope.$ctrl.myComponent).toBe(null); + expect($rootScope.myComponent).toBe(null); }); it('should be compatible with entering/leaving components', inject(function($animate) { - var template = ''; + var template = ''; $rootScope.$ctrl = {}; var parent = $compile('
')($rootScope); - var leavingScope = $rootScope.$new(); - var leaving = $compile(template)(leavingScope); + var leaving = $compile(template)($rootScope); var leavingController = myComponentController; $animate.enter(leaving, parent); - expect($rootScope.$ctrl.myComponent).toBe(leavingController); + expect($rootScope.myComponent).toBe(leavingController); - var enteringScope = $rootScope.$new(); var entering = $compile(template)($rootScope); var enteringController = myComponentController; $animate.enter(entering, parent); $animate.leave(leaving, parent); - leavingScope.$destroy(); - expect($rootScope.$ctrl.myComponent).toBe(enteringController); + expect($rootScope.myComponent).toBe(enteringController); + })); + + it('should throw if alias identifier is not a simple identifier', + inject(function($exceptionHandler) { + forEach([ + 'null', + 'this', + 'undefined', + '$parent', + '$root', + '$id', + 'obj[key]', + 'obj["key"]', + 'obj[\'key\']', + 'obj.property', + 'foo=6' + ], function(identifier) { + var escapedIdentifier = identifier.replace(/"/g, '"'); + var template = ''; + var element = $compile(template)($rootScope); + + expect($exceptionHandler.errors.length).toEqual(1, identifier); + expect($exceptionHandler.errors.shift()[0]).toEqualMinErr('ngRef', 'badident', + 'alias \'' + identifier + '\' is invalid --- must be a valid JS identifier ' + + 'which is not a reserved name'); + + dealoc(element); + }); })); }); @@ -139,9 +151,9 @@ describe('ngAs', function() { }); inject(function($compile, $rootScope) { - $compile('')($rootScope); + $compile('')($rootScope); - expect($rootScope.bar.myDirective).toBe(myDirectiveController); + expect($rootScope.myDirective).toBe(myDirectiveController); }); }); @@ -158,7 +170,7 @@ describe('ngAs', function() { }); inject(function($compile, $rootScope) { - var template = '{{myComponent.text}}'; + var template = '{{myComponent.text}}'; var element = $compile(template)($rootScope); $rootScope.$apply(); expect(element.text()).toBe('SUCCESS'); @@ -183,7 +195,7 @@ describe('ngAs', function() { inject(function($compile, $rootScope) { var template = '
' + - '' + + '' + '{{myComponent.text}}' + '' + '
'; @@ -194,7 +206,39 @@ describe('ngAs', function() { }); }); - it('should be compatible with transclude&destroy components', function() { + 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 @@ -223,24 +267,21 @@ describe('ngAs', function() { inject(function($compile, $rootScope) { var template = '
' + - '' + - '{{$ctrl.myComponent.text}}' + + '' + + '{{myComponent.text}}' + '' + '
'; - $rootScope.$ctrl = {}; var element = $compile(template)($rootScope); $rootScope.$apply(); expect(element.text()).toBe(''); - expect($rootScope.$ctrl.myComponent).toBeUndefined(); myComponentController.transclude('transcludedOk'); $rootScope.$apply(); expect(element.text()).toBe('transcludedOk'); - expect($rootScope.$ctrl.myComponent).toBe(myComponentController); myComponentController.destroy(); $rootScope.$apply(); - expect($rootScope.$ctrl.myComponent).toBe(null); + expect(element.text()).toBe(''); }); }); @@ -263,7 +304,7 @@ describe('ngAs', function() { inject(function($compile, $rootScope) { var template = '
' + - '' + + '' + '{{myDirective.text}}' + '' + '
'; @@ -285,7 +326,7 @@ describe('ngAs', function() { }); inject(function($compile, $httpBackend, $rootScope) { - var template = '
'; + var template = '
'; var element = $compile(template)($rootScope); $httpBackend.expect('GET', 'template.html').respond('ok'); $rootScope.$apply(); From 489332a63df1423cad6b0be136eee974a3245f4e Mon Sep 17 00:00:00 2001 From: David Rodenas Pico Date: Mon, 13 Mar 2017 12:03:29 +0100 Subject: [PATCH 4/5] feat(ngRef): 2. bind dom element --- src/ng/directive/ngRef.js | 11 ++++++----- test/ng/directive/ngRefSpec.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/ng/directive/ngRef.js b/src/ng/directive/ngRef.js index f08c9c178b3d..53ad1e43b407 100644 --- a/src/ng/directive/ngRef.js +++ b/src/ng/directive/ngRef.js @@ -197,15 +197,16 @@ var ngRefDirective = function() { } return function(scope, element) { - // gets the controller of the current element (see jqLiteController for details) + // gets the controller of the current component or the current DOM element var controller = element.data('$' + controllerName + 'Controller'); - scope[symbolName] = controller; + var value = controller || element[0]; + scope[symbolName] = value; // when the element is removed, remove it from the scope assignment (nullify it) element.on('$destroy', function() { - // only remove it if controller has not changed, - // because it can happen that animations (and other procedures) may duplicate elements - if (scope[symbolName] === controller) { + // only remove it if value has not changed, + // carefully because animations (and other procedures) may duplicate elements + if (scope[symbolName] === value) { scope[symbolName] = null; } }); diff --git a/test/ng/directive/ngRefSpec.js b/test/ng/directive/ngRefSpec.js index 23c20551e5fd..cf0fb9b9bca9 100644 --- a/test/ng/directive/ngRefSpec.js +++ b/test/ng/directive/ngRefSpec.js @@ -137,6 +137,17 @@ describe('ngRef', function() { }); + it('should bind the dom element if no component', inject(function($compile, $rootScope) { + $compile('my text')($rootScope); + expect($rootScope.mySpan.textContent).toBe('my text'); + })); + + it('should nullify the dom element value if it is destroyed', inject(function($compile, $rootScope) { + var element = $compile('
my text
')($rootScope); + element.children().remove(); + expect($rootScope.mySpan).toBe(null); + })); + it('should be compatible with directives on entities with controller', function() { var myDirectiveController; From 6b0dd26e02d4b86db746c18ce9dc65459424ce76 Mon Sep 17 00:00:00 2001 From: David Rodenas Pico Date: Mon, 13 Mar 2017 13:17:05 +0100 Subject: [PATCH 5/5] feat(ngRef): 3. bind to any expression --- docs/content/error/ngRef/badident.ngdoc | 37 ------------------------- src/ng/directive/ngRef.js | 25 ++++++----------- test/ng/directive/ngRefSpec.js | 33 ++++------------------ 3 files changed, 15 insertions(+), 80 deletions(-) delete mode 100644 docs/content/error/ngRef/badident.ngdoc diff --git a/docs/content/error/ngRef/badident.ngdoc b/docs/content/error/ngRef/badident.ngdoc deleted file mode 100644 index b411ff57df14..000000000000 --- a/docs/content/error/ngRef/badident.ngdoc +++ /dev/null @@ -1,37 +0,0 @@ -@ngdoc error -@name ngRef:badident -@fullName Invalid identifier expression -@description - -Occurs when an invalid identifier is specified in an {@link ng.directive:ngRef ngRef} expression. - -The {@link ng.directive:ngRef ngRef} directive's symbol syntax is used to assign a reference for the component controller in scope. - -If the expression is not a simple identifier (such that you could declare it with `var {name}`, or if the expression is a reserved name, -this error is thrown. - -Reserved names include: - - - `null` - - `this` - - `undefined` - - `$parent` - - `$id` - - `$root` - -Invalid expressions might look like this: - -```html - - - - - -``` - -Valid expressions might look like this: - -```html - - -``` diff --git a/src/ng/directive/ngRef.js b/src/ng/directive/ngRef.js index 53ad1e43b407..51408561947a 100644 --- a/src/ng/directive/ngRef.js +++ b/src/ng/directive/ngRef.js @@ -177,9 +177,7 @@ * * */ -var ngRefDirective = function() { - var ngRefMinErr = minErr('ngRef'); - +var ngRefDirective = ['$parse',function($parse) { return { priority: -1, restrict: 'A', @@ -187,30 +185,25 @@ var ngRefDirective = function() { // gets the expected controller name, converts into "someThing" var controllerName = directiveNormalize(nodeName_(tElement)); - // get the symbol name where to set the reference in the scope - var symbolName = tAttrs.ngRef; - - if (symbolName && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(symbolName) || - /^(null|undefined|this|\$parent|\$root|\$id)$/.test(symbolName))) { - throw ngRefMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.', - symbolName); - } + // get the expression for value binding + var getter = $parse(tAttrs.ngRef); + var setter = getter.assign; return function(scope, element) { // gets the controller of the current component or the current DOM element var controller = element.data('$' + controllerName + 'Controller'); var value = controller || element[0]; - scope[symbolName] = value; + setter(scope, value); - // when the element is removed, remove it from the scope assignment (nullify it) + // when the element is removed, remove it (nullify it) element.on('$destroy', function() { // only remove it if value has not changed, // carefully because animations (and other procedures) may duplicate elements - if (scope[symbolName] === value) { - scope[symbolName] = null; + if (getter(scope) === value) { + setter(scope, null); } }); }; } }; -}; +}]; diff --git a/test/ng/directive/ngRefSpec.js b/test/ng/directive/ngRefSpec.js index cf0fb9b9bca9..1984750a66b7 100644 --- a/test/ng/directive/ngRefSpec.js +++ b/test/ng/directive/ngRefSpec.js @@ -107,33 +107,12 @@ describe('ngRef', function() { expect($rootScope.myComponent).toBe(enteringController); })); - it('should throw if alias identifier is not a simple identifier', - inject(function($exceptionHandler) { - forEach([ - 'null', - 'this', - 'undefined', - '$parent', - '$root', - '$id', - 'obj[key]', - 'obj["key"]', - 'obj[\'key\']', - 'obj.property', - 'foo=6' - ], function(identifier) { - var escapedIdentifier = identifier.replace(/"/g, '"'); - var template = ''; - var element = $compile(template)($rootScope); - - expect($exceptionHandler.errors.length).toEqual(1, identifier); - expect($exceptionHandler.errors.shift()[0]).toEqualMinErr('ngRef', 'badident', - 'alias \'' + identifier + '\' is invalid --- must be a valid JS identifier ' + - 'which is not a reserved name'); - - dealoc(element); - }); - })); + it('should allow bind to a parent controller', function() { + $rootScope.$ctrl = {}; + + $compile('')($rootScope); + expect($rootScope.$ctrl.myComponent).toBe(myComponentController); + }); });