feat(input): add $touched and $untouched states

Sets the ngModel controller property $touched to True and $untouched to False whenever a 'blur' event is triggered over a control with the ngModel directive.
Also adds the $setTouched and $setUntouched methods to the NgModelController.

References #583
This commit is contained in:
Arturo Guzman
2014-06-03 00:58:04 -04:00
committed by Matias Niemelä
parent 94bcc03f3e
commit adcc5a00bf
3 changed files with 115 additions and 4 deletions

View File

@@ -5,7 +5,9 @@
-VALID_CLASS,
-INVALID_CLASS,
-PRISTINE_CLASS,
-DIRTY_CLASS
-DIRTY_CLASS,
-UNTOUCHED_CLASS,
-TOUCHED_CLASS
*/
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
@@ -1407,7 +1409,9 @@ var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sni
var VALID_CLASS = 'ng-valid',
INVALID_CLASS = 'ng-invalid',
PRISTINE_CLASS = 'ng-pristine',
DIRTY_CLASS = 'ng-dirty';
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched';
/**
* @ngdoc type
@@ -1442,6 +1446,8 @@ var VALID_CLASS = 'ng-valid',
*
* @property {Object} $error An object hash with all errors as keys.
*
* @property {boolean} $untouched True if control has not lost focus yet.
* @property {boolean} $touched True if control has lost focus.
* @property {boolean} $pristine True if user has not interacted with the control yet.
* @property {boolean} $dirty True if user has already interacted with the control.
* @property {boolean} $valid True if there is no error.
@@ -1555,6 +1561,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$parsers = [];
this.$formatters = [];
this.$viewChangeListeners = [];
this.$untouched = true;
this.$touched = false;
this.$pristine = true;
this.$dirty = false;
this.$valid = true;
@@ -1609,7 +1617,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// Setup initial state of the control
$element.addClass(PRISTINE_CLASS);
$element
.addClass(PRISTINE_CLASS)
.addClass(UNTOUCHED_CLASS);
toggleValidCss(true);
// convenience method for easy toggling of classes
@@ -1679,6 +1689,38 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
$animate.addClass($element, PRISTINE_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setUntouched
*
* @description
* Sets the control to its untouched state.
*
* This method can be called to remove the 'ng-touched' class and set the control to its
* untouched state (ng-untouched class).
*/
this.$setUntouched = function() {
ctrl.$touched = false;
ctrl.$untouched = true;
$animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setTouched
*
* @description
* Sets the control to its touched state.
*
* This method can be called to remove the 'ng-untouched' class and set the control to its
* touched state (ng-touched class).
*/
this.$setTouched = function() {
ctrl.$touched = true;
ctrl.$untouched = false;
$animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$rollbackViewValue
@@ -2014,6 +2056,12 @@ var ngModelDirective = function() {
});
});
}
element.on('blur', function(ev) {
scope.$apply(function() {
modelCtrl.$setTouched();
});
});
}
}
};

View File

@@ -48,6 +48,8 @@ beforeEach(function() {
toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'),
toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
toBeUntouched: cssMatcher('ng-untouched', 'ng-touched'),
toBeTouched: cssMatcher('ng-touched', 'ng-untouched'),
toBeShown: function() {
this.message = valueFn(
"Expected element " + (this.isNot ? "": "not ") + "to have 'ng-hide' class");

View File

@@ -51,6 +51,8 @@ describe('NgModelController', function() {
it('should init the properties', function() {
expect(ctrl.$untouched).toBe(true);
expect(ctrl.$touched).toBe(false);
expect(ctrl.$dirty).toBe(false);
expect(ctrl.$pristine).toBe(true);
expect(ctrl.$valid).toBe(true);
@@ -133,6 +135,28 @@ describe('NgModelController', function() {
});
});
describe('setUntouched', function() {
it('should set control to its untouched state', function() {
ctrl.$setTouched();
ctrl.$setUntouched();
expect(ctrl.$touched).toBe(false);
expect(ctrl.$untouched).toBe(true);
});
});
describe('setTouched', function() {
it('should set control to its touched state', function() {
ctrl.$setUntouched();
ctrl.$setTouched();
expect(ctrl.$touched).toBe(true);
expect(ctrl.$untouched).toBe(false);
});
});
describe('view -> model', function() {
it('should set the value to $viewValue', function() {
@@ -265,13 +289,14 @@ describe('NgModelController', function() {
describe('ngModel', function() {
it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty)',
it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)',
inject(function($compile, $rootScope, $sniffer) {
var element = $compile('<input type="email" ng-model="value" />')($rootScope);
$rootScope.$digest();
expect(element).toBeValid();
expect(element).toBePristine();
expect(element).toBeUntouched();
expect(element.hasClass('ng-valid-email')).toBe(true);
expect(element.hasClass('ng-invalid-email')).toBe(false);
@@ -297,6 +322,9 @@ describe('ngModel', function() {
expect(element.hasClass('ng-valid-email')).toBe(true);
expect(element.hasClass('ng-invalid-email')).toBe(false);
browserTrigger(element, 'blur');
expect(element).toBeTouched();
dealoc(element);
}));
@@ -309,6 +337,23 @@ describe('ngModel', function() {
expect(element).toHaveClass('ng-invalid-required');
}));
it('should set the control touched state on "blur" event', inject(function($compile, $rootScope) {
var element = $compile('<form name="myForm">' +
'<input name="myControl" ng-model="value" >' +
'</form>')($rootScope);
var inputElm = element.find('input');
var control = $rootScope.myForm.myControl;
expect(control.$touched).toBe(false);
expect(control.$untouched).toBe(true);
browserTrigger(inputElm, 'blur');
expect(control.$touched).toBe(true);
expect(control.$untouched).toBe(false);
dealoc(element);
}));
it('should register/deregister a nested ngModel with parent form when entering or leaving DOM',
inject(function($compile, $rootScope) {
@@ -2687,6 +2732,22 @@ describe('NgModel animations', function() {
assertValidAnimation(animations[1], 'addClass', 'ng-pristine');
}));
it('should trigger an animation when untouched', inject(function($animate) {
model.$setUntouched();
var animations = findElementAnimations(input, $animate.queue);
assertValidAnimation(animations[0], 'setClass', 'ng-untouched');
expect(animations[0].args[2]).toBe('ng-touched');
}));
it('should trigger an animation when touched', inject(function($animate) {
model.$setTouched();
var animations = findElementAnimations(input, $animate.queue);
assertValidAnimation(animations[0], 'setClass', 'ng-touched', 'ng-untouched');
expect(animations[0].args[2]).toBe('ng-untouched');
}));
it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) {
model.$setValidity('custom-error', false);