diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 3ee544ea2543..6772d93243d9 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -4,6 +4,7 @@ */ var nullFormCtrl = { $addControl: noop, + $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $$setPending: noop, @@ -14,6 +15,10 @@ var nullFormCtrl = { }, SUBMITTED_CLASS = 'ng-submitted'; +function nullFormRenameControl(control, name) { + control.$name = name; +} + /** * @ngdoc type * @name form.FormController @@ -51,17 +56,18 @@ SUBMITTED_CLASS = 'ng-submitted'; * */ //asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; -function FormController(element, attrs, $scope, $animate) { +FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; +function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, controls = []; + var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; + // init state form.$error = {}; form.$$success = {}; form.$pending = undefined; - form.$name = attrs.name || attrs.ngForm; + form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); form.$dirty = false; form.$pristine = true; form.$valid = true; @@ -127,6 +133,17 @@ function FormController(element, attrs, $scope, $animate) { } }; + // Private API: rename a form control + form.$$renameControl = function(control, newName) { + var oldName = control.$name; + + if (form[oldName] === control) { + delete form[oldName]; + } + form[newName] = control; + control.$name = newName; + }; + /** * @ngdoc method * @name form.FormController#$removeControl @@ -466,13 +483,20 @@ var formDirectiveFactory = function(isNgForm) { }); } - var parentFormCtrl = formElement.parent().controller('form'), - alias = attr.name || attr.ngForm; + var parentFormCtrl = controller.$$parentForm, + alias = controller.$name; if (alias) { setter(scope, alias, controller, alias); + attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) { + if (alias === newValue) return; + setter(scope, alias, undefined, alias); + alias = newValue; + setter(scope, alias, controller, alias); + parentFormCtrl.$$renameControl(controller, alias); + }); } - if (parentFormCtrl) { + if (parentFormCtrl !== nullFormCtrl) { formElement.on('$destroy', function() { parentFormCtrl.$removeControl(controller); if (alias) { diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index ba98a5a4cd17..4a50d502746e 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1657,8 +1657,8 @@ var VALID_CLASS = 'ng-valid', * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$validators = {}; @@ -1675,7 +1675,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$error = {}; // keep invalid keys here this.$$success = {}; // keep valid keys here this.$pending = undefined; // keep pending keys here - this.$name = $attr.name; + this.$name = $interpolate($attr.name || '', false)($scope); var parsedNgModel = $parse($attr.ngModel), @@ -2387,6 +2387,12 @@ var ngModelDirective = function() { // notify others, especially parent forms formCtrl.$addControl(modelCtrl); + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + formCtrl.$$renameControl(modelCtrl, newValue); + } + }); + scope.$on('$destroy', function() { formCtrl.$removeControl(modelCtrl); }); diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index f843801d2735..4a82a01e6bb3 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -782,6 +782,57 @@ describe('form', function() { }); }); + + it('should rename nested form controls when interpolated name changes', function() { + scope.idA = 'A'; + scope.idB = 'X'; + + doc = $compile( + '
' + + '
' + + '
' + + '
' + + '')(scope); + var element2 = $compile('
')(scope); + scope.nameID = "A"; + scope.$digest(); + var form = element.controller('form'); + var form2 = element2.controller('form'); + expect(form.$name).toBe('nameA'); + expect(form2.$name).toBe('nameA'); + + scope.nameID = "B"; + scope.$digest(); + expect(form.$name).toBe('nameB'); + expect(form2.$name).toBe('nameB'); + }); + + describe('$setSubmitted', function() { beforeEach(function() { doc = $compile( diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 0db042c1389d..0c43fb322eaf 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1289,6 +1289,42 @@ describe('input', function() { } })); + + it('should interpolate input names', function() { + scope.nameID = '47'; + compileInput(''); + expect(scope.form.name47.$pristine).toBeTruthy(); + changeInputValueTo('caitp'); + expect(scope.form.name47.$dirty).toBeTruthy(); + }); + + + it('should rename form controls in form when interpolated name changes', function() { + scope.nameID = "A"; + compileInput(''); + expect(scope.form.nameA.$name).toBe('nameA'); + var oldModel = scope.form.nameA; + scope.nameID = "B"; + scope.$digest(); + expect(scope.form.nameA).toBeUndefined(); + expect(scope.form.nameB).toBe(oldModel); + expect(scope.form.nameB.$name).toBe('nameB'); + }); + + + it('should rename form controls in null form when interpolated name changes', function() { + var element = $compile('')(scope); + scope.nameID = "A"; + scope.$digest(); + var model = element.controller('ngModel'); + expect(model.$name).toBe('nameA'); + + scope.nameID = "B"; + scope.$digest(); + expect(model.$name).toBe('nameB'); + }); + + describe('"change" event', function() { function assertBrowserSupportsChangeEvent(inputEventSupported) { // Force browser to report a lack of an 'input' event diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index cfcea8b947b7..e896dc05836d 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -148,6 +148,33 @@ describe('select', function() { }); + it('should interpolate select names', function() { + scope.robots = ['c3p0', 'r2d2']; + scope.name = 'r2d2'; + scope.nameID = 47; + compile(''); + expect(scope.form.name47.$pristine).toBeTruthy(); + browserTrigger(element.find('option').eq(0)); + expect(scope.form.name47.$dirty).toBeTruthy(); + expect(scope.name).toBe('c3p0'); + }); + + + it('should rename select controls in form when interpolated name changes', function() { + scope.nameID = "A"; + compile(''); + expect(scope.form.nameA.$name).toBe('nameA'); + var oldModel = scope.form.nameA; + scope.nameID = "B"; + scope.$digest(); + expect(scope.form.nameA).toBeUndefined(); + expect(scope.form.nameB).toBe(oldModel); + expect(scope.form.nameB.$name).toBe('nameB'); + }); + + describe('empty option', function() { it('should select the empty option when model is undefined', function() {