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

Commit a8c7cb8

Browse files
committed
feat(NgModel): introduce the $validators pipeline
1 parent 545d22b commit a8c7cb8

File tree

2 files changed

+200
-15
lines changed

2 files changed

+200
-15
lines changed

src/ng/directive/input.js

+51-15
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,12 @@ var VALID_CLASS = 'ng-valid',
14401440
* ngModel.$formatters.push(formatter);
14411441
* ```
14421442
*
1443+
* @property {Object.<string, function>} $validators A collection of validators that are applied
1444+
* whenever the model value changes. The key value within the object refers to the name of the
1445+
* validator while the function refers to the validation operation. The validation operation is
1446+
* provided with the model value as an argument and must return a true or false value depending
1447+
* on the response of that validation.
1448+
*
14431449
* @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
14441450
* view value has changed. It is called with no arguments, and its return value is ignored.
14451451
* This can be used in place of additional $watches against the model value.
@@ -1558,6 +1564,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15581564
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout) {
15591565
this.$viewValue = Number.NaN;
15601566
this.$modelValue = Number.NaN;
1567+
this.$validators = {};
15611568
this.$parsers = [];
15621569
this.$formatters = [];
15631570
this.$viewChangeListeners = [];
@@ -1637,7 +1644,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16371644
* Change the validity state, and notifies the form when the control changes validity. (i.e. it
16381645
* does not notify form if given validator is already marked as invalid).
16391646
*
1640-
* This method should be called by validators - i.e. the parser or formatter functions.
1647+
* This method can be called within $parsers/$formatters. However, if possible, please use the
1648+
* `ngModel.$validators` pipeline which is designed to handle validations with true/false values.
16411649
*
16421650
* @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
16431651
* to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
@@ -1786,6 +1794,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17861794
ctrl.$render();
17871795
};
17881796

1797+
/**
1798+
* @ngdoc method
1799+
* @name ngModel.NgModelController#$validate
1800+
*
1801+
* @description
1802+
* Runs each of the registered validations set on the $validators object.
1803+
*/
1804+
this.$validate = function() {
1805+
this.$$runValidators(ctrl.$modelValue, ctrl.$viewValue);
1806+
};
1807+
1808+
this.$$runValidators = function(modelValue, viewValue) {
1809+
forEach(ctrl.$validators, function(fn, name) {
1810+
ctrl.$setValidity(name, fn(modelValue, viewValue));
1811+
});
1812+
};
1813+
17891814
/**
17901815
* @ngdoc method
17911816
* @name ngModel.NgModelController#$commitViewValue
@@ -1798,12 +1823,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17981823
* usually handles calling this in response to input events.
17991824
*/
18001825
this.$commitViewValue = function() {
1801-
var value = ctrl.$viewValue;
1826+
var viewValue = ctrl.$viewValue;
18021827
$timeout.cancel(pendingDebounce);
1803-
if (ctrl.$$lastCommittedViewValue === value) {
1828+
if (ctrl.$$lastCommittedViewValue === viewValue) {
18041829
return;
18051830
}
1806-
ctrl.$$lastCommittedViewValue = value;
1831+
ctrl.$$lastCommittedViewValue = viewValue;
18071832

18081833
// change to dirty
18091834
if (ctrl.$pristine) {
@@ -1814,13 +1839,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18141839
parentForm.$setDirty();
18151840
}
18161841

1842+
var modelValue = viewValue;
18171843
forEach(ctrl.$parsers, function(fn) {
1818-
value = fn(value);
1844+
modelValue = fn(modelValue);
18191845
});
18201846

1821-
if (ctrl.$modelValue !== value) {
1822-
ctrl.$modelValue = value;
1823-
ngModelSet($scope, value);
1847+
if (ctrl.$modelValue !== modelValue &&
1848+
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
1849+
1850+
ctrl.$$runValidators(modelValue, viewValue);
1851+
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
1852+
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;
1853+
1854+
ngModelSet($scope, ctrl.$modelValue);
18241855
forEach(ctrl.$viewChangeListeners, function(listener) {
18251856
try {
18261857
listener();
@@ -1894,26 +1925,31 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18941925

18951926
// model -> value
18961927
$scope.$watch(function ngModelWatch() {
1897-
var value = ngModelGet($scope);
1928+
var modelValue = ngModelGet($scope);
18981929

18991930
// if scope model value and ngModel value are out of sync
1900-
if (ctrl.$modelValue !== value) {
1931+
if (ctrl.$modelValue !== modelValue &&
1932+
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
19011933

19021934
var formatters = ctrl.$formatters,
19031935
idx = formatters.length;
19041936

1905-
ctrl.$modelValue = value;
1937+
var viewValue = modelValue;
19061938
while(idx--) {
1907-
value = formatters[idx](value);
1939+
viewValue = formatters[idx](viewValue);
19081940
}
19091941

1910-
if (ctrl.$viewValue !== value) {
1911-
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value;
1942+
ctrl.$$runValidators(modelValue, viewValue);
1943+
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
1944+
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;
1945+
1946+
if (ctrl.$viewValue !== viewValue) {
1947+
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
19121948
ctrl.$render();
19131949
}
19141950
}
19151951

1916-
return value;
1952+
return modelValue;
19171953
});
19181954
}];
19191955

test/ng/directive/inputSpec.js

+149
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,155 @@ describe('NgModelController', function() {
285285
expect(ctrl.$render).toHaveBeenCalledOnce();
286286
});
287287
});
288+
289+
describe('$validators', function() {
290+
291+
it('should perform validations when $validate() is called', function() {
292+
ctrl.$validators.uppercase = function(value) {
293+
return (/^[A-Z]+$/).test(value);
294+
};
295+
296+
ctrl.$modelValue = 'test';
297+
ctrl.$validate();
298+
299+
expect(ctrl.$valid).toBe(false);
300+
301+
ctrl.$modelValue = 'TEST';
302+
ctrl.$validate();
303+
304+
expect(ctrl.$valid).toBe(true);
305+
});
306+
307+
it('should perform validations when $validate() is called', function() {
308+
ctrl.$validators.uppercase = function(value) {
309+
return (/^[A-Z]+$/).test(value);
310+
};
311+
312+
ctrl.$modelValue = 'test';
313+
ctrl.$validate();
314+
315+
expect(ctrl.$valid).toBe(false);
316+
317+
ctrl.$modelValue = 'TEST';
318+
ctrl.$validate();
319+
320+
expect(ctrl.$valid).toBe(true);
321+
});
322+
323+
it('should always perform validations using the parsed model value', function() {
324+
var captures;
325+
ctrl.$validators.raw = function() {
326+
captures = arguments;
327+
return captures[0];
328+
};
329+
330+
ctrl.$parsers.push(function(value) {
331+
return value.toUpperCase();
332+
});
333+
334+
ctrl.$setViewValue('my-value');
335+
336+
expect(captures).toEqual(['MY-VALUE', 'my-value']);
337+
});
338+
339+
it('should always perform validations using the formatted view value', function() {
340+
var captures;
341+
ctrl.$validators.raw = function() {
342+
captures = arguments;
343+
return captures[0];
344+
};
345+
346+
ctrl.$formatters.push(function(value) {
347+
return value + '...';
348+
});
349+
350+
scope.$apply(function() {
351+
scope.value = 'matias';
352+
});
353+
354+
expect(captures).toEqual(['matias', 'matias...']);
355+
});
356+
357+
it('should only perform validations if the view value is different', function() {
358+
var count = 0;
359+
ctrl.$validators.countMe = function() {
360+
count++;
361+
};
362+
363+
ctrl.$setViewValue('my-value');
364+
expect(count).toBe(1);
365+
366+
ctrl.$setViewValue('my-value');
367+
expect(count).toBe(1);
368+
369+
ctrl.$setViewValue('your-value');
370+
expect(count).toBe(2);
371+
});
372+
373+
it('should perform validations twice each time the model value changes within a digest', function() {
374+
var count = 0;
375+
ctrl.$validators.number = function(value) {
376+
count++;
377+
return (/^\d+$/).test(value);
378+
};
379+
380+
function val(v) {
381+
scope.$apply(function() {
382+
scope.value = v;
383+
});
384+
}
385+
386+
val('');
387+
expect(count).toBe(1);
388+
389+
val(1);
390+
expect(count).toBe(2);
391+
392+
val(1);
393+
expect(count).toBe(2);
394+
395+
val('');
396+
expect(count).toBe(3);
397+
});
398+
399+
it('should only validate to true if all validations are true', function() {
400+
var curry = function(v) {
401+
return function() {
402+
return v;
403+
};
404+
};
405+
406+
ctrl.$validators.a = curry(true);
407+
ctrl.$validators.b = curry(true);
408+
ctrl.$validators.c = curry(false);
409+
410+
ctrl.$validate();
411+
expect(ctrl.$valid).toBe(false);
412+
413+
ctrl.$validators.c = curry(true);
414+
415+
ctrl.$validate();
416+
expect(ctrl.$valid).toBe(true);
417+
});
418+
419+
it('should register invalid validations on the $error object', function() {
420+
var curry = function(v) {
421+
return function() {
422+
return v;
423+
};
424+
};
425+
426+
ctrl.$validators.unique = curry(false);
427+
ctrl.$validators.tooLong = curry(false);
428+
ctrl.$validators.notNumeric = curry(true);
429+
430+
ctrl.$validate();
431+
432+
expect(ctrl.$error.unique).toBe(true);
433+
expect(ctrl.$error.tooLong).toBe(true);
434+
expect(ctrl.$error.notNumeric).not.toBe(true);
435+
});
436+
});
288437
});
289438

290439
describe('ngModel', function() {

0 commit comments

Comments
 (0)