Skip to content

Commit e4e63a3

Browse files
shahataMichael Gallagher
authored and
Michael Gallagher
committed
feat(ngModelOptions): add allowInvalid option
This option allows to write invalid values to the model instead of having them become undefined. Use this together with calling `ctrl.$setValidity` directly for displaying errors from serverside validation. Closes angular#8290 Closes angular#8313
1 parent dd8eb70 commit e4e63a3

File tree

2 files changed

+88
-2
lines changed

2 files changed

+88
-2
lines changed

src/ng/directive/input.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -2037,12 +2037,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20372037
ctrl.$modelValue = ngModelGet();
20382038
}
20392039
var prevModelValue = ctrl.$modelValue;
2040+
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
2041+
if (allowInvalid) {
2042+
ctrl.$modelValue = modelValue;
2043+
writeToModelIfNeeded();
2044+
}
20402045
ctrl.$$runValidators(parserValid, modelValue, viewValue, function() {
2041-
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
2046+
if (!allowInvalid) {
2047+
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
2048+
writeToModelIfNeeded();
2049+
}
2050+
});
2051+
2052+
function writeToModelIfNeeded() {
20422053
if (ctrl.$modelValue !== prevModelValue) {
20432054
ctrl.$$writeModelToScope();
20442055
}
2045-
});
2056+
}
20462057
};
20472058

20482059
this.$$writeModelToScope = function() {
@@ -2765,6 +2776,8 @@ var ngValueDirective = function() {
27652776
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
27662777
* custom value for each event. For example:
27672778
* `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
2779+
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
2780+
* not validate correctly instead of the default behavior of setting the model to undefined.
27682781
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
27692782
`ngModel` as getters/setters.
27702783
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for

test/ng/directive/inputSpec.js

+73
Original file line numberDiff line numberDiff line change
@@ -1769,6 +1769,79 @@ describe('input', function() {
17691769
'ng-model-options="{ getterSetter: true }" />');
17701770
});
17711771

1772+
it('should assign invalid values to the scope if allowInvalid is true', function() {
1773+
compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
1774+
'ng-model-options="{allowInvalid: true}" />');
1775+
changeInputValueTo('12345');
1776+
1777+
expect(scope.value).toBe('12345');
1778+
expect(inputElm).toBeInvalid();
1779+
});
1780+
1781+
it('should not assign not parsable values to the scope if allowInvalid is true', function() {
1782+
compileInput('<input type="number" name="input" ng-model="value" ' +
1783+
'ng-model-options="{allowInvalid: true}" />', {
1784+
valid: false,
1785+
badInput: true
1786+
});
1787+
changeInputValueTo('abcd');
1788+
1789+
expect(scope.value).toBeUndefined();
1790+
expect(inputElm).toBeInvalid();
1791+
});
1792+
1793+
it('should update the scope before async validators execute if allowInvalid is true', inject(function($q) {
1794+
compileInput('<input type="text" name="input" ng-model="value" ' +
1795+
'ng-model-options="{allowInvalid: true}" />');
1796+
var defer;
1797+
scope.form.input.$asyncValidators.promiseValidator = function(value) {
1798+
defer = $q.defer();
1799+
return defer.promise;
1800+
};
1801+
changeInputValueTo('12345');
1802+
1803+
expect(scope.value).toBe('12345');
1804+
expect(scope.form.input.$pending.promiseValidator).toBe(true);
1805+
defer.reject();
1806+
scope.$digest();
1807+
expect(scope.value).toBe('12345');
1808+
expect(inputElm).toBeInvalid();
1809+
}));
1810+
1811+
it('should update the view before async validators execute if allowInvalid is true', inject(function($q) {
1812+
compileInput('<input type="text" name="input" ng-model="value" ' +
1813+
'ng-model-options="{allowInvalid: true}" />');
1814+
var defer;
1815+
scope.form.input.$asyncValidators.promiseValidator = function(value) {
1816+
defer = $q.defer();
1817+
return defer.promise;
1818+
};
1819+
scope.$apply('value = \'12345\'');
1820+
1821+
expect(inputElm.val()).toBe('12345');
1822+
expect(scope.form.input.$pending.promiseValidator).toBe(true);
1823+
defer.reject();
1824+
scope.$digest();
1825+
expect(inputElm.val()).toBe('12345');
1826+
expect(inputElm).toBeInvalid();
1827+
}));
1828+
1829+
it('should not call ng-change listeners twice if the model did not change with allowInvalid', function() {
1830+
compileInput('<input type="text" name="input" ng-model="value" ' +
1831+
'ng-model-options="{allowInvalid: true}" ng-change="changed()" />');
1832+
scope.changed = jasmine.createSpy('changed');
1833+
scope.form.input.$parsers.push(function(value) {
1834+
return 'modelValue';
1835+
});
1836+
1837+
changeInputValueTo('input1');
1838+
expect(scope.value).toBe('modelValue');
1839+
expect(scope.changed).toHaveBeenCalledOnce();
1840+
1841+
changeInputValueTo('input2');
1842+
expect(scope.value).toBe('modelValue');
1843+
expect(scope.changed).toHaveBeenCalledOnce();
1844+
});
17721845
});
17731846

17741847
it('should allow complex reference binding', function() {

0 commit comments

Comments
 (0)