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

Commit 7ef1d0f

Browse files
committed
feat(input): support dynamic element validation
Interpolates the form controll attribute name, so that dynamic form controls (such as those rendered in an ngRepeat) will always have their expected interpolated name. Closes #4791 feat(input): rename form controls when name is updated feat(form): support dynamic form validation test(select): add tests for form control interpolation in selectDirective
1 parent ace40d5 commit 7ef1d0f

File tree

5 files changed

+184
-9
lines changed

5 files changed

+184
-9
lines changed

src/ng/directive/form.js

+29-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
var nullFormCtrl = {
66
$addControl: noop,
7+
$$renameControl: nullFormRenameControl,
78
$removeControl: noop,
89
$setValidity: noop,
910
$$setPending: noop,
@@ -14,6 +15,10 @@ var nullFormCtrl = {
1415
},
1516
SUBMITTED_CLASS = 'ng-submitted';
1617

18+
function nullFormRenameControl(control, name) {
19+
control.$name = name;
20+
}
21+
1722
/**
1823
* @ngdoc type
1924
* @name form.FormController
@@ -51,8 +56,8 @@ SUBMITTED_CLASS = 'ng-submitted';
5156
*
5257
*/
5358
//asks for $scope to fool the BC controller module
54-
FormController.$inject = ['$element', '$attrs', '$scope', '$animate'];
55-
function FormController(element, attrs, $scope, $animate) {
59+
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
60+
function FormController(element, attrs, $scope, $animate, $interpolate) {
5661
var form = this,
5762
parentForm = element.parent().controller('form') || nullFormCtrl,
5863
controls = [];
@@ -61,7 +66,7 @@ function FormController(element, attrs, $scope, $animate) {
6166
form.$error = {};
6267
form.$$success = {};
6368
form.$pending = undefined;
64-
form.$name = attrs.name || attrs.ngForm;
69+
form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
6570
form.$dirty = false;
6671
form.$pristine = true;
6772
form.$valid = true;
@@ -127,6 +132,17 @@ function FormController(element, attrs, $scope, $animate) {
127132
}
128133
};
129134

135+
// Private API: rename a form control
136+
form.$$renameControl = function(control, newName) {
137+
var oldName = control.$name;
138+
139+
if (form[oldName] === control) {
140+
delete form[oldName];
141+
}
142+
form[newName] = control;
143+
control.$name = newName;
144+
};
145+
130146
/**
131147
* @ngdoc method
132148
* @name form.FormController#$removeControl
@@ -466,13 +482,20 @@ var formDirectiveFactory = function(isNgForm) {
466482
});
467483
}
468484

469-
var parentFormCtrl = formElement.parent().controller('form'),
470-
alias = attr.name || attr.ngForm;
485+
var parentFormCtrl = formElement.parent().controller('form') || nullFormCtrl,
486+
alias = controller.$name;
471487

472488
if (alias) {
473489
setter(scope, alias, controller, alias);
490+
attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) {
491+
if (alias === newValue) return;
492+
setter(scope, alias, undefined, alias);
493+
alias = newValue;
494+
setter(scope, alias, controller, alias);
495+
parentFormCtrl.$$renameControl(controller, alias);
496+
});
474497
}
475-
if (parentFormCtrl) {
498+
if (parentFormCtrl !== nullFormCtrl) {
476499
formElement.on('$destroy', function() {
477500
parentFormCtrl.$removeControl(controller);
478501
if (alias) {

src/ng/directive/input.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -1657,8 +1657,8 @@ var VALID_CLASS = 'ng-valid',
16571657
*
16581658
*
16591659
*/
1660-
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q',
1661-
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q) {
1660+
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
1661+
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
16621662
this.$viewValue = Number.NaN;
16631663
this.$modelValue = Number.NaN;
16641664
this.$validators = {};
@@ -1675,7 +1675,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16751675
this.$error = {}; // keep invalid keys here
16761676
this.$$success = {}; // keep valid keys here
16771677
this.$pending = undefined; // keep pending keys here
1678-
this.$name = $attr.name;
1678+
this.$name = $interpolate($attr.name || '', false)($scope);
16791679

16801680

16811681
var parsedNgModel = $parse($attr.ngModel),
@@ -2387,6 +2387,12 @@ var ngModelDirective = function() {
23872387
// notify others, especially parent forms
23882388
formCtrl.$addControl(modelCtrl);
23892389

2390+
attr.$observe('name', function(newValue) {
2391+
if (modelCtrl.$name !== newValue) {
2392+
formCtrl.$$renameControl(modelCtrl, newValue);
2393+
}
2394+
});
2395+
23902396
scope.$on('$destroy', function() {
23912397
formCtrl.$removeControl(modelCtrl);
23922398
});

test/ng/directive/formSpec.js

+51
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,57 @@ describe('form', function() {
782782
});
783783
});
784784

785+
786+
it('should rename nested form controls when interpolated name changes', function() {
787+
scope.idA = 'A';
788+
scope.idB = 'X';
789+
790+
doc = $compile(
791+
'<form name="form">' +
792+
'<div ng-form="nested{{idA}}">' +
793+
'<div ng-form name="nested{{idB}}"' +
794+
'</div>' +
795+
'</div>' +
796+
'</form'
797+
)(scope);
798+
799+
scope.$digest();
800+
var formA = scope.form.nestedA;
801+
expect(formA).toBeDefined();
802+
expect(formA.$name).toBe('nestedA');
803+
804+
var formX = formA.nestedX;
805+
expect(formX).toBeDefined();
806+
expect(formX.$name).toBe('nestedX');
807+
808+
scope.idA = 'B';
809+
scope.idB = 'Y';
810+
scope.$digest();
811+
812+
expect(scope.form.nestedA).toBeUndefined();
813+
expect(scope.form.nestedB).toBe(formA);
814+
expect(formA.nestedX).toBeUndefined();
815+
expect(formA.nestedY).toBe(formX);
816+
});
817+
818+
819+
it('should rename forms with no parent when interpolated name changes', function() {
820+
var element = $compile('<form name="name{{nameID}}"></form>')(scope);
821+
var element2 = $compile('<div ng-form="name{{nameID}}"></div>')(scope);
822+
scope.nameID = "A";
823+
scope.$digest();
824+
var form = element.controller('form');
825+
var form2 = element2.controller('form');
826+
expect(form.$name).toBe('nameA');
827+
expect(form2.$name).toBe('nameA');
828+
829+
scope.nameID = "B";
830+
scope.$digest();
831+
expect(form.$name).toBe('nameB');
832+
expect(form2.$name).toBe('nameB');
833+
});
834+
835+
785836
describe('$setSubmitted', function() {
786837
beforeEach(function() {
787838
doc = $compile(

test/ng/directive/inputSpec.js

+54
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,60 @@ describe('input', function() {
12891289
}
12901290
}));
12911291

1292+
1293+
it('should interpolate input names (string)', function() {
1294+
scope.nameID = '47';
1295+
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
1296+
expect(scope.form.name47.$pristine).toBeTruthy();
1297+
changeInputValueTo('caitp');
1298+
expect(scope.form.name47.$dirty).toBeTruthy();
1299+
});
1300+
1301+
1302+
it('should interpolate input names (number)', function() {
1303+
scope.nameID = 47;
1304+
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
1305+
expect(scope.form.name47.$pristine).toBeTruthy();
1306+
changeInputValueTo('caitp');
1307+
expect(scope.form.name47.$dirty).toBeTruthy();
1308+
});
1309+
1310+
1311+
it('should interpolate input names (undefined)', function() {
1312+
scope.nameID = undefined;
1313+
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
1314+
expect(scope.form.name.$pristine).toBeTruthy();
1315+
changeInputValueTo('caitp');
1316+
expect(scope.form.name.$dirty).toBeTruthy();
1317+
});
1318+
1319+
1320+
it('should rename form controls in form when interpolated name changes', function() {
1321+
scope.nameID = "A";
1322+
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
1323+
expect(scope.form.nameA.$name).toBe('nameA');
1324+
var oldModel = scope.form.nameA;
1325+
scope.nameID = "B";
1326+
scope.$digest();
1327+
expect(scope.form.nameA).toBeUndefined();
1328+
expect(scope.form.nameB).toBe(oldModel);
1329+
expect(scope.form.nameB.$name).toBe('nameB');
1330+
});
1331+
1332+
1333+
it('should rename form controls in null form when interpolated name changes', function() {
1334+
var element = $compile('<input type="text" ng-model="name" name="name{{nameID}}" />')(scope);
1335+
scope.nameID = "A";
1336+
scope.$digest();
1337+
var model = element.controller('ngModel');
1338+
expect(model.$name).toBe('nameA');
1339+
1340+
scope.nameID = "B";
1341+
scope.$digest();
1342+
expect(model.$name).toBe('nameB');
1343+
});
1344+
1345+
12921346
describe('"change" event', function() {
12931347
function assertBrowserSupportsChangeEvent(inputEventSupported) {
12941348
// Force browser to report a lack of an 'input' event

test/ng/directive/selectSpec.js

+41
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,47 @@ describe('select', function() {
148148
});
149149

150150

151+
it('should interpolate select names (number)', function() {
152+
scope.robots = ['c3p0', 'r2d2'];
153+
scope.name = 'r2d2';
154+
scope.nameID = 47;
155+
compile('<select ng-model="name" name="name{{nameID}}">' +
156+
'<option ng-repeat="r in robots">{{r}}</option>' +
157+
'</select>');
158+
expect(scope.form.name47.$pristine).toBeTruthy();
159+
browserTrigger(element.find('option').eq(0));
160+
expect(scope.form.name47.$dirty).toBeTruthy();
161+
expect(scope.name).toBe('c3p0');
162+
});
163+
164+
165+
it('should interpolate select names (undefined)', function() {
166+
scope.robots = ['c3p0', 'r2d2'];
167+
scope.name = 'r2d2';
168+
scope.nameID = undefined;
169+
compile('<select ng-model="name" name="name{{nameID}}">' +
170+
'<option ng-repeat="r in robots">{{r}}</option>' +
171+
'</select>');
172+
expect(scope.form.name.$pristine).toBeTruthy();
173+
browserTrigger(element.find('option').eq(0));
174+
expect(scope.form.name.$dirty).toBeTruthy();
175+
expect(scope.name).toBe('c3p0');
176+
});
177+
178+
179+
it('should rename select controls in form when interpolated name changes', function() {
180+
scope.nameID = "A";
181+
compile('<select ng-model="name" name="name{{nameID}}"></select>');
182+
expect(scope.form.nameA.$name).toBe('nameA');
183+
var oldModel = scope.form.nameA;
184+
scope.nameID = "B";
185+
scope.$digest();
186+
expect(scope.form.nameA).toBeUndefined();
187+
expect(scope.form.nameB).toBe(oldModel);
188+
expect(scope.form.nameB.$name).toBe('nameB');
189+
});
190+
191+
151192
describe('empty option', function() {
152193

153194
it('should select the empty option when model is undefined', function() {

0 commit comments

Comments
 (0)