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() {