Skip to content

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 angular#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)