From 5c884242cb5b015bc33b2a025217f32097edb24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 27 May 2014 13:05:12 -0400 Subject: [PATCH] Strict validators --- src/ng/directive/input.js | 47 ++++++++++++++++------------- test/ng/directive/inputSpec.js | 55 +++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 36b903428dce..5380498b7e5d 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1762,11 +1762,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Runs each of the registered validations set on the $validators object. */ - this.$validate = function() { - ctrl.$$deferValidation = false; - var value = ctrl.$modelValue; + this.$validate = function(modelValue, viewValue) { + modelValue = modelValue || ctrl.$modelValue; + viewValue = viewValue || ctrl.$viewValue; forEach(ctrl.$validators, function(fn, name) { - ctrl.$setValidity(name, fn(value)); + ctrl.$setValidity(name, fn(modelValue, viewValue)); }); }; @@ -1782,12 +1782,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * usually handles calling this in response to input events. */ this.$commitViewValue = function() { - var value = ctrl.$viewValue; + var viewValue = ctrl.$viewValue; $timeout.cancel(pendingDebounce); - if (ctrl.$$lastCommittedViewValue === value) { + if (ctrl.$$lastCommittedViewValue === viewValue) { return; } - ctrl.$$lastCommittedViewValue = value; + ctrl.$$lastCommittedViewValue = viewValue; // change to dirty if (ctrl.$pristine) { @@ -1798,15 +1798,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ parentForm.$setDirty(); } + var modelValue = viewValue; forEach(ctrl.$parsers, function(fn) { - value = fn(value); + modelValue = fn(modelValue); }); - if (ctrl.$modelValue !== value) { - ctrl.$modelValue = value; - ctrl.$validate(); + if (ctrl.$modelValue !== modelValue && (!ctrl.$rawModelValue || ctrl.$rawModelValue != modelValue)) { - ngModelSet($scope, value); + ctrl.$validate(modelValue, viewValue); + ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + ctrl.$rawModelValue = ctrl.$valid ? undefined : modelValue; + + ngModelSet($scope, ctrl.$modelValue); forEach(ctrl.$viewChangeListeners, function(listener) { try { listener(); @@ -1880,28 +1883,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ // model -> value $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + var modelValue = ngModelGet($scope); // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { + if (ctrl.$modelValue !== modelValue && (!ctrl.$rawModelValue || ctrl.$rawModelValue != modelValue)) { var formatters = ctrl.$formatters, idx = formatters.length; - ctrl.$modelValue = value; - ctrl.$validate(); - + var viewValue = modelValue; while(idx--) { - value = formatters[idx](value); + viewValue = formatters[idx](viewValue); } - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value; + ctrl.$validate(modelValue, viewValue); + ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + ctrl.$rawModelValue = ctrl.$valid ? undefined : modelValue; + + if (ctrl.$viewValue !== viewValue) { + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); } } - return value; + return modelValue; }); }]; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 0aa68b604428..206c6218145a 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -280,19 +280,54 @@ describe('NgModelController', function() { expect(ctrl.$valid).toBe(true); }); + it('should perform validations when $validate() is called', function() { + ctrl.$validators.uppercase = function(value) { + return (/^[A-Z]+$/).test(value); + }; + + ctrl.$modelValue = 'test'; + ctrl.$validate(); + + expect(ctrl.$valid).toBe(false); + + ctrl.$modelValue = 'TEST'; + ctrl.$validate(); + + expect(ctrl.$valid).toBe(true); + }); + it('should always perform validations using the parsed model value', function() { - var expectedSpy = jasmine.createSpy('expected value'); + var captures; + ctrl.$validators.raw = function() { + captures = arguments; + return captures[0]; + }; - ctrl.$validators.raw = expectedSpy; ctrl.$parsers.push(function(value) { return value.toUpperCase(); }); ctrl.$setViewValue('my-value'); - expect(expectedSpy).toHaveBeenCalled(); - var data = expectedSpy.mostRecentCall.args[0]; - expect(data).toBe('MY-VALUE'); + expect(captures).toEqual(['MY-VALUE', 'my-value']); + }); + + it('should always perform validations using the formatted view value', function() { + var captures; + ctrl.$validators.raw = function() { + captures = arguments; + return captures[0]; + }; + + ctrl.$formatters.push(function(value) { + return value + '...'; + }); + + scope.$apply(function() { + scope.value = 'matias'; + }); + + expect(captures).toEqual(['matias', 'matias...']); }); it('should only perform validations if the view value is different', function() { @@ -311,7 +346,7 @@ describe('NgModelController', function() { expect(count).toBe(2); }); - it('should perform validations each time the model value changes within a digest', function() { + it('should perform validations twice each time the model value changes within a digest', function() { var count = 0; ctrl.$validators.number = function(value) { count++; @@ -325,16 +360,16 @@ describe('NgModelController', function() { } val(''); - expect(count).toBe(1); + expect(count).toBe(2); val(1); - expect(count).toBe(2); + expect(count).toBe(3); val(1); - expect(count).toBe(2); + expect(count).toBe(3); val(''); - expect(count).toBe(3); + expect(count).toBe(4); }); it('should only validate to true if all validations are true', function() {