diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 30a932a46347..5cb8984a2e52 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -5,6 +5,7 @@ var nullFormCtrl = { $addControl: noop, $$renameControl: nullFormRenameControl, + $$updatePristine: noop, $removeControl: noop, $setValidity: noop, $setDirty: noop, @@ -254,19 +255,39 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * This method can be called to remove the 'ng-dirty' class and set the form to its pristine * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. + * in this form and to the parent form. * * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ form.$setPristine = function() { + form.$$setPristineSelf(); + forEach(controls, function(control) { + control.$setPristine(); + }); + }; + + // Private API: Sets the form to its pristine state. + // This method does not affect nested controls. + form.$$setPristineSelf = function() { $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); form.$dirty = false; form.$pristine = true; form.$submitted = false; - forEach(controls, function(control) { - control.$setPristine(); + form.$$parentForm.$$updatePristine(); + }; + + // Private API: update form pristine-ness + form.$$updatePristine = function() { + var isPristine = controls.every(function(control) { + return control.$pristine; }); + + if (isPristine) { + // All the nested controls are already pristine. + // Set pristine-ness only for the form itself. + form.$$setPristineSelf(); + } }; /** diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 4683f4e31c4d..d5fb671f7289 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -383,6 +383,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$pristine = true; $animate.removeClass($element, DIRTY_CLASS); $animate.addClass($element, PRISTINE_CLASS); + ctrl.$$parentForm.$$updatePristine(); }; /** diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index 500be66d25d3..e59057eb7451 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -714,9 +714,7 @@ describe('form', function() { expect(form.$error.maxlength[0].$name).toBe('childform'); inputController.$setPristine(); - expect(form.$dirty).toBe(true); - - form.$setPristine(); + expect(form.$dirty).toBe(false); // remove child form form.$removeControl(childformController); @@ -1043,6 +1041,163 @@ describe('form', function() { expect(nestedInputCtrl.$pristine).toBe(true); expect(nestedInputCtrl.$dirty).toBe(false); }); + + it('should propagate pristine-ness to the parent form', function() { + doc = $compile( + '
')(scope); + + var parentForm = doc, + childForm = parentForm.find('div').eq(0), + childFormCtrl = scope.childForm; + + childFormCtrl.$setDirty(); + scope.$apply(); + expect(parentForm).toBeDirty(); + + childFormCtrl.$setPristine(); + scope.$apply(); + expect(childForm).toBePristine(); + expect(parentForm).toBePristine(); + }); + + it('should be pristine if all the nested controls are pristine', function() { + doc = $compile( + '')(scope); + + var form = doc, + childForm = form.find('div').eq(0), + input1 = form.find('input').eq(0), + input2 = form.find('input').eq(1), + inputCtrl1 = input1.controller('ngModel'), + inputCtrl2 = input2.controller('ngModel'); + + inputCtrl1.$setDirty(); + inputCtrl1.$setDirty(); + scope.$apply(); + expect(form).toBeDirty(); + expect(childForm).toBeDirty(); + + inputCtrl2.$setDirty(); + inputCtrl2.$setDirty(); + scope.$apply(); + expect(form).toBeDirty(); + expect(childForm).toBeDirty(); + + inputCtrl1.$setPristine(); + scope.$apply(); + expect(form).toBeDirty(); + expect(childForm).toBeDirty(); + + inputCtrl2.$setPristine(); + scope.$apply(); + expect(form).toBePristine(); + expect(childForm).toBePristine(); + }); + + it('should be pristine if all the nested forms are pristine', function() { + doc = $compile( + '')(scope); + + var outerForm1 = doc, + outerForm2 = doc.find('div').eq(0), + childFormCtrl1 = scope.childForm1, + childFormCtrl2 = scope.childForm2; + + childFormCtrl1.$setDirty(); + scope.$apply(); + expect(outerForm1).toBeDirty(); + expect(outerForm2).toBeDirty(); + childFormCtrl2.$setDirty(); + scope.$apply(); + expect(outerForm1).toBeDirty(); + expect(outerForm2).toBeDirty(); + + childFormCtrl1.$setPristine(); + scope.$apply(); + expect(outerForm1).toBeDirty(); + expect(outerForm2).toBeDirty(); + + childFormCtrl2.$setPristine(); + scope.$apply(); + expect(outerForm1).toBePristine(); + expect(outerForm2).toBePristine(); + }); + + it('should properly handle added/removed controls', function() { + + var test = function(input, inputCtrl) { + doc = $compile( + '')(scope); + + var outerForm = doc, + innerForm = doc.find('div').eq(0), + innerFormCtrl = innerForm.controller('form'); + + inputCtrl.$setDirty(); + + // just add control does not change form pristine-ness + innerFormCtrl.$addControl(inputCtrl); + scope.$apply(); + expect(innerForm).toBePristine(); + + // change after adding + inputCtrl.$setDirty(); + scope.$apply(); + expect(innerForm).toBeDirty(); + + innerFormCtrl.$removeControl(inputCtrl); + + // removed control does not affect + inputCtrl.$setPristine(); + scope.$apply(); + expect(innerForm).toBeDirty(); + + innerFormCtrl.$addControl(inputCtrl); + scope.$apply(); + expect(innerForm).toBeDirty(); + + inputCtrl.$setPristine(); + scope.$apply(); + expect(innerForm).toBePristine(); + + innerFormCtrl.$removeControl(inputCtrl); + inputCtrl.$setPristine(); + innerFormCtrl.$addControl(inputCtrl); + scope.$apply(); + expect(innerForm).toBePristine(); + + inputCtrl.$setDirty(); + scope.$apply(); + expect(outerForm).toBeDirty(); + }; + + var input1 = $compile('')(scope), + inputCtrl1 = input1.controller('ngModel'), + + input2 = $compile('')(scope), + inputCtrl2 = input2.controller('form'); + + // test for input + test(input1, inputCtrl1); + dealoc(doc); + + // test for ng-form + test(input2, inputCtrl2); + }); }); describe('$setUntouched', function() { diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js index e725d495fe00..56d1476a905b 100644 --- a/test/ng/directive/ngModelSpec.js +++ b/test/ng/directive/ngModelSpec.js @@ -15,7 +15,8 @@ describe('ngModel', function() { $$setPending: jasmine.createSpy('$$setPending'), $setValidity: jasmine.createSpy('$setValidity'), $setDirty: jasmine.createSpy('$setDirty'), - $$clearControlValidity: noop + $$clearControlValidity: noop, + $$updatePristine: jasmine.createSpy('$$updatePristine') }; element = jqLite(''); @@ -145,6 +146,11 @@ describe('ngModel', function() { expect(ctrl.$dirty).toBe(false); expect(ctrl.$pristine).toBe(true); }); + + it('should propagate pristine to the parent form', function() { + ctrl.$setPristine(); + expect(parentFormCtrl.$$updatePristine).toHaveBeenCalledOnce(); + }); }); describe('setDirty', function() {