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

fix(form): make ngForm $pristine after nested control.$setPristine() (brute force version) #13798

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/ng/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
var nullFormCtrl = {
$addControl: noop,
$$renameControl: nullFormRenameControl,
$$updatePristine: noop,
$removeControl: noop,
$setValidity: noop,
$setDirty: noop,
Expand Down Expand Up @@ -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();
}
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/ng/directive/ngModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

/**
Expand Down
161 changes: 158 additions & 3 deletions test/ng/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
'<form name="parentForm">' +
'<div ng-form name="childForm"></div>' +
'</form>')(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(
'<form name="form">' +
'<div ng-form name="childForm">' +
'<input ng-model="inputModel1" name="input1">' +
'<input ng-model="inputModel2" name="input2">' +
'</div>' +
'</form>')(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(
'<form name="outerForm1">' +
'<div ng-form name="outerForm2">' +
'<div ng-form name="childForm1"></div>' +
'<div ng-form name="childForm2"></div>' +
'</div>' +
'</form>')(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(
'<form name="outerForm">' +
'<div ng-form name="innerForm"></div>' +
'</form>')(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('<input ng-model="inputModel" name="input">')(scope),
inputCtrl1 = input1.controller('ngModel'),

input2 = $compile('<div ng-form name="input"></div>')(scope),
inputCtrl2 = input2.controller('form');

// test for input
test(input1, inputCtrl1);
dealoc(doc);

// test for ng-form
test(input2, inputCtrl2);
});
});

describe('$setUntouched', function() {
Expand Down
8 changes: 7 additions & 1 deletion test/ng/directive/ngModelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<form><input></form>');
Expand Down Expand Up @@ -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() {
Expand Down