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

Commit 729c238

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 #4791 Closes #1404
1 parent dc3de7f commit 729c238

File tree

5 files changed

+154
-10
lines changed

5 files changed

+154
-10
lines changed

src/ng/directive/form.js

+31-7
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,17 +56,18 @@ 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,
57-
parentForm = element.parent().controller('form') || nullFormCtrl,
5862
controls = [];
5963

64+
var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
65+
6066
// init state
6167
form.$error = {};
6268
form.$$success = {};
6369
form.$pending = undefined;
64-
form.$name = attrs.name || attrs.ngForm;
70+
form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
6571
form.$dirty = false;
6672
form.$pristine = true;
6773
form.$valid = true;
@@ -127,6 +133,17 @@ function FormController(element, attrs, $scope, $animate) {
127133
}
128134
};
129135

136+
// Private API: rename a form control
137+
form.$$renameControl = function(control, newName) {
138+
var oldName = control.$name;
139+
140+
if (form[oldName] === control) {
141+
delete form[oldName];
142+
}
143+
form[newName] = control;
144+
control.$name = newName;
145+
};
146+
130147
/**
131148
* @ngdoc method
132149
* @name form.FormController#$removeControl
@@ -466,13 +483,20 @@ var formDirectiveFactory = function(isNgForm) {
466483
});
467484
}
468485

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

472489
if (alias) {
473490
setter(scope, alias, controller, alias);
491+
attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) {
492+
if (alias === newValue) return;
493+
setter(scope, alias, undefined, alias);
494+
alias = newValue;
495+
setter(scope, alias, controller, alias);
496+
parentFormCtrl.$$renameControl(controller, alias);
497+
});
474498
}
475-
if (parentFormCtrl) {
499+
if (parentFormCtrl !== nullFormCtrl) {
476500
formElement.on('$destroy', function() {
477501
parentFormCtrl.$removeControl(controller);
478502
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)