Skip to content

Commit a8fba75

Browse files
committed
feat(input): support dynamic element validation
Interpolates the form and form control attribute name, so that dynamic form controls (such as those rendered in an ngRepeat) will always have their expected interpolated name. The control will be present in its parent form controller with the interpolated property name, and this name can change when the interpolated value changes. Closes angular#4791
1 parent ace40d5 commit a8fba75

File tree

5 files changed

+154
-9
lines changed

5 files changed

+154
-9
lines changed

src/ng/directive/form.js

+31-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,19 @@ 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+
146+
form.$$parentForm = parentForm;
147+
130148
/**
131149
* @ngdoc method
132150
* @name form.FormController#$removeControl
@@ -466,13 +484,20 @@ var formDirectiveFactory = function(isNgForm) {
466484
});
467485
}
468486

469-
var parentFormCtrl = formElement.parent().controller('form'),
470-
alias = attr.name || attr.ngForm;
487+
var parentFormCtrl = controller.$$parentForm,
488+
alias = controller.$name;
471489

472490
if (alias) {
473491
setter(scope, alias, controller, alias);
492+
attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) {
493+
if (alias === newValue) return;
494+
setter(scope, alias, undefined, alias);
495+
alias = newValue;
496+
setter(scope, alias, controller, alias);
497+
parentFormCtrl.$$renameControl(controller, alias);
498+
});
474499
}
475-
if (parentFormCtrl) {
500+
if (parentFormCtrl !== nullFormCtrl) {
476501
formElement.on('$destroy', function() {
477502
parentFormCtrl.$removeControl(controller);
478503
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

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

1292+
1293+
it('should interpolate input names', 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 rename form controls in form when interpolated name changes', function() {
1303+
scope.nameID = "A";
1304+
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
1305+
expect(scope.form.nameA.$name).toBe('nameA');
1306+
var oldModel = scope.form.nameA;
1307+
scope.nameID = "B";
1308+
scope.$digest();
1309+
expect(scope.form.nameA).toBeUndefined();
1310+
expect(scope.form.nameB).toBe(oldModel);
1311+
expect(scope.form.nameB.$name).toBe('nameB');
1312+
});
1313+
1314+
1315+
it('should rename form controls in null form when interpolated name changes', function() {
1316+
var element = $compile('<input type="text" ng-model="name" name="name{{nameID}}" />')(scope);
1317+
scope.nameID = "A";
1318+
scope.$digest();
1319+
var model = element.controller('ngModel');
1320+
expect(model.$name).toBe('nameA');
1321+
1322+
scope.nameID = "B";
1323+
scope.$digest();
1324+
expect(model.$name).toBe('nameB');
1325+
});
1326+
1327+
12921328
describe('"change" event', function() {
12931329
function assertBrowserSupportsChangeEvent(inputEventSupported) {
12941330
// Force browser to report a lack of an 'input' event

test/ng/directive/selectSpec.js

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

150150

151+
it('should interpolate select names', 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 rename select controls in form when interpolated name changes', function() {
166+
scope.nameID = "A";
167+
compile('<select ng-model="name" name="name{{nameID}}"></select>');
168+
expect(scope.form.nameA.$name).toBe('nameA');
169+
var oldModel = scope.form.nameA;
170+
scope.nameID = "B";
171+
scope.$digest();
172+
expect(scope.form.nameA).toBeUndefined();
173+
expect(scope.form.nameB).toBe(oldModel);
174+
expect(scope.form.nameB.$name).toBe('nameB');
175+
});
176+
177+
151178
describe('empty option', function() {
152179

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

0 commit comments

Comments
 (0)