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

feat(ngModel, form): add 'reset all errors' functionality #11012

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions src/ng/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var nullFormCtrl = {
$$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
$resetErrors: noop,
$setDirty: noop,
$setPristine: noop,
$setSubmitted: noop
Expand Down Expand Up @@ -183,6 +184,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
addSetValidityMethod({
ctrl: this,
$element: element,
classCache: {},
set: function(object, property, controller) {
var list = object[property];
if (!list) {
Expand Down Expand Up @@ -281,6 +283,28 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$submitted = true;
parentForm.$setSubmitted();
};

/**
* @ngdoc method
* @name form.FormController#$resetErrors
*
* @description
* Deletes all validities of a form control.
*/
form.$resetErrors = function() {
var errors = copy(form.$error);
forEach(errors, function(fields, validityName) {
forEach(fields, function(field) {
field.$resetErrors();
cleanUp(form.$error, validityName);
$animate.removeClass(element, INVALID_CLASS + '-' + snake_case(validityName, '-'));
});
});

function cleanUp(object, name) {
delete object[name];
}
};
}

/**
Expand Down
62 changes: 59 additions & 3 deletions src/ng/directive/ngModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
currentValidationRunId = 0;

var classCache = {};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setValidity
Expand All @@ -344,6 +345,31 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
addSetValidityMethod({
ctrl: this,
$element: $element,
classCache: classCache,
set: function(object, property) {
object[property] = true;
},
unset: function(object, property) {
delete object[property];
},
parentForm: parentForm,
$animate: $animate
});

/**
* @ngdoc method
* @name ngModel.NgModelController#$resetErrors
*
* @description
* Reset all errors (added by $setValidity method).
*
* This method should be called in order to reset all ng-invalid-* css classes and all validities added within $setValidity method.
*
*/
addResetErrorsMethod({
ctrl: this,
$element: $element,
classCache: classCache,
set: function(object, property) {
object[property] = true;
},
Expand Down Expand Up @@ -1231,13 +1257,11 @@ var ngModelOptionsDirective = function() {
};
};



// helper methods
function addSetValidityMethod(context) {
var ctrl = context.ctrl,
$element = context.$element,
classCache = {},
classCache = context.classCache,
set = context.set,
unset = context.unset,
parentForm = context.parentForm,
Expand Down Expand Up @@ -1329,6 +1353,38 @@ function addSetValidityMethod(context) {
}
}

function addResetErrorsMethod(context) {
var ctrl = context.ctrl,
$element = context.$element,
classCache = context.classCache,
set = context.set,
unset = context.unset,
parentForm = context.parentForm,
$animate = context.$animate;

ctrl.$resetErrors = resetErrors;

function resetErrors(controller) {
var errors = copy(ctrl.$error);
forEach(errors, function(value, validationName) {
unset(ctrl.$error, validationName, controller);
unset(ctrl.$$success, validationName, controller);
toggleValidationCss(validationName);
});
}

function cachedToggleClass(className) {
$animate.removeClass($element, className);
delete classCache[className];
}

function toggleValidationCss(validationErrorKey) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
cachedToggleClass(VALID_CLASS + validationErrorKey);
cachedToggleClass(INVALID_CLASS + validationErrorKey);
}
}

function isObjectEmpty(obj) {
if (obj) {
for (var prop in obj) {
Expand Down
53 changes: 53 additions & 0 deletions test/ng/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1000,3 +1000,56 @@ describe('form animations', function() {
assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error');
}));
});

describe('reset errors', function() {

var doc, scope, form;
beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) {
scope = $rootScope.$new();
doc = jqLite('<form name="myForm"><input name="alias" ng-model="name" type="text"/></form>');
$rootElement.append(doc);
$compile(doc)(scope);
$animate.queue = [];
form = scope.myForm;
}));

afterEach(function() {
dealoc(doc);
});

function expectClear() {
expect(form.$$success).toEqual({});
expect(form.$error).toEqual({});
expect(form.alias.$error).toEqual({});
}

function expectError() {
expect(form.$error.someError.length).toBe(1);
expect(form.alias.$error).toEqual({someError: true});
expect(form.$$success).toEqual({});
}

function expectTwoErrors() {
expect(form.$error.someError1.length).toBe(1);
expect(form.$error.someError2.length).toBe(1);
expect(form.alias.$error).toEqual({someError1: true, someError2: true});
expect(form.$$success).toEqual({});
}

it('should trigger an animation when invalid', function() {
expectClear();

form.alias.$setValidity('someError', false);
expectError();

form.$resetErrors();
expectClear();

form.alias.$setValidity('someError1', false);
form.alias.$setValidity('someError2', false);
expectTwoErrors();

form.$resetErrors();
expectClear();
});
});
48 changes: 48 additions & 0 deletions test/ng/directive/ngModelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2263,3 +2263,51 @@ describe('ngModelOptions attributes', function() {
expect($rootScope.changed).toHaveBeenCalledOnce();
});
});

describe('reset errors', function() {

function assertValidAnimation(animation, event, classNameA, classNameB) {
expect(animation.event).toBe(event);
expect(animation.args[1]).toBe(classNameA);
if (classNameB) expect(animation.args[2]).toBe(classNameB);
}

var doc, input, scope, model;

beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) {
scope = $rootScope.$new();
doc = jqLite('<form name="myForm">' +
' <input type="text" ng-model="input" name="myInput" />' +
'</form>');
$rootElement.append(doc);
$compile(doc)(scope);
$animate.queue = [];

input = doc.find('input');
model = scope.myForm.myInput;
}));

afterEach(function() {
dealoc(input);
});

function expectErrors(errors) {
for (var index = 0; index < errors.length; index++) {
var currentError = errors[index];
expect(model.$error[currentError]).toBe(true);
}
}

function expectClear() {
expect(model.$error).toEqual({});
}

it('should clear all errors', inject(function($animate) {
model.$setValidity('required', false);
model.$setValidity('onlyNumbers', false);
expectErrors(['required', 'onlyNumbers']);
model.$resetErrors();
expectClear();

}));
});