Skip to content

Commit

Permalink
feat(ngModelOptions): add allowInvalid option
Browse files Browse the repository at this point in the history
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
  • Loading branch information
shahata authored and Michael Gallagher committed Sep 10, 2014
1 parent dd8eb70 commit e4e63a3
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 2 deletions.
17 changes: 15 additions & 2 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2037,12 +2037,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$modelValue = ngModelGet();
}
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
if (allowInvalid) {
ctrl.$modelValue = modelValue;
writeToModelIfNeeded();
}
ctrl.$$runValidators(parserValid, modelValue, viewValue, function() {
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
if (!allowInvalid) {
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
writeToModelIfNeeded();
}
});

function writeToModelIfNeeded() {
if (ctrl.$modelValue !== prevModelValue) {
ctrl.$$writeModelToScope();
}
});
}
};

this.$$writeModelToScope = function() {
Expand Down Expand Up @@ -2765,6 +2776,8 @@ var ngValueDirective = function() {
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
* custom value for each event. For example:
* `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
`ngModel` as getters/setters.
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
Expand Down
73 changes: 73 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1769,6 +1769,79 @@ describe('input', function() {
'ng-model-options="{ getterSetter: true }" />');
});

it('should assign invalid values to the scope if allowInvalid is true', function() {
compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
'ng-model-options="{allowInvalid: true}" />');
changeInputValueTo('12345');

expect(scope.value).toBe('12345');
expect(inputElm).toBeInvalid();
});

it('should not assign not parsable values to the scope if allowInvalid is true', function() {
compileInput('<input type="number" name="input" ng-model="value" ' +
'ng-model-options="{allowInvalid: true}" />', {
valid: false,
badInput: true
});
changeInputValueTo('abcd');

expect(scope.value).toBeUndefined();
expect(inputElm).toBeInvalid();
});

it('should update the scope before async validators execute if allowInvalid is true', inject(function($q) {
compileInput('<input type="text" name="input" ng-model="value" ' +
'ng-model-options="{allowInvalid: true}" />');
var defer;
scope.form.input.$asyncValidators.promiseValidator = function(value) {
defer = $q.defer();
return defer.promise;
};
changeInputValueTo('12345');

expect(scope.value).toBe('12345');
expect(scope.form.input.$pending.promiseValidator).toBe(true);
defer.reject();
scope.$digest();
expect(scope.value).toBe('12345');
expect(inputElm).toBeInvalid();
}));

it('should update the view before async validators execute if allowInvalid is true', inject(function($q) {
compileInput('<input type="text" name="input" ng-model="value" ' +
'ng-model-options="{allowInvalid: true}" />');
var defer;
scope.form.input.$asyncValidators.promiseValidator = function(value) {
defer = $q.defer();
return defer.promise;
};
scope.$apply('value = \'12345\'');

expect(inputElm.val()).toBe('12345');
expect(scope.form.input.$pending.promiseValidator).toBe(true);
defer.reject();
scope.$digest();
expect(inputElm.val()).toBe('12345');
expect(inputElm).toBeInvalid();
}));

it('should not call ng-change listeners twice if the model did not change with allowInvalid', function() {
compileInput('<input type="text" name="input" ng-model="value" ' +
'ng-model-options="{allowInvalid: true}" ng-change="changed()" />');
scope.changed = jasmine.createSpy('changed');
scope.form.input.$parsers.push(function(value) {
return 'modelValue';
});

changeInputValueTo('input1');
expect(scope.value).toBe('modelValue');
expect(scope.changed).toHaveBeenCalledOnce();

changeInputValueTo('input2');
expect(scope.value).toBe('modelValue');
expect(scope.changed).toHaveBeenCalledOnce();
});
});

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

0 comments on commit e4e63a3

Please sign in to comment.