From 3d197aa3f4c58bdba6d6021084a8a2e7c56489cc Mon Sep 17 00:00:00 2001 From: sbarker4183 Date: Wed, 5 Mar 2014 16:33:04 -0500 Subject: [PATCH] feat(directive): ngName Create ngName directive. Currently input name attributes cannot be evaluated. This is because of the task of handling adding and removing controls to parent FormControllers. NgName handles evaluating the name safely without risking unintended manipulation later. It prevents changing the existing controllers and directives and therefore contains no risk to the current source. It simply evaluates the expression and makes the changes prior to ngModel link execution. This does not handle adding or removing controls... (yet) --- src/AngularPublic.js | 4 +- src/ng/directive/input.js | 110 +++++++++++++++++++++++++ test/ng/directive/inputSpec.js | 142 +++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 4f2474fba9f7..60c901a08781 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -55,6 +55,7 @@ ngModelOptionsDirective, ngAttributeAliasDirectives, ngEventDirectives, + ngNameDirective, $AnchorScrollProvider, $AnimateProvider, @@ -197,7 +198,8 @@ function publishExternalAPI(angular){ maxlength: maxlengthDirective, ngMaxlength: maxlengthDirective, ngValue: ngValueDirective, - ngModelOptions: ngModelOptionsDirective + ngModelOptions: ngModelOptionsDirective, + ngName: ngNameDirective }). directive({ ngInclude: ngIncludeFillContentDirective diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index fa6fe55d9f5e..891e590927aa 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -2510,3 +2510,113 @@ var ngModelOptionsDirective = function() { }] }; }; + +/** + * @ngdoc directive + * @name ngName + * + * @priority 100 + * + * @description + * The `ng-name` directive allows you the ability to evaluate scope expressions on the name attribute. + * This directive does not react to the scope expression. It merely evaluates the expression, and sets the + * {@link ngModel.NgModelController NgModelController}'s `$name` property and the `` element's + * `name` attribute. + * + *
+ * This is particularly useful when building forms while looping through data with `ng-repeat`, + * allowing you evaluate expressions such as as control names. + *
+ * + *
+ * This is NOT a data binding, in the sense that the attribute is not observed nor is the scope + * expression watched. The ngName directive's link function runs after the ngModelController but before ngModel's + * link function. This allows the evaluated result to be updated to the $name property prior to the + * {@link form.FormController FormController}'s `$addControl` function being called. + *
+ * + * @element input + * @param {expression} ngName {@link guide/expression Expression} to evaluate. + * + * @example + + +
+
+

Enter the amount of candy you want.

+
+ + +
+
+
+
+
{{ c.type }}
+
candyForm.{{ c.type + 'Qty' }}.$valid =
+
Quantity = {{ c.qty }}
+
+
+
+
+ + function Ctrl($scope) { + $scope.candy = [ + { + type: 'chocolates', + qty: null + }, + { + type: 'peppermints', + qty: null + }, + { + type: 'lollipops', + qty: null + } + ]; + } + + + var chocolatesInput = element(by.id('chocolates')); + var chocolatesValid = element(by.binding('candyForm.chocolatesQty.$valid')); + var peppermintsInput = element(by.id('peppermints')); + var peppermintsValid = element(by.binding('candyForm.peppermintsQty.$valid')); + var lollipopsInput = element(by.id('lollipops')); + var lollipopsValid = element(by.binding('candyForm.lollipopsQty.$valid')); + + it('should initialize controls properly', function() { + expect(chocolatesValid.getText()).toBe('false'); + expect(peppermintsValid.getText()).toBe('false'); + expect(lollipopsValid.getText()).toBe('false'); + }); + + it('should be valid when entering n >= 0', function() { + chocolatesInput.sendKeys('5'); + peppermintsInput.sendKeys('55'); + lollipopsInput.sendKeys('555'); + + expect(chocolatesValid.getText()).toBe('true'); + expect(peppermintsValid.getText()).toBe('true'); + expect(lollipopsValid.getText()).toBe('true'); + }); + +
+ */ +var ngNameDirective = function() { + return { + priority: 100, + restrict: 'A', + require: 'ngModel', + link: { + pre: function ngNameLinkFn(scope, elem, attrs, ctrl) { + ctrl.$name = scope.$eval(attrs.ngName); + attrs.$set('name', ctrl.$name); + } + } + }; +}; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index e48a2a082672..03ae4b4f7c77 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2884,6 +2884,148 @@ describe('input', function() { expect(scope.items[0].selected).toBe(false); }); }); + + describe('ngName', function() { + + it('should set name attribute and property on element', function() { + scope.controlNamespace = 'test'; + scope.something = 'modelVal'; + compileInput(''); + expect(inputElm[0].name).toBe('testControl'); + expect(inputElm[0].getAttribute('name')).toBe('testControl'); + }); + + it('should set the $name property on the ngModelController', function() { + scope.controlName = 'test'; + scope.something = 'modelVal'; + compileInput(''); + + var ctrl = inputElm['data']('$ngModelController'); + expect(ctrl.$name).toBe('test'); + }); + + it('should set have set the controller $name prior to adding the control to the form controller', function() { + scope.controlNamespace = 'test'; + scope.something = 'modelVal'; + compileInput(''); + + expect(scope.form.testControl).toBeDefined(); + }); + + it('should work inside ngRepeat', function() { + compileInput('
' + + '' + + '
'); + + scope.$apply(function() { + scope.packages = [ + { + id: 0, + isDelivered: false + }, + { + id: 1, + isDelivered: false + }, + { + id: 2, + isDelivered: false + } + ]; + }); + + inputElm = formElm.find('input'); + var ctrls = []; + forEach(inputElm, function(val) { + ctrls.push(jqLite(val)['data']('$ngModelController')); + }); + + expect(inputElm[0].name).toBe('isDelivered0'); + expect(inputElm[0].getAttribute('name')).toBe('isDelivered0'); + expect(ctrls[0].$name).toBe('isDelivered0'); + expect(scope.form.isDelivered0).toBeDefined(); + + expect(inputElm[1].name).toBe('isDelivered1'); + expect(inputElm[1].getAttribute('name')).toBe('isDelivered1'); + expect(ctrls[1].$name).toBe('isDelivered1'); + expect(scope.form.isDelivered1).toBeDefined(); + + expect(inputElm[2].name).toBe('isDelivered2'); + expect(inputElm[2].getAttribute('name')).toBe('isDelivered2'); + expect(ctrls[2].$name).toBe('isDelivered2'); + expect(scope.form.isDelivered2).toBeDefined(); + + scope.$apply(function() { + scope.packages = [ + { + id: 0, + isDelivered: false + }, + { + id: 2, + isDelivered: false + } + ]; + }); + + inputElm = formElm.find('input'); + ctrls = []; + forEach(inputElm, function(val) { + ctrls.push(jqLite(val)['data']('$ngModelController')); + }); + + expect(inputElm[0].name).toBe('isDelivered0'); + expect(inputElm[0].getAttribute('name')).toBe('isDelivered0'); + expect(ctrls[0].$name).toBe('isDelivered0'); + expect(scope.form.isDelivered0).toBeDefined(); + + expect(inputElm[1].name).toBe('isDelivered2'); + expect(inputElm[1].getAttribute('name')).toBe('isDelivered2'); + expect(ctrls[1].$name).toBe('isDelivered2'); + expect(scope.form.isDelivered2).toBeDefined(); + + expect(scope.form.isDelivered1).toBeUndefined(); + }); + + it('should not affect the control or form when the evaluated result changes', function() { + compileInput('
' + + '' + + '
'); + + scope.$apply(function() { + scope.packages = [ + { + id: 0, + isDelivered: false + }, + { + id: 1, + isDelivered: false + } + ]; + }); + + inputElm = formElm.find('input'); + var ctrls = []; + forEach(inputElm, function(val) { + ctrls.push(jqLite(val)['data']('$ngModelController')); + }); + + expect(ctrls.length).toBe(2); + expect(ctrls[0].$name).toBe('isDelivered0'); + expect(scope.form.isDelivered0).toBeDefined(); + + scope.$apply(function() { + scope.packages[0].id = 10; + }); + + expect(ctrls.length).toBe(2); + expect(ctrls[0].$name).toBe('isDelivered0'); + expect(scope.form.isDelivered0).toBeDefined(); + expect(scope.form.isDelivered10).toBeUndefined(); + }); + + }); }); describe('NgModel animations', function() {