feat(ngModelOptions): allow options to be inherited from ancestor ngModelOptions
Previously, you had to apply a complete set of `ngModelOptions` at many places in the DOM where you might want to modify just one or two settings. This change allows more general settings to be applied nearer to the root of the DOM and then for more specific settings to inherit those general settings further down in the DOM. To prevent unwanted inheritance you must opt-in on a case by case basis: * To inherit as single property you simply provide the special value `"$inherit"`. * To inherit all properties not specified locally then include a property `"*": "$inherit"`. Closes #10922 Closes #15389 BREAKING CHANGE: The programmatic API for `ngModelOptions` has changed. You must now read options via the `ngModelController.$options.getOption(name)` method, rather than accessing the option directly as a property of the `ngModelContoller.$options` object. This does not affect the usage in templates and only affects custom directives that might have been reading options for their own purposes. One benefit of these changes, though, is that the `ngModelControler.$options` property is now guaranteed to be defined so there is no need to check before accessing. So, previously: ``` var myOption = ngModelController.$options && ngModelController.$options['my-option']; ``` and now: ``` var myOption = ngModelController.$options.getOption('my-option'); ```
This commit is contained in:
Vendored
+1
@@ -70,6 +70,7 @@ var angularFiles = {
|
||||
'src/ng/directive/ngInit.js',
|
||||
'src/ng/directive/ngList.js',
|
||||
'src/ng/directive/ngModel.js',
|
||||
'src/ng/directive/ngModelOptions.js',
|
||||
'src/ng/directive/ngNonBindable.js',
|
||||
'src/ng/directive/ngOptions.js',
|
||||
'src/ng/directive/ngPluralize.js',
|
||||
|
||||
@@ -1427,7 +1427,7 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
|
||||
badInputChecker(scope, element, attr, ctrl);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
|
||||
var timezone = ctrl && ctrl.$options.getOption('timezone');
|
||||
var previousDate;
|
||||
|
||||
ctrl.$$parserName = type;
|
||||
|
||||
+23
-209
@@ -8,9 +8,11 @@
|
||||
TOUCHED_CLASS: true,
|
||||
PENDING_CLASS: true,
|
||||
addSetValidityMethod: true,
|
||||
setupValidity: true
|
||||
setupValidity: true,
|
||||
$defaultModelOptions: false
|
||||
*/
|
||||
|
||||
|
||||
var VALID_CLASS = 'ng-valid',
|
||||
INVALID_CLASS = 'ng-invalid',
|
||||
PRISTINE_CLASS = 'ng-pristine',
|
||||
@@ -243,6 +245,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
|
||||
this.$pending = undefined; // keep pending keys here
|
||||
this.$name = $interpolate($attr.name || '', false)($scope);
|
||||
this.$$parentForm = nullFormCtrl;
|
||||
this.$options = $defaultModelOptions;
|
||||
|
||||
this.$$parsedNgModel = $parse($attr.ngModel);
|
||||
this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
|
||||
@@ -267,9 +270,8 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
|
||||
}
|
||||
|
||||
NgModelController.prototype = {
|
||||
$$setOptions: function(options) {
|
||||
this.$options = options;
|
||||
if (options && options.getterSetter) {
|
||||
$$initGetterSetters: function() {
|
||||
if (this.$options.getOption('getterSetter')) {
|
||||
var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'),
|
||||
invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)');
|
||||
|
||||
@@ -543,7 +545,7 @@ NgModelController.prototype = {
|
||||
var prevValid = this.$valid;
|
||||
var prevModelValue = this.$modelValue;
|
||||
|
||||
var allowInvalid = this.$options && this.$options.allowInvalid;
|
||||
var allowInvalid = this.$options.getOption('allowInvalid');
|
||||
|
||||
var that = this;
|
||||
this.$$runValidators(modelValue, viewValue, function(allValid) {
|
||||
@@ -708,7 +710,7 @@ NgModelController.prototype = {
|
||||
this.$modelValue = this.$$ngModelGet(this.$$scope);
|
||||
}
|
||||
var prevModelValue = this.$modelValue;
|
||||
var allowInvalid = this.$options && this.$options.allowInvalid;
|
||||
var allowInvalid = this.$options.getOption('allowInvalid');
|
||||
this.$$rawModelValue = modelValue;
|
||||
|
||||
if (allowInvalid) {
|
||||
@@ -800,25 +802,18 @@ NgModelController.prototype = {
|
||||
*/
|
||||
$setViewValue: function(value, trigger) {
|
||||
this.$viewValue = value;
|
||||
if (!this.$options || this.$options.updateOnDefault) {
|
||||
if (this.$options.getOption('updateOnDefault')) {
|
||||
this.$$debounceViewValueCommit(trigger);
|
||||
}
|
||||
},
|
||||
|
||||
$$debounceViewValueCommit: function(trigger) {
|
||||
var debounceDelay = 0,
|
||||
options = this.$options,
|
||||
debounce;
|
||||
var debounceDelay = this.$options.getOption('debounce');
|
||||
|
||||
if (options && isDefined(options.debounce)) {
|
||||
debounce = options.debounce;
|
||||
if (isNumber(debounce)) {
|
||||
debounceDelay = debounce;
|
||||
} else if (isNumber(debounce[trigger])) {
|
||||
debounceDelay = debounce[trigger];
|
||||
} else if (isNumber(debounce['default'])) {
|
||||
debounceDelay = debounce['default'];
|
||||
}
|
||||
if (isNumber(debounceDelay[trigger])) {
|
||||
debounceDelay = debounceDelay[trigger];
|
||||
} else if (isNumber(debounceDelay['default'])) {
|
||||
debounceDelay = debounceDelay['default'];
|
||||
}
|
||||
|
||||
this.$$timeout.cancel(this.$$pendingDebounce);
|
||||
@@ -1116,9 +1111,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
|
||||
return {
|
||||
pre: function ngModelPreLink(scope, element, attr, ctrls) {
|
||||
var modelCtrl = ctrls[0],
|
||||
formCtrl = ctrls[1] || modelCtrl.$$parentForm;
|
||||
formCtrl = ctrls[1] || modelCtrl.$$parentForm,
|
||||
optionsCtrl = ctrls[2];
|
||||
|
||||
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
|
||||
if (optionsCtrl) {
|
||||
modelCtrl.$options = optionsCtrl.$options;
|
||||
}
|
||||
|
||||
modelCtrl.$$initGetterSetters();
|
||||
|
||||
// notify others, especially parent forms
|
||||
formCtrl.$addControl(modelCtrl);
|
||||
@@ -1135,8 +1135,8 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
|
||||
},
|
||||
post: function ngModelPostLink(scope, element, attr, ctrls) {
|
||||
var modelCtrl = ctrls[0];
|
||||
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
|
||||
element.on(modelCtrl.$options.updateOn, function(ev) {
|
||||
if (modelCtrl.$options.getOption('updateOn')) {
|
||||
element.on(modelCtrl.$options.getOption('updateOn'), function(ev) {
|
||||
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
|
||||
});
|
||||
}
|
||||
@@ -1159,189 +1159,3 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
|
||||
|
||||
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngModelOptions
|
||||
*
|
||||
* @description
|
||||
* Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
|
||||
* events that will trigger a model update and/or a debouncing delay so that the actual update only
|
||||
* takes place when a timer expires; this timer will be reset after another change takes place.
|
||||
*
|
||||
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
|
||||
* be different from the value in the actual model. This means that if you update the model you
|
||||
* should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
|
||||
* order to make sure it is synchronized with the model and that any debounced action is canceled.
|
||||
*
|
||||
* The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
|
||||
* method is by making sure the input is placed inside a form that has a `name` attribute. This is
|
||||
* important because `form` controllers are published to the related scope under the name in their
|
||||
* `name` attribute.
|
||||
*
|
||||
* Any pending changes will take place immediately when an enclosing form is submitted via the
|
||||
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
|
||||
* to have access to the updated model.
|
||||
*
|
||||
* `ngModelOptions` has an effect on the element it's declared on and its descendants.
|
||||
*
|
||||
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
|
||||
* - `updateOn`: string specifying which event should the input be bound to. You can set several
|
||||
* events using an space delimited list. There is a special event called `default` that
|
||||
* matches the default events belonging to the control.
|
||||
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
|
||||
* 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
|
||||
* `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
|
||||
* continental US time zone abbreviations, but for general use, use a time zone offset, for
|
||||
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
|
||||
* If not specified, the timezone of the browser will be used.
|
||||
*
|
||||
* @example
|
||||
|
||||
The following example shows how to override immediate updates. Changes on the inputs within the
|
||||
form will update the model only when the control loses focus (blur event). If `escape` key is
|
||||
pressed while the input field is focused, the value is reset to the value in the current model.
|
||||
|
||||
<example name="ngModelOptions-directive-blur" module="optionsExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<form name="userForm">
|
||||
<label>Name:
|
||||
<input type="text" name="userName"
|
||||
ng-model="user.name"
|
||||
ng-model-options="{ updateOn: 'blur' }"
|
||||
ng-keyup="cancel($event)" />
|
||||
</label><br />
|
||||
<label>Other data:
|
||||
<input type="text" ng-model="user.data" />
|
||||
</label><br />
|
||||
</form>
|
||||
<pre>user.name = <span ng-bind="user.name"></span></pre>
|
||||
<pre>user.data = <span ng-bind="user.data"></span></pre>
|
||||
</div>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('optionsExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.user = { name: 'John', data: '' };
|
||||
|
||||
$scope.cancel = function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
$scope.userForm.userName.$rollbackViewValue();
|
||||
}
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
var model = element(by.binding('user.name'));
|
||||
var input = element(by.model('user.name'));
|
||||
var other = element(by.model('user.data'));
|
||||
|
||||
it('should allow custom events', function() {
|
||||
input.sendKeys(' Doe');
|
||||
input.click();
|
||||
expect(model.getText()).toEqual('John');
|
||||
other.click();
|
||||
expect(model.getText()).toEqual('John Doe');
|
||||
});
|
||||
|
||||
it('should $rollbackViewValue when model changes', function() {
|
||||
input.sendKeys(' Doe');
|
||||
expect(input.getAttribute('value')).toEqual('John Doe');
|
||||
input.sendKeys(protractor.Key.ESCAPE);
|
||||
expect(input.getAttribute('value')).toEqual('John');
|
||||
other.click();
|
||||
expect(model.getText()).toEqual('John');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
|
||||
If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
|
||||
|
||||
<example name="ngModelOptions-directive-debounce" module="optionsExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<form name="userForm">
|
||||
<label>Name:
|
||||
<input type="text" name="userName"
|
||||
ng-model="user.name"
|
||||
ng-model-options="{ debounce: 1000 }" />
|
||||
</label>
|
||||
<button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
|
||||
<br />
|
||||
</form>
|
||||
<pre>user.name = <span ng-bind="user.name"></span></pre>
|
||||
</div>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('optionsExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.user = { name: 'Igor' };
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
|
||||
This one shows how to bind to getter/setters:
|
||||
|
||||
<example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<form name="userForm">
|
||||
<label>Name:
|
||||
<input type="text" name="userName"
|
||||
ng-model="user.name"
|
||||
ng-model-options="{ getterSetter: true }" />
|
||||
</label>
|
||||
</form>
|
||||
<pre>user.name = <span ng-bind="user.name()"></span></pre>
|
||||
</div>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('getterSetterExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
var _name = 'Brian';
|
||||
$scope.user = {
|
||||
name: function(newName) {
|
||||
// Note that newName can be undefined for two reasons:
|
||||
// 1. Because it is called as a getter and thus called with no arguments
|
||||
// 2. Because the property should actually be set to undefined. This happens e.g. if the
|
||||
// input is invalid
|
||||
return arguments.length ? (_name = newName) : _name;
|
||||
}
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
var ngModelOptionsDirective = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
controller: ['$scope', '$attrs', function NgModelOptionsController($scope, $attrs) {
|
||||
var that = this;
|
||||
this.$options = copy($scope.$eval($attrs.ngModelOptions));
|
||||
// Allow adding/overriding bound events
|
||||
if (isDefined(this.$options.updateOn)) {
|
||||
this.$options.updateOnDefault = false;
|
||||
// extract "default" pseudo-event from list of events that can trigger a model update
|
||||
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
|
||||
that.$options.updateOnDefault = true;
|
||||
return ' ';
|
||||
}));
|
||||
} else {
|
||||
this.$options.updateOnDefault = true;
|
||||
}
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
'use strict';
|
||||
|
||||
/* exported $defaultModelOptions */
|
||||
var $defaultModelOptions;
|
||||
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
|
||||
|
||||
/**
|
||||
* @ngdoc type
|
||||
* @name ModelOptions
|
||||
* @description
|
||||
* A container for the options set by the {@link ngModelOptions} directive
|
||||
*/
|
||||
function ModelOptions(options) {
|
||||
this.$$options = options;
|
||||
}
|
||||
|
||||
ModelOptions.prototype = {
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name ModelOptions#getOption
|
||||
* @param {string} name the name of the option to retrieve
|
||||
* @returns {*} the value of the option
|
||||
* @description
|
||||
* Returns the value of the given option
|
||||
*/
|
||||
getOption: function(name) {
|
||||
return this.$$options[name];
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name ModelOptions#createChild
|
||||
* @param {Object} options a hash of options for the new child that will override the parent's options
|
||||
* @return {ModelOptions} a new `ModelOptions` object initialized with the given options.
|
||||
*/
|
||||
createChild: function(options) {
|
||||
var inheritAll = false;
|
||||
|
||||
// make a shallow copy
|
||||
options = extend({}, options);
|
||||
|
||||
// Inherit options from the parent if specified by the value `"$inherit"`
|
||||
forEach(options, /* @this */ function(option, key) {
|
||||
if (option === '$inherit') {
|
||||
if (key === '*') {
|
||||
inheritAll = true;
|
||||
} else {
|
||||
options[key] = this.$$options[key];
|
||||
// `updateOn` is special so we must also inherit the `updateOnDefault` option
|
||||
if (key === 'updateOn') {
|
||||
options.updateOnDefault = this.$$options.updateOnDefault;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (key === 'updateOn') {
|
||||
// If the `updateOn` property contains the `default` event then we have to remove
|
||||
// it from the event list and set the `updateOnDefault` flag.
|
||||
options.updateOnDefault = false;
|
||||
options[key] = trim(option.replace(DEFAULT_REGEXP, function() {
|
||||
options.updateOnDefault = true;
|
||||
return ' ';
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (inheritAll) {
|
||||
// We have a property of the form: `"*": "$inherit"`
|
||||
delete options['*'];
|
||||
defaults(options, this.$$options);
|
||||
}
|
||||
|
||||
// Finally add in any missing defaults
|
||||
defaults(options, $defaultModelOptions.$$options);
|
||||
|
||||
return new ModelOptions(options);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$defaultModelOptions = new ModelOptions({
|
||||
updateOn: '',
|
||||
updateOnDefault: true,
|
||||
debounce: 0,
|
||||
getterSetter: false,
|
||||
allowInvalid: false,
|
||||
timezone: null
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngModelOptions
|
||||
*
|
||||
* @description
|
||||
* This directive allows you to modify the behaviour of {@link ngModel} directives within your
|
||||
* application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel}
|
||||
* directives will use the options of their nearest `ngModelOptions` ancestor.
|
||||
*
|
||||
* The `ngModelOptions` settings are found by evaluating the value of the attribute directive as
|
||||
* an Angular expression. This expression should evaluate to an object, whose properties contain
|
||||
* the settings. For example: `<div "ng-model-options"="{ debounce: 100 }"`.
|
||||
*
|
||||
* ## Inheriting Options
|
||||
*
|
||||
* You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions`
|
||||
* directive by giving it the value of `"$inherit"`.
|
||||
* Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the
|
||||
* DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings
|
||||
* will be used.
|
||||
*
|
||||
* For example given the following fragment of HTML
|
||||
*
|
||||
*
|
||||
* ```html
|
||||
* <div ng-model-options="{ allowInvalid: true, debounce: 200 }">
|
||||
* <form ng-model-options="{ updateOn: 'blur', allowInvalid: '$inherit' }">
|
||||
* <input ng-model-options="{ updateOn: 'default', allowInvalid: '$inherit' }" />
|
||||
* </form>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* the `input` element will have the following settings
|
||||
*
|
||||
* ```js
|
||||
* { allowInvalid: true, updateOn: 'default', debounce: 0 }
|
||||
* ```
|
||||
*
|
||||
* Notice that the `debounce` setting was not inherited and used the default value instead.
|
||||
*
|
||||
* You can specify that all undefined settings are automatically inherited from an ancestor by
|
||||
* including a property with key of `"*"` and value of `"$inherit"`.
|
||||
*
|
||||
* For example given the following fragment of HTML
|
||||
*
|
||||
*
|
||||
* ```html
|
||||
* <div ng-model-options="{ allowInvalid: true, debounce: 200 }">
|
||||
* <form ng-model-options="{ updateOn: 'blur', "*": '$inherit' }">
|
||||
* <input ng-model-options="{ updateOn: 'default', "*": '$inherit' }" />
|
||||
* </form>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* the `input` element will have the following settings
|
||||
*
|
||||
* ```js
|
||||
* { allowInvalid: true, updateOn: 'default', debounce: 200 }
|
||||
* ```
|
||||
*
|
||||
* Notice that the `debounce` setting now inherits the value from the outer `<div>` element.
|
||||
*
|
||||
* If you are creating a reusable component then you should be careful when using `"*": "$inherit"`
|
||||
* since you may inadvertently inherit a setting in the future that changes the behavior of your component.
|
||||
*
|
||||
*
|
||||
* ## Triggering and debouncing model updates
|
||||
*
|
||||
* The `updateOn` and `debounce` properties allow you to specify a custom list of events that will
|
||||
* trigger a model update and/or a debouncing delay so that the actual update only takes place when
|
||||
* a timer expires; this timer will be reset after another change takes place.
|
||||
*
|
||||
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
|
||||
* be different from the value in the actual model. This means that if you update the model you
|
||||
* should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in
|
||||
* order to make sure it is synchronized with the model and that any debounced action is canceled.
|
||||
*
|
||||
* The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue}
|
||||
* method is by making sure the input is placed inside a form that has a `name` attribute. This is
|
||||
* important because `form` controllers are published to the related scope under the name in their
|
||||
* `name` attribute.
|
||||
*
|
||||
* Any pending changes will take place immediately when an enclosing form is submitted via the
|
||||
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
|
||||
* to have access to the updated model.
|
||||
*
|
||||
* The following example shows how to override immediate updates. Changes on the inputs within the
|
||||
* form will update the model only when the control loses focus (blur event). If `escape` key is
|
||||
* pressed while the input field is focused, the value is reset to the value in the current model.
|
||||
*
|
||||
* <example name="ngModelOptions-directive-blur" module="optionsExample">
|
||||
* <file name="index.html">
|
||||
* <div ng-controller="ExampleController">
|
||||
* <form name="userForm">
|
||||
* <label>
|
||||
* Name:
|
||||
* <input type="text" name="userName"
|
||||
* ng-model="user.name"
|
||||
* ng-model-options="{ updateOn: 'blur' }"
|
||||
* ng-keyup="cancel($event)" />
|
||||
* </label><br />
|
||||
* <label>
|
||||
* Other data:
|
||||
* <input type="text" ng-model="user.data" />
|
||||
* </label><br />
|
||||
* </form>
|
||||
* <pre>user.name = <span ng-bind="user.name"></span></pre>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="app.js">
|
||||
* angular.module('optionsExample', [])
|
||||
* .controller('ExampleController', ['$scope', function($scope) {
|
||||
* $scope.user = { name: 'say', data: '' };
|
||||
*
|
||||
* $scope.cancel = function(e) {
|
||||
* if (e.keyCode === 27) {
|
||||
* $scope.userForm.userName.$rollbackViewValue();
|
||||
* }
|
||||
* };
|
||||
* }]);
|
||||
* </file>
|
||||
* <file name="protractor.js" type="protractor">
|
||||
* var model = element(by.binding('user.name'));
|
||||
* var input = element(by.model('user.name'));
|
||||
* var other = element(by.model('user.data'));
|
||||
*
|
||||
* it('should allow custom events', function() {
|
||||
* input.sendKeys(' hello');
|
||||
* input.click();
|
||||
* expect(model.getText()).toEqual('say');
|
||||
* other.click();
|
||||
* expect(model.getText()).toEqual('say hello');
|
||||
* });
|
||||
*
|
||||
* it('should $rollbackViewValue when model changes', function() {
|
||||
* input.sendKeys(' hello');
|
||||
* expect(input.getAttribute('value')).toEqual('say hello');
|
||||
* input.sendKeys(protractor.Key.ESCAPE);
|
||||
* expect(input.getAttribute('value')).toEqual('say');
|
||||
* other.click();
|
||||
* expect(model.getText()).toEqual('say');
|
||||
* });
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
* The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
|
||||
* If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
|
||||
*
|
||||
* <example name="ngModelOptions-directive-debounce" module="optionsExample">
|
||||
* <file name="index.html">
|
||||
* <div ng-controller="ExampleController">
|
||||
* <form name="userForm">
|
||||
* Name:
|
||||
* <input type="text" name="userName"
|
||||
* ng-model="user.name"
|
||||
* ng-model-options="{ debounce: 1000 }" />
|
||||
* <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
|
||||
* </form>
|
||||
* <pre>user.name = <span ng-bind="user.name"></span></pre>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="app.js">
|
||||
* angular.module('optionsExample', [])
|
||||
* .controller('ExampleController', ['$scope', function($scope) {
|
||||
* $scope.user = { name: 'say' };
|
||||
* }]);
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
* ## Model updates and validation
|
||||
*
|
||||
* The default behaviour in `ngModel` is that the model value is set to `undefined` when the
|
||||
* validation determines that the value is invalid. By setting the `allowInvalid` property to true,
|
||||
* the model will still be updated even if the value is invalid.
|
||||
*
|
||||
*
|
||||
* ## Connecting to the scope
|
||||
*
|
||||
* By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression
|
||||
* on the scope refers to a "getter/setter" function rather than the value itself.
|
||||
*
|
||||
* The following example shows how to bind to getter/setters:
|
||||
*
|
||||
* <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
|
||||
* <file name="index.html">
|
||||
* <div ng-controller="ExampleController">
|
||||
* <form name="userForm">
|
||||
* <label>
|
||||
* Name:
|
||||
* <input type="text" name="userName"
|
||||
* ng-model="user.name"
|
||||
* ng-model-options="{ getterSetter: true }" />
|
||||
* </label>
|
||||
* </form>
|
||||
* <pre>user.name = <span ng-bind="user.name()"></span></pre>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="app.js">
|
||||
* angular.module('getterSetterExample', [])
|
||||
* .controller('ExampleController', ['$scope', function($scope) {
|
||||
* var _name = 'Brian';
|
||||
* $scope.user = {
|
||||
* name: function(newName) {
|
||||
* return angular.isDefined(newName) ? (_name = newName) : _name;
|
||||
* }
|
||||
* };
|
||||
* }]);
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
*
|
||||
* ## Specifying timezones
|
||||
*
|
||||
* You can specify the timezone that date/time input directives expect by providing its name in the
|
||||
* `timezone` property.
|
||||
*
|
||||
* @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
|
||||
* and its descendents. Valid keys are:
|
||||
* - `updateOn`: string specifying which event should the input be bound to. You can set several
|
||||
* events using an space delimited list. There is a special event called `default` that
|
||||
* matches the default events belonging to the control.
|
||||
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
|
||||
* 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
|
||||
* `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
|
||||
* continental US time zone abbreviations, but for general use, use a time zone offset, for
|
||||
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
|
||||
* If not specified, the timezone of the browser will be used.
|
||||
*
|
||||
*/
|
||||
var ngModelOptionsDirective = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
// ngModelOptions needs to run before ngModel and input directives
|
||||
priority: 10,
|
||||
require: ['ngModelOptions', '?^^ngModelOptions'],
|
||||
controller: function NgModelOptionsController() {},
|
||||
link: {
|
||||
pre: function ngModelOptionsPreLinkFn(scope, element, attrs, ctrls) {
|
||||
var optionsCtrl = ctrls[0];
|
||||
var parentOptions = ctrls[1] ? ctrls[1].$options : $defaultModelOptions;
|
||||
optionsCtrl.$options = parentOptions.createChild(scope.$eval(attrs.ngModelOptions));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// shallow copy over values from `src` that are not already specified on `dst`
|
||||
function defaults(dst, src) {
|
||||
forEach(src, function(value, key) {
|
||||
if (!isDefined(dst[key])) {
|
||||
dst[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
'use strict';
|
||||
|
||||
/* globals
|
||||
generateInputCompilerHelper: false,
|
||||
$defaultModelOptions: false
|
||||
*/
|
||||
describe('ngModelOptions', function() {
|
||||
|
||||
describe('$defaultModelOptions', function() {
|
||||
it('should provide default values', function() {
|
||||
expect($defaultModelOptions.getOption('updateOn')).toEqual('');
|
||||
expect($defaultModelOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
expect($defaultModelOptions.getOption('debounce')).toBe(0);
|
||||
expect($defaultModelOptions.getOption('getterSetter')).toBe(false);
|
||||
expect($defaultModelOptions.getOption('allowInvalid')).toBe(false);
|
||||
expect($defaultModelOptions.getOption('timezone')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('directive', function() {
|
||||
|
||||
var helper = {}, $rootScope, $compile, $timeout, $q;
|
||||
|
||||
generateInputCompilerHelper(helper);
|
||||
|
||||
beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_, _$q_) {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
$timeout = _$timeout_;
|
||||
$q = _$q_;
|
||||
}));
|
||||
|
||||
|
||||
describe('should fall back to `$defaultModelOptions`', function() {
|
||||
it('if there is no `ngModelOptions` directive', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" />');
|
||||
|
||||
var inputOptions = $rootScope.form.alias.$options;
|
||||
expect(inputOptions.getOption('updateOn')).toEqual($defaultModelOptions.getOption('updateOn'));
|
||||
expect(inputOptions.getOption('updateOnDefault')).toEqual($defaultModelOptions.getOption('updateOnDefault'));
|
||||
expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
|
||||
expect(inputOptions.getOption('getterSetter')).toEqual($defaultModelOptions.getOption('getterSetter'));
|
||||
expect(inputOptions.getOption('allowInvalid')).toEqual($defaultModelOptions.getOption('allowInvalid'));
|
||||
expect(inputOptions.getOption('timezone')).toEqual($defaultModelOptions.getOption('timezone'));
|
||||
});
|
||||
|
||||
|
||||
it('if `ngModelOptions` on the same element does not specify the option', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ng-model-options="{ updateOn: \'blur\' }"/>');
|
||||
|
||||
var inputOptions = $rootScope.form.alias.$options;
|
||||
expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
|
||||
expect(inputOptions.getOption('updateOnDefault')).toBe(false);
|
||||
expect(inputOptions.getOption('updateOnDefault')).not.toEqual($defaultModelOptions.getOption('updateOnDefault'));
|
||||
});
|
||||
|
||||
|
||||
it('if the first `ngModelOptions` ancestor does not specify the option', function() {
|
||||
var form = $compile('<form name="form" ng-model-options="{ updateOn: \'blur\' }">' +
|
||||
'<input name="alias" ng-model="x">' +
|
||||
'</form>')($rootScope);
|
||||
var inputOptions = $rootScope.form.alias.$options;
|
||||
|
||||
expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
|
||||
expect(inputOptions.getOption('updateOnDefault')).toBe(false);
|
||||
expect(inputOptions.getOption('updateOnDefault')).not.toEqual($defaultModelOptions.getOption('updateOnDefault'));
|
||||
dealoc(form);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('sharing and inheritance', function() {
|
||||
|
||||
it('should not inherit options from ancestor `ngModelOptions` directives by default', function() {
|
||||
var container = $compile(
|
||||
'<div ng-model-options="{ allowInvalid: true }">' +
|
||||
'<form ng-model-options="{ updateOn: \'blur\' }">' +
|
||||
'<input ng-model-options="{ updateOn: \'default\' }">' +
|
||||
'</form>' +
|
||||
'</div>')($rootScope);
|
||||
|
||||
var form = container.find('form');
|
||||
var input = container.find('input');
|
||||
|
||||
var containerOptions = container.controller('ngModelOptions').$options;
|
||||
var formOptions = form.controller('ngModelOptions').$options;
|
||||
var inputOptions = input.controller('ngModelOptions').$options;
|
||||
|
||||
expect(containerOptions.getOption('allowInvalid')).toEqual(true);
|
||||
expect(formOptions.getOption('allowInvalid')).toEqual(false);
|
||||
expect(inputOptions.getOption('allowInvalid')).toEqual(false);
|
||||
|
||||
expect(containerOptions.getOption('updateOn')).toEqual('');
|
||||
expect(containerOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
expect(formOptions.getOption('updateOn')).toEqual('blur');
|
||||
expect(formOptions.getOption('updateOnDefault')).toEqual(false);
|
||||
expect(inputOptions.getOption('updateOn')).toEqual('');
|
||||
expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
|
||||
dealoc(container);
|
||||
});
|
||||
|
||||
it('should inherit options that are marked with "$inherit" from the nearest ancestor `ngModelOptions` directive', function() {
|
||||
var container = $compile(
|
||||
'<div ng-model-options="{ allowInvalid: true }">' +
|
||||
'<form ng-model-options="{ updateOn: \'blur\', allowInvalid: \'$inherit\' }">' +
|
||||
'<input ng-model-options="{ updateOn: \'default\' }">' +
|
||||
'</form>' +
|
||||
'</div>')($rootScope);
|
||||
|
||||
var form = container.find('form');
|
||||
var input = container.find('input');
|
||||
|
||||
var containerOptions = container.controller('ngModelOptions').$options;
|
||||
var formOptions = form.controller('ngModelOptions').$options;
|
||||
var inputOptions = input.controller('ngModelOptions').$options;
|
||||
|
||||
expect(containerOptions.getOption('allowInvalid')).toEqual(true);
|
||||
expect(formOptions.getOption('allowInvalid')).toEqual(true);
|
||||
expect(inputOptions.getOption('allowInvalid')).toEqual(false);
|
||||
|
||||
expect(containerOptions.getOption('updateOn')).toEqual('');
|
||||
expect(containerOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
expect(formOptions.getOption('updateOn')).toEqual('blur');
|
||||
expect(formOptions.getOption('updateOnDefault')).toEqual(false);
|
||||
expect(inputOptions.getOption('updateOn')).toEqual('');
|
||||
expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
|
||||
dealoc(container);
|
||||
});
|
||||
|
||||
it('should inherit all unspecified options if the options object contains a `"*"` property with value "$inherit"', function() {
|
||||
var container = $compile(
|
||||
'<div ng-model-options="{ allowInvalid: true, debounce: 100, updateOn: \'keyup\' }">' +
|
||||
'<form ng-model-options="{ updateOn: \'blur\', \'*\': \'$inherit\' }">' +
|
||||
'<input ng-model-options="{ updateOn: \'default\' }">' +
|
||||
'</form>' +
|
||||
'</div>')($rootScope);
|
||||
|
||||
var form = container.find('form');
|
||||
var input = container.find('input');
|
||||
|
||||
var containerOptions = container.controller('ngModelOptions').$options;
|
||||
var formOptions = form.controller('ngModelOptions').$options;
|
||||
var inputOptions = input.controller('ngModelOptions').$options;
|
||||
|
||||
expect(containerOptions.getOption('allowInvalid')).toEqual(true);
|
||||
expect(formOptions.getOption('allowInvalid')).toEqual(true);
|
||||
expect(inputOptions.getOption('allowInvalid')).toEqual(false);
|
||||
|
||||
expect(containerOptions.getOption('debounce')).toEqual(100);
|
||||
expect(formOptions.getOption('debounce')).toEqual(100);
|
||||
expect(inputOptions.getOption('debounce')).toEqual(0);
|
||||
|
||||
expect(containerOptions.getOption('updateOn')).toEqual('keyup');
|
||||
expect(containerOptions.getOption('updateOnDefault')).toEqual(false);
|
||||
expect(formOptions.getOption('updateOn')).toEqual('blur');
|
||||
expect(formOptions.getOption('updateOnDefault')).toEqual(false);
|
||||
expect(inputOptions.getOption('updateOn')).toEqual('');
|
||||
expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
|
||||
dealoc(container);
|
||||
});
|
||||
|
||||
it('should correctly inherit default and another specified event for `updateOn`', function() {
|
||||
var container = $compile(
|
||||
'<div ng-model-options="{updateOn: \'default blur\'}">' +
|
||||
'<input ng-model-options="{\'*\': \'$inherit\'}">' +
|
||||
'</div>')($rootScope);
|
||||
|
||||
var input = container.find('input');
|
||||
var inputOptions = input.controller('ngModelOptions').$options;
|
||||
|
||||
expect(inputOptions.getOption('updateOn')).toEqual('blur');
|
||||
expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
|
||||
|
||||
dealoc(container);
|
||||
});
|
||||
|
||||
|
||||
it('should `updateOnDefault` as well if we have `updateOn: "$inherit"`', function() {
|
||||
var container = $compile(
|
||||
'<div ng-model-options="{updateOn: \'keyup\'}">' +
|
||||
'<input ng-model-options="{updateOn: \'$inherit\'}">' +
|
||||
'<div ng-model-options="{updateOn: \'default blur\'}">' +
|
||||
'<input ng-model-options="{updateOn: \'$inherit\'}">' +
|
||||
'</div>' +
|
||||
'</div>')($rootScope);
|
||||
|
||||
var input1 = container.find('input').eq(0);
|
||||
var inputOptions1 = input1.controller('ngModelOptions').$options;
|
||||
|
||||
expect(inputOptions1.getOption('updateOn')).toEqual('keyup');
|
||||
expect(inputOptions1.getOption('updateOnDefault')).toEqual(false);
|
||||
|
||||
var input2 = container.find('input').eq(1);
|
||||
var inputOptions2 = input2.controller('ngModelOptions').$options;
|
||||
|
||||
expect(inputOptions2.getOption('updateOn')).toEqual('blur');
|
||||
expect(inputOptions2.getOption('updateOnDefault')).toEqual(true);
|
||||
|
||||
dealoc(container);
|
||||
});
|
||||
|
||||
|
||||
it('should make a copy of the options object', function() {
|
||||
$rootScope.options = {updateOn: 'default'};
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="options"' +
|
||||
'/>');
|
||||
expect($rootScope.options).toEqual({updateOn: 'default'});
|
||||
expect($rootScope.form.alias.$options).not.toBe($rootScope.options);
|
||||
});
|
||||
|
||||
it('should be retrieved from an ancestor element containing an `ngModelOptions` directive', function() {
|
||||
var doc = $compile(
|
||||
'<form name="test" ' +
|
||||
'ng-model-options="{ debounce: 10000, updateOn: \'blur\' }" >' +
|
||||
'<input type="text" ng-model="name" name="alias" />' +
|
||||
'</form>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var inputElm = doc.find('input');
|
||||
helper.changeGivenInputTo(inputElm, 'a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
$timeout.flush(9000);
|
||||
expect($rootScope.name).toEqual('a');
|
||||
dealoc(doc);
|
||||
});
|
||||
|
||||
it('should allow sharing options between multiple inputs', function() {
|
||||
$rootScope.options = {updateOn: 'default'};
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name1" name="alias1" ' +
|
||||
'ng-model-options="options"' +
|
||||
'/>' +
|
||||
'<input type="text" ng-model="name2" name="alias2" ' +
|
||||
'ng-model-options="options"' +
|
||||
'/>');
|
||||
|
||||
helper.changeGivenInputTo(inputElm.eq(0), 'a');
|
||||
helper.changeGivenInputTo(inputElm.eq(1), 'b');
|
||||
expect($rootScope.name1).toEqual('a');
|
||||
expect($rootScope.name2).toEqual('b');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('updateOn', function() {
|
||||
it('should allow overriding the model update trigger event on text inputs', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should not dirty the input if nothing was changed before updateOn trigger', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.form.alias.$pristine).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on text areas', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<textarea ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should bind the element to a list of events', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur mousemove\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
|
||||
helper.changeInputValueTo('b');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
browserTrigger(inputElm, 'mousemove');
|
||||
expect($rootScope.name).toEqual('b');
|
||||
});
|
||||
|
||||
|
||||
it('should allow keeping the default update behavior on text inputs', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'default\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on checkboxes', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should allow keeping the default update behavior on checkboxes', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on radio buttons', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="radio" ng-model="color" value="white" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\'}"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="red" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\'}"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="blue" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\'}"' +
|
||||
'/>');
|
||||
|
||||
$rootScope.$apply('color = \'white\'');
|
||||
browserTrigger(inputElm[2], 'click');
|
||||
expect($rootScope.color).toBe('white');
|
||||
|
||||
browserTrigger(inputElm[2], 'blur');
|
||||
expect($rootScope.color).toBe('blue');
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should allow keeping the default update behavior on radio buttons', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="radio" ng-model="color" value="white" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="red" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="blue" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>');
|
||||
|
||||
$rootScope.$apply('color = \'white\'');
|
||||
browserTrigger(inputElm[2], 'click');
|
||||
expect($rootScope.color).toBe('blue');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('debounce', function() {
|
||||
it('should trigger only after timeout in text inputs', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 10000 }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
helper.changeInputValueTo('b');
|
||||
helper.changeInputValueTo('c');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(9000);
|
||||
expect($rootScope.name).toEqual('c');
|
||||
});
|
||||
|
||||
|
||||
it('should trigger only after timeout in checkboxes', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ debounce: 10000 }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(9000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should trigger only after timeout in radio buttons', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="radio" ng-model="color" value="white" />' +
|
||||
'<input type="radio" ng-model="color" value="red" ' +
|
||||
'ng-model-options="{ debounce: 20000 }"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="blue" ' +
|
||||
'ng-model-options="{ debounce: 30000 }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm[0], 'click');
|
||||
expect($rootScope.color).toBe('white');
|
||||
browserTrigger(inputElm[1], 'click');
|
||||
expect($rootScope.color).toBe('white');
|
||||
$timeout.flush(12000);
|
||||
expect($rootScope.color).toBe('white');
|
||||
$timeout.flush(10000);
|
||||
expect($rootScope.color).toBe('red');
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should not trigger digest while debouncing', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 10000 }"' +
|
||||
'/>');
|
||||
|
||||
var watchSpy = jasmine.createSpy('watchSpy');
|
||||
$rootScope.$watch(watchSpy);
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(watchSpy).not.toHaveBeenCalled();
|
||||
|
||||
$timeout.flush(10000);
|
||||
expect(watchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should allow selecting different debounce timeouts for each event',
|
||||
function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{' +
|
||||
'updateOn: \'default blur\', ' +
|
||||
'debounce: { default: 10000, blur: 5000 }' +
|
||||
'}"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(6000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(4000);
|
||||
expect($rootScope.name).toEqual('a');
|
||||
helper.changeInputValueTo('b');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
$timeout.flush(4000);
|
||||
expect($rootScope.name).toEqual('a');
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.name).toEqual('b');
|
||||
});
|
||||
|
||||
|
||||
it('should allow selecting different debounce timeouts for each event on checkboxes', function() {
|
||||
var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ ' +
|
||||
'updateOn: \'default blur\', debounce: { default: 10000, blur: 5000 } }"' +
|
||||
'/>');
|
||||
|
||||
inputElm[0].checked = false;
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(8000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
inputElm[0].checked = true;
|
||||
browserTrigger(inputElm, 'click');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should allow selecting 0 for non-default debounce timeouts for each event on checkboxes', function() {
|
||||
var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ ' +
|
||||
'updateOn: \'default blur\', debounce: { default: 10000, blur: 0 } }"' +
|
||||
'/>');
|
||||
|
||||
inputElm[0].checked = false;
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(8000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
inputElm[0].checked = true;
|
||||
browserTrigger(inputElm, 'click');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
$timeout.flush(0);
|
||||
expect($rootScope.checkbox).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should flush debounced events when calling $commitViewValue directly', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 1000 }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$rootScope.form.alias.$commitViewValue();
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
it('should cancel debounced events when calling $commitViewValue', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 1000 }"/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
$rootScope.form.alias.$commitViewValue();
|
||||
expect($rootScope.name).toEqual('a');
|
||||
|
||||
$rootScope.form.alias.$setPristine();
|
||||
$timeout.flush(1000);
|
||||
expect($rootScope.form.alias.$pristine).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should reset input val if rollbackViewValue called during pending update', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(inputElm.val()).toBe('a');
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect(inputElm.val()).toBe('');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect(inputElm.val()).toBe('');
|
||||
});
|
||||
|
||||
|
||||
it('should allow canceling pending updates', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
it('should allow canceling debounced updates', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 10000 }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(2000);
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(10000);
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
it('should handle model updates correctly even if rollbackViewValue is not invoked', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
$rootScope.$apply('name = \'b\'');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toBe('b');
|
||||
});
|
||||
|
||||
|
||||
it('should reset input val if rollbackViewValue called during debounce', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 2000 }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(inputElm.val()).toBe('a');
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect(inputElm.val()).toBe('');
|
||||
$timeout.flush(3000);
|
||||
expect(inputElm.val()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getterSetter', function() {
|
||||
it('should not try to invoke a model if getterSetter is false', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" ' +
|
||||
'ng-model-options="{ getterSetter: false }" />');
|
||||
|
||||
var spy = $rootScope.name = jasmine.createSpy('setterSpy');
|
||||
helper.changeInputValueTo('a');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(inputElm.val()).toBe('a');
|
||||
});
|
||||
|
||||
|
||||
it('should not try to invoke a model if getterSetter is not set', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" ng-model="name" />');
|
||||
|
||||
var spy = $rootScope.name = jasmine.createSpy('setterSpy');
|
||||
helper.changeInputValueTo('a');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(inputElm.val()).toBe('a');
|
||||
});
|
||||
|
||||
|
||||
it('should try to invoke a function model if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
|
||||
var spy = $rootScope.name = jasmine.createSpy('setterSpy').and.callFake(function() {
|
||||
return 'b';
|
||||
});
|
||||
$rootScope.$apply();
|
||||
expect(inputElm.val()).toBe('b');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(inputElm.val()).toBe('b');
|
||||
expect(spy).toHaveBeenCalledWith('a');
|
||||
expect($rootScope.name).toBe(spy);
|
||||
});
|
||||
|
||||
|
||||
it('should assign to non-function models if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
|
||||
$rootScope.name = 'c';
|
||||
helper.changeInputValueTo('d');
|
||||
expect(inputElm.val()).toBe('d');
|
||||
expect($rootScope.name).toBe('d');
|
||||
});
|
||||
|
||||
|
||||
it('should fail on non-assignable model binding if getterSetter is false', function() {
|
||||
expect(function() {
|
||||
var inputElm = helper.compileInput('<input type="text" ng-model="accessor(user, \'name\')" />');
|
||||
}).toThrowMinErr('ngModel', 'nonassign', 'Expression \'accessor(user, \'name\')\' is non-assignable.');
|
||||
});
|
||||
|
||||
|
||||
it('should not fail on non-assignable model binding if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="accessor(user, \'name\')" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
});
|
||||
|
||||
|
||||
it('should invoke a model in the correct context if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="someService.getterSetter" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
|
||||
$rootScope.someService = {
|
||||
value: 'a',
|
||||
getterSetter: function(newValue) {
|
||||
this.value = newValue || this.value;
|
||||
return this.value;
|
||||
}
|
||||
};
|
||||
spyOn($rootScope.someService, 'getterSetter').and.callThrough();
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(inputElm.val()).toBe('a');
|
||||
expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
|
||||
expect($rootScope.someService.value).toBe('a');
|
||||
|
||||
helper.changeInputValueTo('b');
|
||||
expect($rootScope.someService.getterSetter).toHaveBeenCalledWith('b');
|
||||
expect($rootScope.someService.value).toBe('b');
|
||||
|
||||
$rootScope.someService.value = 'c';
|
||||
$rootScope.$apply();
|
||||
expect(inputElm.val()).toBe('c');
|
||||
expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('allowInvalid', function() {
|
||||
it('should assign invalid values to the scope if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />');
|
||||
helper.changeInputValueTo('12345');
|
||||
|
||||
expect($rootScope.value).toBe('12345');
|
||||
expect(inputElm).toBeInvalid();
|
||||
});
|
||||
|
||||
|
||||
it('should not assign not parsable values to the scope if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="number" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />', {
|
||||
valid: false,
|
||||
badInput: true
|
||||
});
|
||||
helper.changeInputValueTo('abcd');
|
||||
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect(inputElm).toBeInvalid();
|
||||
});
|
||||
|
||||
|
||||
it('should update the scope before async validators execute if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />');
|
||||
var defer;
|
||||
$rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
helper.changeInputValueTo('12345');
|
||||
|
||||
expect($rootScope.value).toBe('12345');
|
||||
expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
|
||||
defer.reject();
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.value).toBe('12345');
|
||||
expect(inputElm).toBeInvalid();
|
||||
});
|
||||
|
||||
|
||||
it('should update the view before async validators execute if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />');
|
||||
var defer;
|
||||
$rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
$rootScope.$apply('value = \'12345\'');
|
||||
|
||||
expect(inputElm.val()).toBe('12345');
|
||||
expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
|
||||
defer.reject();
|
||||
$rootScope.$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() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" ng-change="changed()" />');
|
||||
$rootScope.changed = jasmine.createSpy('changed');
|
||||
$rootScope.form.input.$parsers.push(function(value) {
|
||||
return 'modelValue';
|
||||
});
|
||||
|
||||
helper.changeInputValueTo('input1');
|
||||
expect($rootScope.value).toBe('modelValue');
|
||||
expect($rootScope.changed).toHaveBeenCalledOnce();
|
||||
|
||||
helper.changeInputValueTo('input2');
|
||||
expect($rootScope.value).toBe('modelValue');
|
||||
expect($rootScope.changed).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1783,615 +1783,3 @@ describe('ngModel', function() {
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('ngModelOptions attributes', function() {
|
||||
|
||||
var helper = {}, $rootScope, $compile, $timeout, $q;
|
||||
|
||||
generateInputCompilerHelper(helper);
|
||||
|
||||
beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_, _$q_) {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
$timeout = _$timeout_;
|
||||
$q = _$q_;
|
||||
}));
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on text inputs', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should not dirty the input if nothing was changed before updateOn trigger', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.form.alias.$pristine).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on text areas', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<textarea ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should bind the element to a list of events', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur mousemove\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
|
||||
helper.changeInputValueTo('b');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
browserTrigger(inputElm, 'mousemove');
|
||||
expect($rootScope.name).toEqual('b');
|
||||
});
|
||||
|
||||
|
||||
it('should allow keeping the default update behavior on text inputs', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'default\' }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should allow sharing options between multiple inputs', function() {
|
||||
$rootScope.options = {updateOn: 'default'};
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name1" name="alias1" ' +
|
||||
'ng-model-options="options"' +
|
||||
'/>' +
|
||||
'<input type="text" ng-model="name2" name="alias2" ' +
|
||||
'ng-model-options="options"' +
|
||||
'/>');
|
||||
|
||||
helper.changeGivenInputTo(inputElm.eq(0), 'a');
|
||||
helper.changeGivenInputTo(inputElm.eq(1), 'b');
|
||||
expect($rootScope.name1).toEqual('a');
|
||||
expect($rootScope.name2).toEqual('b');
|
||||
});
|
||||
|
||||
|
||||
it('should hold a copy of the options object', function() {
|
||||
$rootScope.options = {updateOn: 'default'};
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="options"' +
|
||||
'/>');
|
||||
expect($rootScope.options).toEqual({updateOn: 'default'});
|
||||
expect($rootScope.form.alias.$options).not.toBe($rootScope.options);
|
||||
});
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on checkboxes', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should allow keeping the default update behavior on checkboxes', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should allow overriding the model update trigger event on radio buttons', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="radio" ng-model="color" value="white" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\'}"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="red" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\'}"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="blue" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\'}"' +
|
||||
'/>');
|
||||
|
||||
$rootScope.$apply('color = \'white\'');
|
||||
browserTrigger(inputElm[2], 'click');
|
||||
expect($rootScope.color).toBe('white');
|
||||
|
||||
browserTrigger(inputElm[2], 'blur');
|
||||
expect($rootScope.color).toBe('blue');
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should allow keeping the default update behavior on radio buttons', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="radio" ng-model="color" value="white" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="red" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="blue" ' +
|
||||
'ng-model-options="{ updateOn: \'blur default\' }"' +
|
||||
'/>');
|
||||
|
||||
$rootScope.$apply('color = \'white\'');
|
||||
browserTrigger(inputElm[2], 'click');
|
||||
expect($rootScope.color).toBe('blue');
|
||||
});
|
||||
|
||||
|
||||
it('should trigger only after timeout in text inputs', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 10000 }"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
helper.changeInputValueTo('b');
|
||||
helper.changeInputValueTo('c');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(9000);
|
||||
expect($rootScope.name).toEqual('c');
|
||||
});
|
||||
|
||||
|
||||
it('should trigger only after timeout in checkboxes', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ debounce: 10000 }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(9000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should trigger only after timeout in radio buttons', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="radio" ng-model="color" value="white" />' +
|
||||
'<input type="radio" ng-model="color" value="red" ' +
|
||||
'ng-model-options="{ debounce: 20000 }"' +
|
||||
'/>' +
|
||||
'<input type="radio" ng-model="color" value="blue" ' +
|
||||
'ng-model-options="{ debounce: 30000 }"' +
|
||||
'/>');
|
||||
|
||||
browserTrigger(inputElm[0], 'click');
|
||||
expect($rootScope.color).toBe('white');
|
||||
browserTrigger(inputElm[1], 'click');
|
||||
expect($rootScope.color).toBe('white');
|
||||
$timeout.flush(12000);
|
||||
expect($rootScope.color).toBe('white');
|
||||
$timeout.flush(10000);
|
||||
expect($rootScope.color).toBe('red');
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should not trigger digest while debouncing', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 10000 }"' +
|
||||
'/>');
|
||||
|
||||
var watchSpy = jasmine.createSpy('watchSpy');
|
||||
$rootScope.$watch(watchSpy);
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(watchSpy).not.toHaveBeenCalled();
|
||||
|
||||
$timeout.flush(10000);
|
||||
expect(watchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should allow selecting different debounce timeouts for each event',
|
||||
function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{' +
|
||||
'updateOn: \'default blur\', ' +
|
||||
'debounce: { default: 10000, blur: 5000 }' +
|
||||
'}"' +
|
||||
'/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(6000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(4000);
|
||||
expect($rootScope.name).toEqual('a');
|
||||
helper.changeInputValueTo('b');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
$timeout.flush(4000);
|
||||
expect($rootScope.name).toEqual('a');
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.name).toEqual('b');
|
||||
});
|
||||
|
||||
|
||||
it('should allow selecting different debounce timeouts for each event on checkboxes', function() {
|
||||
var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ ' +
|
||||
'updateOn: \'default blur\', debounce: { default: 10000, blur: 5000 } }"' +
|
||||
'/>');
|
||||
|
||||
inputElm[0].checked = false;
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(8000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
inputElm[0].checked = true;
|
||||
browserTrigger(inputElm, 'click');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should allow selecting 0 for non-default debounce timeouts for each event on checkboxes', function() {
|
||||
var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" ' +
|
||||
'ng-model-options="{ ' +
|
||||
'updateOn: \'default blur\', debounce: { default: 10000, blur: 0 } }"' +
|
||||
'/>');
|
||||
|
||||
inputElm[0].checked = false;
|
||||
browserTrigger(inputElm, 'click');
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(8000);
|
||||
expect($rootScope.checkbox).toBeUndefined();
|
||||
$timeout.flush(3000);
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
inputElm[0].checked = true;
|
||||
browserTrigger(inputElm, 'click');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
$timeout.flush(0);
|
||||
expect($rootScope.checkbox).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should inherit model update settings from ancestor elements', function() {
|
||||
var doc = $compile(
|
||||
'<form name="test" ' +
|
||||
'ng-model-options="{ debounce: 10000, updateOn: \'blur\' }" >' +
|
||||
'<input type="text" ng-model="name" name="alias" />' +
|
||||
'</form>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var inputElm = doc.find('input').eq(0);
|
||||
helper.changeGivenInputTo(inputElm, 'a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
$timeout.flush(2000);
|
||||
expect($rootScope.name).toBeUndefined();
|
||||
$timeout.flush(9000);
|
||||
expect($rootScope.name).toEqual('a');
|
||||
dealoc(doc);
|
||||
});
|
||||
|
||||
|
||||
it('should flush debounced events when calling $commitViewValue directly', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 1000 }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$rootScope.form.alias.$commitViewValue();
|
||||
expect($rootScope.name).toEqual('a');
|
||||
});
|
||||
|
||||
|
||||
it('should cancel debounced events when calling $commitViewValue', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 1000 }"/>');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
$rootScope.form.alias.$commitViewValue();
|
||||
expect($rootScope.name).toEqual('a');
|
||||
|
||||
$rootScope.form.alias.$setPristine();
|
||||
$timeout.flush(1000);
|
||||
expect($rootScope.form.alias.$pristine).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should reset input val if rollbackViewValue called during pending update', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(inputElm.val()).toBe('a');
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect(inputElm.val()).toBe('');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect(inputElm.val()).toBe('');
|
||||
});
|
||||
|
||||
|
||||
it('should allow canceling pending updates', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
it('should allow canceling debounced updates', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 10000 }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(2000);
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
$timeout.flush(10000);
|
||||
expect($rootScope.name).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
it('should handle model updates correctly even if rollbackViewValue is not invoked', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ updateOn: \'blur\' }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
$rootScope.$apply('name = \'b\'');
|
||||
browserTrigger(inputElm, 'blur');
|
||||
expect($rootScope.name).toBe('b');
|
||||
});
|
||||
|
||||
|
||||
it('should reset input val if rollbackViewValue called during debounce', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" name="alias" ' +
|
||||
'ng-model-options="{ debounce: 2000 }" />');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(inputElm.val()).toBe('a');
|
||||
$rootScope.form.alias.$rollbackViewValue();
|
||||
expect(inputElm.val()).toBe('');
|
||||
$timeout.flush(3000);
|
||||
expect(inputElm.val()).toBe('');
|
||||
});
|
||||
|
||||
|
||||
it('should not try to invoke a model if getterSetter is false', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" ' +
|
||||
'ng-model-options="{ getterSetter: false }" />');
|
||||
|
||||
var spy = $rootScope.name = jasmine.createSpy('setterSpy');
|
||||
helper.changeInputValueTo('a');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(inputElm.val()).toBe('a');
|
||||
});
|
||||
|
||||
|
||||
it('should not try to invoke a model if getterSetter is not set', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" ng-model="name" />');
|
||||
|
||||
var spy = $rootScope.name = jasmine.createSpy('setterSpy');
|
||||
helper.changeInputValueTo('a');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(inputElm.val()).toBe('a');
|
||||
});
|
||||
|
||||
|
||||
it('should try to invoke a function model if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
|
||||
var spy = $rootScope.name = jasmine.createSpy('setterSpy').and.callFake(function() {
|
||||
return 'b';
|
||||
});
|
||||
$rootScope.$apply();
|
||||
expect(inputElm.val()).toBe('b');
|
||||
|
||||
helper.changeInputValueTo('a');
|
||||
expect(inputElm.val()).toBe('b');
|
||||
expect(spy).toHaveBeenCalledWith('a');
|
||||
expect($rootScope.name).toBe(spy);
|
||||
});
|
||||
|
||||
|
||||
it('should assign to non-function models if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="name" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
|
||||
$rootScope.name = 'c';
|
||||
helper.changeInputValueTo('d');
|
||||
expect(inputElm.val()).toBe('d');
|
||||
expect($rootScope.name).toBe('d');
|
||||
});
|
||||
|
||||
|
||||
it('should fail on non-assignable model binding if getterSetter is false', function() {
|
||||
expect(function() {
|
||||
var inputElm = helper.compileInput('<input type="text" ng-model="accessor(user, \'name\')" />');
|
||||
}).toThrowMinErr('ngModel', 'nonassign', 'Expression \'accessor(user, \'name\')\' is non-assignable.');
|
||||
});
|
||||
|
||||
|
||||
it('should not fail on non-assignable model binding if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="accessor(user, \'name\')" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
});
|
||||
|
||||
|
||||
it('should invoke a model in the correct context if getterSetter is true', function() {
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="text" ng-model="someService.getterSetter" ' +
|
||||
'ng-model-options="{ getterSetter: true }" />');
|
||||
|
||||
$rootScope.someService = {
|
||||
value: 'a',
|
||||
getterSetter: function(newValue) {
|
||||
this.value = newValue || this.value;
|
||||
return this.value;
|
||||
}
|
||||
};
|
||||
spyOn($rootScope.someService, 'getterSetter').and.callThrough();
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(inputElm.val()).toBe('a');
|
||||
expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
|
||||
expect($rootScope.someService.value).toBe('a');
|
||||
|
||||
helper.changeInputValueTo('b');
|
||||
expect($rootScope.someService.getterSetter).toHaveBeenCalledWith('b');
|
||||
expect($rootScope.someService.value).toBe('b');
|
||||
|
||||
$rootScope.someService.value = 'c';
|
||||
$rootScope.$apply();
|
||||
expect(inputElm.val()).toBe('c');
|
||||
expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
|
||||
it('should assign invalid values to the scope if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />');
|
||||
helper.changeInputValueTo('12345');
|
||||
|
||||
expect($rootScope.value).toBe('12345');
|
||||
expect(inputElm).toBeInvalid();
|
||||
});
|
||||
|
||||
|
||||
it('should not assign not parsable values to the scope if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="number" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />', {
|
||||
valid: false,
|
||||
badInput: true
|
||||
});
|
||||
helper.changeInputValueTo('abcd');
|
||||
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect(inputElm).toBeInvalid();
|
||||
});
|
||||
|
||||
|
||||
it('should update the scope before async validators execute if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />');
|
||||
var defer;
|
||||
$rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
helper.changeInputValueTo('12345');
|
||||
|
||||
expect($rootScope.value).toBe('12345');
|
||||
expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
|
||||
defer.reject();
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.value).toBe('12345');
|
||||
expect(inputElm).toBeInvalid();
|
||||
});
|
||||
|
||||
|
||||
it('should update the view before async validators execute if allowInvalid is true', function() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" />');
|
||||
var defer;
|
||||
$rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
$rootScope.$apply('value = \'12345\'');
|
||||
|
||||
expect(inputElm.val()).toBe('12345');
|
||||
expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
|
||||
defer.reject();
|
||||
$rootScope.$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() {
|
||||
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' +
|
||||
'ng-model-options="{allowInvalid: true}" ng-change="changed()" />');
|
||||
$rootScope.changed = jasmine.createSpy('changed');
|
||||
$rootScope.form.input.$parsers.push(function(value) {
|
||||
return 'modelValue';
|
||||
});
|
||||
|
||||
helper.changeInputValueTo('input1');
|
||||
expect($rootScope.value).toBe('modelValue');
|
||||
expect($rootScope.changed).toHaveBeenCalledOnce();
|
||||
|
||||
helper.changeInputValueTo('input2');
|
||||
expect($rootScope.value).toBe('modelValue');
|
||||
expect($rootScope.changed).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user