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

ngModelController.$validators pipeline and refactoring #7377

Closed
wants to merge 6 commits into from
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
1 change: 0 additions & 1 deletion src/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"nodeName_": false,
"uid": false,

"REGEX_STRING_REGEXP" : false,
"lowercase": false,
"uppercase": false,
"manualLowercase": false,
Expand Down
3 changes: 0 additions & 3 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
-angularModule,
-nodeName_,
-uid,
-REGEX_STRING_REGEXP,

-lowercase,
-uppercase,
Expand Down Expand Up @@ -103,8 +102,6 @@
* <div doc-module-components="ng"></div>
*/

var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;

/**
* @ngdoc function
* @name angular.lowercase
Expand Down
12 changes: 12 additions & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@
ngModelDirective,
ngListDirective,
ngChangeDirective,
patternDirective,
patternDirective,
requiredDirective,
requiredDirective,
minlengthDirective,
minlengthDirective,
maxlengthDirective,
maxlengthDirective,
ngValueDirective,
ngModelOptionsDirective,
ngAttributeAliasDirectives,
Expand Down Expand Up @@ -182,8 +188,14 @@ function publishExternalAPI(angular){
ngModel: ngModelDirective,
ngList: ngListDirective,
ngChange: ngChangeDirective,
pattern: patternDirective,
ngPattern: patternDirective,
required: requiredDirective,
ngRequired: requiredDirective,
minlength: minlengthDirective,
ngMinlength: minlengthDirective,
maxlength: maxlengthDirective,
ngMaxlength: maxlengthDirective,
ngValue: ngValueDirective,
ngModelOptions: ngModelOptionsDirective
}).
Expand Down
2 changes: 1 addition & 1 deletion src/ng/directive/attrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
//special case ngPattern when a literal regular expression value
//is used as the expression (this way we don't have to watch anything).
if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
var match = attr.ngPattern.match(/^\/(.+)\/([a-z]*)$/);
if (match) {
attr.$set("ngPattern", new RegExp(match[1], match[2]));
return;
Expand Down
197 changes: 121 additions & 76 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -973,56 +973,6 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
};

// pattern validator
if (attr.ngPattern) {
var regexp, patternExp = attr.ngPattern;
attr.$observe('pattern', function(regex) {
if(isString(regex)) {
var match = regex.match(REGEX_STRING_REGEXP);
if(match) {
regex = new RegExp(match[1], match[2]);
}
}

if (regex && !regex.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
regex, startingTag(element));
}

regexp = regex || undefined;
});

var patternValidator = function(value) {
return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value), value);
};

ctrl.$formatters.push(patternValidator);
ctrl.$parsers.push(patternValidator);
}

// min length validator
if (attr.ngMinlength) {
var minlength = int(attr.ngMinlength);
var minLengthValidator = function(value) {
return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value);
};

ctrl.$parsers.push(minLengthValidator);
ctrl.$formatters.push(minLengthValidator);
}

// max length validator
if (attr.ngMaxlength) {
var maxlength = int(attr.ngMaxlength);
var maxLengthValidator = function(value) {
return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value);
};

ctrl.$parsers.push(maxLengthValidator);
ctrl.$formatters.push(maxLengthValidator);
}
}

function weekParser(isoWeek) {
Expand Down Expand Up @@ -1435,6 +1385,12 @@ var VALID_CLASS = 'ng-valid',
* ngModel.$formatters.push(formatter);
* ```
*
* @property {Object.<string, function>} $validators A collection of validators that are applied
* whenever the model value changes. The key value within the object refers to the name of the
* validator while the function refers to the validation operation. The validation operation is
* provided with the model value as an argument and must return a true or false value depending
* on the response of that validation.
*
* @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
* view value has changed. It is called with no arguments, and its return value is ignored.
* This can be used in place of additional $watches against the model value.
Expand Down Expand Up @@ -1551,6 +1507,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$validators = {};
this.$parsers = [];
this.$formatters = [];
this.$viewChangeListeners = [];
Expand Down Expand Up @@ -1626,7 +1583,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* Change the validity state, and notifies the form when the control changes validity. (i.e. it
* does not notify form if given validator is already marked as invalid).
*
* This method should be called by validators - i.e. the parser or formatter functions.
* This method can be called within $parsers/$formatters. However, if possible, please use the
* `ngModel.$validators` pipeline which is designed to handle validations with true/false values.
*
* @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
* to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
Expand Down Expand Up @@ -1743,6 +1701,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$render();
};

/**
* @ngdoc method
* @name ngModel.NgModelController#$validate
*
* @description
* Runs each of the registered validations set on the $validators object.
*/
this.$validate = function() {
this.$$runValidators(ctrl.$modelValue, ctrl.$viewValue);
};

this.$$runValidators = function(modelValue, viewValue) {
forEach(ctrl.$validators, function(fn, name) {
ctrl.$setValidity(name, fn(modelValue, viewValue));
});
};

/**
* @ngdoc method
* @name ngModel.NgModelController#$commitViewValue
Expand All @@ -1755,12 +1730,13 @@ 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) {
Expand All @@ -1771,13 +1747,19 @@ 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;
ngModelSet($scope, value);
if (ctrl.$modelValue !== modelValue &&
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {

ctrl.$$runValidators(modelValue, viewValue);
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;

ngModelSet($scope, ctrl.$modelValue);
forEach(ctrl.$viewChangeListeners, function(listener) {
try {
listener();
Expand Down Expand Up @@ -1851,26 +1833,31 @@ 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 &&
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {

var formatters = ctrl.$formatters,
idx = formatters.length;

ctrl.$modelValue = value;
var viewValue = modelValue;
while(idx--) {
value = formatters[idx](value);
viewValue = formatters[idx](viewValue);
}

if (ctrl.$viewValue !== value) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value;
ctrl.$$runValidators(modelValue, viewValue);
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;

if (ctrl.$viewValue !== viewValue) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
}
}

return value;
return modelValue;
});
}];

Expand Down Expand Up @@ -2094,22 +2081,80 @@ var requiredDirective = function() {
if (!ctrl) return;
attr.required = true; // force truthy in case we are on non input element

var validator = function(value) {
if (attr.required && ctrl.$isEmpty(value)) {
ctrl.$setValidity('required', false);
return;
} else {
ctrl.$setValidity('required', true);
return value;
ctrl.$validators.required = function(modelValue, viewValue) {
return !attr.required || !ctrl.$isEmpty(viewValue);
};

attr.$observe('required', function() {
ctrl.$validate();
});
}
};
};


var patternDirective = function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;

var regexp, patternExp = attr.ngPattern || attr.pattern;
attr.$observe('pattern', function(regex) {
if(isString(regex) && regex.length > 0) {
regex = new RegExp(regex);
}

if (regex && !regex.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
regex, startingTag(elm));
}

regexp = regex || undefined;
ctrl.$validate();
});

ctrl.$validators.pattern = function(value) {
return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value);
};
}
};
};

ctrl.$formatters.push(validator);
ctrl.$parsers.unshift(validator);

attr.$observe('required', function() {
validator(ctrl.$viewValue);
var maxlengthDirective = function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;

var maxlength = 0;
attr.$observe('maxlength', function(value) {
maxlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(value) {
return ctrl.$isEmpty(value) || value.length <= maxlength;
};
}
};
};

var minlengthDirective = function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;

var minlength = 0;
attr.$observe('minlength', function(value) {
minlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.minlength = function(value) {
return ctrl.$isEmpty(value) || value.length >= minlength;
};
}
};
};
Expand Down
Loading