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

Commit adcc5a0

Browse files
guzartmatsko
authored andcommitted
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
1 parent 94bcc03 commit adcc5a0

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

src/ng/directive/input.js

+51-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
-VALID_CLASS,
66
-INVALID_CLASS,
77
-PRISTINE_CLASS,
8-
-DIRTY_CLASS
8+
-DIRTY_CLASS,
9+
-UNTOUCHED_CLASS,
10+
-TOUCHED_CLASS
911
*/
1012

1113
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
14071409
var VALID_CLASS = 'ng-valid',
14081410
INVALID_CLASS = 'ng-invalid',
14091411
PRISTINE_CLASS = 'ng-pristine',
1410-
DIRTY_CLASS = 'ng-dirty';
1412+
DIRTY_CLASS = 'ng-dirty',
1413+
UNTOUCHED_CLASS = 'ng-untouched',
1414+
TOUCHED_CLASS = 'ng-touched';
14111415

14121416
/**
14131417
* @ngdoc type
@@ -1442,6 +1446,8 @@ var VALID_CLASS = 'ng-valid',
14421446
*
14431447
* @property {Object} $error An object hash with all errors as keys.
14441448
*
1449+
* @property {boolean} $untouched True if control has not lost focus yet.
1450+
* @property {boolean} $touched True if control has lost focus.
14451451
* @property {boolean} $pristine True if user has not interacted with the control yet.
14461452
* @property {boolean} $dirty True if user has already interacted with the control.
14471453
* @property {boolean} $valid True if there is no error.
@@ -1555,6 +1561,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15551561
this.$parsers = [];
15561562
this.$formatters = [];
15571563
this.$viewChangeListeners = [];
1564+
this.$untouched = true;
1565+
this.$touched = false;
15581566
this.$pristine = true;
15591567
this.$dirty = false;
15601568
this.$valid = true;
@@ -1609,7 +1617,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16091617

16101618

16111619
// Setup initial state of the control
1612-
$element.addClass(PRISTINE_CLASS);
1620+
$element
1621+
.addClass(PRISTINE_CLASS)
1622+
.addClass(UNTOUCHED_CLASS);
16131623
toggleValidCss(true);
16141624

16151625
// convenience method for easy toggling of classes
@@ -1679,6 +1689,38 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16791689
$animate.addClass($element, PRISTINE_CLASS);
16801690
};
16811691

1692+
/**
1693+
* @ngdoc method
1694+
* @name ngModel.NgModelController#$setUntouched
1695+
*
1696+
* @description
1697+
* Sets the control to its untouched state.
1698+
*
1699+
* This method can be called to remove the 'ng-touched' class and set the control to its
1700+
* untouched state (ng-untouched class).
1701+
*/
1702+
this.$setUntouched = function() {
1703+
ctrl.$touched = false;
1704+
ctrl.$untouched = true;
1705+
$animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
1706+
};
1707+
1708+
/**
1709+
* @ngdoc method
1710+
* @name ngModel.NgModelController#$setTouched
1711+
*
1712+
* @description
1713+
* Sets the control to its touched state.
1714+
*
1715+
* This method can be called to remove the 'ng-untouched' class and set the control to its
1716+
* touched state (ng-touched class).
1717+
*/
1718+
this.$setTouched = function() {
1719+
ctrl.$touched = true;
1720+
ctrl.$untouched = false;
1721+
$animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
1722+
};
1723+
16821724
/**
16831725
* @ngdoc method
16841726
* @name ngModel.NgModelController#$rollbackViewValue
@@ -2014,6 +2056,12 @@ var ngModelDirective = function() {
20142056
});
20152057
});
20162058
}
2059+
2060+
element.on('blur', function(ev) {
2061+
scope.$apply(function() {
2062+
modelCtrl.$setTouched();
2063+
});
2064+
});
20172065
}
20182066
}
20192067
};

test/helpers/matchers.js

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ beforeEach(function() {
4848
toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
4949
toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'),
5050
toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
51+
toBeUntouched: cssMatcher('ng-untouched', 'ng-touched'),
52+
toBeTouched: cssMatcher('ng-touched', 'ng-untouched'),
5153
toBeShown: function() {
5254
this.message = valueFn(
5355
"Expected element " + (this.isNot ? "": "not ") + "to have 'ng-hide' class");

test/ng/directive/inputSpec.js

+62-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ describe('NgModelController', function() {
5151

5252

5353
it('should init the properties', function() {
54+
expect(ctrl.$untouched).toBe(true);
55+
expect(ctrl.$touched).toBe(false);
5456
expect(ctrl.$dirty).toBe(false);
5557
expect(ctrl.$pristine).toBe(true);
5658
expect(ctrl.$valid).toBe(true);
@@ -133,6 +135,28 @@ describe('NgModelController', function() {
133135
});
134136
});
135137

138+
describe('setUntouched', function() {
139+
140+
it('should set control to its untouched state', function() {
141+
ctrl.$setTouched();
142+
143+
ctrl.$setUntouched();
144+
expect(ctrl.$touched).toBe(false);
145+
expect(ctrl.$untouched).toBe(true);
146+
});
147+
});
148+
149+
describe('setTouched', function() {
150+
151+
it('should set control to its touched state', function() {
152+
ctrl.$setUntouched();
153+
154+
ctrl.$setTouched();
155+
expect(ctrl.$touched).toBe(true);
156+
expect(ctrl.$untouched).toBe(false);
157+
});
158+
});
159+
136160
describe('view -> model', function() {
137161

138162
it('should set the value to $viewValue', function() {
@@ -265,13 +289,14 @@ describe('NgModelController', function() {
265289

266290
describe('ngModel', function() {
267291

268-
it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty)',
292+
it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)',
269293
inject(function($compile, $rootScope, $sniffer) {
270294
var element = $compile('<input type="email" ng-model="value" />')($rootScope);
271295

272296
$rootScope.$digest();
273297
expect(element).toBeValid();
274298
expect(element).toBePristine();
299+
expect(element).toBeUntouched();
275300
expect(element.hasClass('ng-valid-email')).toBe(true);
276301
expect(element.hasClass('ng-invalid-email')).toBe(false);
277302

@@ -297,6 +322,9 @@ describe('ngModel', function() {
297322
expect(element.hasClass('ng-valid-email')).toBe(true);
298323
expect(element.hasClass('ng-invalid-email')).toBe(false);
299324

325+
browserTrigger(element, 'blur');
326+
expect(element).toBeTouched();
327+
300328
dealoc(element);
301329
}));
302330

@@ -309,6 +337,23 @@ describe('ngModel', function() {
309337
expect(element).toHaveClass('ng-invalid-required');
310338
}));
311339

340+
it('should set the control touched state on "blur" event', inject(function($compile, $rootScope) {
341+
var element = $compile('<form name="myForm">' +
342+
'<input name="myControl" ng-model="value" >' +
343+
'</form>')($rootScope);
344+
var inputElm = element.find('input');
345+
var control = $rootScope.myForm.myControl;
346+
347+
expect(control.$touched).toBe(false);
348+
expect(control.$untouched).toBe(true);
349+
350+
browserTrigger(inputElm, 'blur');
351+
expect(control.$touched).toBe(true);
352+
expect(control.$untouched).toBe(false);
353+
354+
dealoc(element);
355+
}));
356+
312357

313358
it('should register/deregister a nested ngModel with parent form when entering or leaving DOM',
314359
inject(function($compile, $rootScope) {
@@ -2687,6 +2732,22 @@ describe('NgModel animations', function() {
26872732
assertValidAnimation(animations[1], 'addClass', 'ng-pristine');
26882733
}));
26892734

2735+
it('should trigger an animation when untouched', inject(function($animate) {
2736+
model.$setUntouched();
2737+
2738+
var animations = findElementAnimations(input, $animate.queue);
2739+
assertValidAnimation(animations[0], 'setClass', 'ng-untouched');
2740+
expect(animations[0].args[2]).toBe('ng-touched');
2741+
}));
2742+
2743+
it('should trigger an animation when touched', inject(function($animate) {
2744+
model.$setTouched();
2745+
2746+
var animations = findElementAnimations(input, $animate.queue);
2747+
assertValidAnimation(animations[0], 'setClass', 'ng-touched', 'ng-untouched');
2748+
expect(animations[0].args[2]).toBe('ng-untouched');
2749+
}));
2750+
26902751
it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) {
26912752
model.$setValidity('custom-error', false);
26922753

0 commit comments

Comments
 (0)