From 2c01fcc188f562228a7fae9425fb706aa601fb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Thu, 30 May 2013 21:16:36 +0200 Subject: [PATCH] feat(input): Allow custom events and timeouts to trigger model updates By default, any change on the content will trigger an immediate model update and form validation. Now you can override this behavior using the `ng-update-model-on` attribute to bind only to a comma-delimited list of events. I.e. `ng-update-model-on="blur"` will update and validate only after the control loses focus. If you want to keep the default behavior and just add new events that may trigger the model update and validation, add `default` as one of the specified events. I.e. `ng-update-model-on="default,mousedown"` Also, a `ng-update-model-debounce` attribute will allow defering the actual model update some time after the last trigger event takes place (debouncing). This feature is not available in radio buttons. I.e. `ng-update-model-debounce="500"` Custom debouncing timeouts can be set for each event if you use an object in `ng-update-model-on`. I.e. `ng-update-model-on="{default: 500, blur: 0}"` You can specify both attributes in any tag so they became the default settings for any child control, although they can be overriden. Closes #1285 --- angularFiles.js | 1 + docs/content/guide/forms.ngdoc | 40 ++++ src/AngularPublic.js | 4 + src/ng/directive/input.js | 313 +++++++++++++++++++++--------- src/ng/directive/ngUpdateModel.js | 97 +++++++++ test/ng/directive/inputSpec.js | 169 ++++++++++++++++ 6 files changed, 533 insertions(+), 91 deletions(-) create mode 100644 src/ng/directive/ngUpdateModel.js diff --git a/angularFiles.js b/angularFiles.js index 9603c289abd8..1b63b3243c2a 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -60,6 +60,7 @@ angularFiles = { 'src/ng/directive/ngStyle.js', 'src/ng/directive/ngSwitch.js', 'src/ng/directive/ngTransclude.js', + 'src/ng/directive/ngUpdateModel.js', 'src/ng/directive/script.js', 'src/ng/directive/select.js', 'src/ng/directive/style.js' diff --git a/docs/content/guide/forms.ngdoc b/docs/content/guide/forms.ngdoc index f3f95b7c81d7..ce25c2177515 100644 --- a/docs/content/guide/forms.ngdoc +++ b/docs/content/guide/forms.ngdoc @@ -175,6 +175,46 @@ This allows us to extend the above example with these features: +# Non-immediate (debounced) or custom triggered model updates + +By default, any change on the content will trigger a model update and form validation. You can override this behavior using the `ng-update-model-on` +attribute to bind only to a comma-delimited list of events. I.e. `ng-update-model-on="blur"` will update and validate only after the control loses +focus. + +If you want to keep the default behavior and just add new events that may trigger the model update +and validation, add "default" as one of the specified events. I.e. `ng-update-model-on="default,mousedown"` + +You can delay the model update/validation using `ng-update-model-debounce`. I.e. `ng-update-model-debounce="500"` will wait for half a second since +the last content change before triggering the model update and form validation. This debouncing feature is not available on radio buttons. + +Custom debouncing timeouts can be set for each event for each event if you use an object in `ng-update-model-on`. +I.e. `ng-update-model-on="{default: 500, blur: 0}"` + +Using the object notation allows any valid Angular expression to be used inside, including data and function calls from the scope. + +If those attributes are added to an element, they will be applied to all the child elements and controls that inherit from it unless they are +overriden. + +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). + + + +
+
+ Name: +
+
+
model = {{user | json}}
+
+ +
+
+ # Custom Validation diff --git a/src/AngularPublic.js b/src/AngularPublic.js index eb97b4c5ff46..608960abdb0f 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -47,6 +47,8 @@ ngValueDirective, ngAttributeAliasDirectives, ngEventDirectives, + ngUpdateModelOnDirective, + ngUpdateModelDebounceDirective, $AnchorScrollProvider, $AnimateProvider, @@ -174,6 +176,8 @@ function publishExternalAPI(angular){ ngChange: ngChangeDirective, required: requiredDirective, ngRequired: requiredDirective, + ngUpdateModelOn: ngUpdateModelOnDirective, + ngUpdateModelDebounce: ngUpdateModelDebounceDirective, ngValue: ngValueDirective }). directive(ngAttributeAliasDirectives). diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index da8825e34063..f3da58c32468 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -37,6 +37,12 @@ var inputType = { * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered + * content change before triggering a model update (debouncing). + * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. You can also specify an + * object in which the key is the event and the value the particular timeout to be applied to it. * * @example @@ -115,6 +121,11 @@ var inputType = { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered + * content change before triggering a model update. + * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. * * @example @@ -184,6 +195,11 @@ var inputType = { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered + * content change before triggering a model update. + * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. * * @example @@ -252,6 +268,11 @@ var inputType = { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered + * content change before triggering a model update. + * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. * * @example @@ -309,6 +330,9 @@ var inputType = { * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. * * @example @@ -351,6 +375,11 @@ var inputType = { * @param {string=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered + * content change before triggering a model update. + * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. * * @example @@ -390,10 +419,41 @@ var inputType = { 'reset': noop }; +function textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) { + // In composition mode, users are still inputing intermediate text buffer, + // hold the listener until composition is done. + // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent + var composing = false; -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + element.on('compositionstart', function() { + composing = true; + }); + + element.on('compositionend', function() { + composing = false; + }); + + var timeout = null, + eventList, + updateTimeout, + updateDefaultTimeout; + + var isEmpty = function(value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; + + // Get update model details from controllers + if (isDefined(updModOnCtrl)) { + eventList = updModOnCtrl.$getEventList(); + updateTimeout = updModOnCtrl.$getDebounceTimeout(); + } + + if (isDefined(updModlTimCtrl)) { + updateDefaultTimeout = updModlTimCtrl.$getDefaultTimeout(); + } + + var getValue = function() { - var listener = function() { var value = element.val(); // By default we will trim the value @@ -402,7 +462,11 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (toBoolean(attr.ngTrim || 'T')) { value = trim(value); } + return value; + }; + var update = function() { + var value = getValue(); if (ctrl.$viewValue !== value) { scope.$apply(function() { ctrl.$setViewValue(value); @@ -410,42 +474,74 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }; - // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the - // input event on backspace, delete or cut - if ($sniffer.hasEvent('input')) { - element.on('input', listener); - } else { - var timeout; + var listener = function(event) { + if (composing) return; - var deferListener = function() { - if (!timeout) { - timeout = $browser.defer(function() { - listener(); - timeout = null; - }); - } - }; + var callbackTimeout = (!isEmpty(updateTimeout)) + ? updateTimeout[event.type] || updateTimeout['default'] || updateDefaultTimeout || 0 + : updateDefaultTimeout || 0; + + if (callbackTimeout>0) { + timeout = $timeout(update, callbackTimeout, false, timeout); + } + else { + update(); + } + }; - element.on('keydown', function(event) { - var key = event.keyCode; + var deferListener = function(ev) { + $browser.defer(function() { + listener(ev); + }); + }; - // ignore - // command modifiers arrows - if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; + var defaultEvents = true; - deferListener(); + // Allow adding/overriding bound events + if (!isEmpty(eventList)) { + defaultEvents = false; + // bind to user-defined events + forEach(eventList.split(','), function(ev) { + ev = trim(ev).toLowerCase(); + if (ev === 'default') { + defaultEvents = true; + } + else { + element.on(ev, listener); + } }); + } + + if (defaultEvents) { + + // default behavior: bind to input events or keydown+change + + // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the + // input event on backspace, delete or cut + if ($sniffer.hasEvent('input')) { + element.bind('input', listener); + } else { + element.on('keydown', function(event) { + var key = event.keyCode; + + // ignore + // command modifiers arrows + if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; + + deferListener('keydown'); + }); - // if user paste into input using mouse, we need "change" event to catch it - element.on('change', listener); + // if user paste into input using mouse on older browser + // or form autocomplete on newer browser, we need "change" event to catch it + element.on('change', listener); - // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it - if ($sniffer.hasEvent('paste')) { - element.on('paste cut', deferListener); + // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it + if ($sniffer.hasEvent('paste')) { + element.on('paste cut', deferListener); + } } } - ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; @@ -524,8 +620,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } } -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); +function numberInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser); ctrl.$parsers.push(function(value) { var empty = ctrl.$isEmpty(value); @@ -586,8 +682,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } -function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); +function urlInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser); var urlValidator = function(value) { if (ctrl.$isEmpty(value) || URL_REGEXP.test(value)) { @@ -603,8 +699,8 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$parsers.push(urlValidator); } -function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); +function emailInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser); var emailValidator = function(value) { if (ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value)) { @@ -620,18 +716,31 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$parsers.push(emailValidator); } -function radioInputType(scope, element, attr, ctrl) { - // make the name unique, if not defined - if (isUndefined(attr.name)) { - element.attr('name', nextUid()); - } +function radioInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout) { + + // Get update model details from controllers + var eventList = (isDefined(updModOnCtrl)) ? updModOnCtrl.$getEventList() : 'click'; - element.on('click', function() { + var listener = function() { if (element[0].checked) { scope.$apply(function() { ctrl.$setViewValue(attr.value); }); } + }; + + // make the name unique, if not defined + if (isUndefined(attr.name)) { + element.attr('name', nextUid()); + } + + // bind to user-defined/default events + forEach(eventList.split(','), function(ev) { + ev = trim(ev).toLowerCase(); + if (ev === 'default') { + ev = 'click'; + } + element.bind(ev, listener); }); ctrl.$render = function() { @@ -642,17 +751,61 @@ function radioInputType(scope, element, attr, ctrl) { attr.$observe('value', ctrl.$render); } -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; +function checkboxInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout) { + var timeout = null, + trueValue = attr.ngTrueValue, + falseValue = attr.ngFalseValue, + eventList, + updateDefaultTimeout, + updateTimeout; + + // Get update model details from controllers + eventList = 'click'; + + // Get update model details from controllers + if (isDefined(updModOnCtrl)) { + eventList = updModOnCtrl.$getEventList(); + updateTimeout = updModOnCtrl.$getDebounceTimeout(); + } - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; + if (isDefined(updModlTimCtrl)) { + updateDefaultTimeout = updModlTimCtrl.$getDefaultTimeout(); + } - element.on('click', function() { + var update = function() { scope.$apply(function() { ctrl.$setViewValue(element[0].checked); }); + }; + + var listener = function(event) { + + var isEmpty = function(value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; + + var callbackTimeout = (!isEmpty(updateTimeout)) + ? updateTimeout[event.type] || updateTimeout['default'] || updateDefaultTimeout || 0 + : updateDefaultTimeout || 0; + + if (callbackTimeout>0) { + timeout = $timeout(update, callbackTimeout, false, timeout); + } + else { + update(); + } + }; + + if (!isString(trueValue)) trueValue = true; + if (!isString(falseValue)) falseValue = false; + + // bind to user-defined/default events + forEach(eventList.split(','), function(ev) { + ev = trim(ev).toLowerCase(); + if (ev === 'default') { + ev = 'click'; + } + element.bind(ev, listener); }); ctrl.$render = function() { @@ -797,14 +950,14 @@ function checkboxInputType(scope, element, attr, ctrl) { */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { +var inputDirective = ['$browser', '$sniffer', '$timeout', function($browser, $sniffer, $timeout) { return { restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); + require: ['?ngModel', '^?ngUpdateModelOn', '^?ngUpdateModelDebounce'], + link: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], ctrls[1], ctrls[2], $timeout, + $sniffer, $browser); } } }; @@ -840,6 +993,11 @@ var VALID_CLASS = 'ng-valid', * } * ngModel.$formatters.push(formatter); * + * + * @property {Array.} $viewChangeListeners Array of functions to execute whenever the + * view value has changed. It is called with no arguments, and its return value is ignored. + * This can be used in place of additional $watches against the model value. + * * @property {Object} $error An object hash with all errors as keys. * * @property {boolean} $pristine True if user has not interacted with the control yet. @@ -933,39 +1091,6 @@ var VALID_CLASS = 'ng-valid', * * - * ## Isolated Scope Pitfall - * - * Note that if you have a directive with an isolated scope, you cannot require `ngModel` - * since the model value will be looked up on the isolated scope rather than the outer scope. - * When the directive updates the model value, calling `ngModel.$setViewValue()` the property - * on the outer scope will not be updated. However you can get around this by using $parent. - * - * Here is an example of this situation. You'll notice that the first div is not updating the input. - * However the second div can update the input properly. - * - * - - angular.module('badIsolatedDirective', []).directive('isolate', function() { - return { - require: 'ngModel', - scope: { }, - template: '', - link: function(scope, element, attrs, ngModel) { - scope.$watch('innerModel', function(value) { - console.log(value); - ngModel.$setViewValue(value); - }); - } - }; - }); - - - -
-
-
- *
- * * */ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', @@ -1103,14 +1228,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @methodOf ng.directive:ngModel.NgModelController * * @description - * Read a value from view. + * Update the view value. * - * This method should be called from within a DOM event handler. - * For example {@link ng.directive:input input} or + * This method should be called when the view value changes, typically from within a DOM event handler. + * For example {@link ng.directive:input input} and * {@link ng.directive:select select} directives call it. * - * It internally calls all `$parsers` (including validators) and updates the `$modelValue` and the actual model path. - * Lastly it calls all registered change listeners. + * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, + * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to + * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * + * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * + * Note that calling this function does not trigger a `$digest`. * * @param {string} value Value from the view. */ @@ -1164,6 +1294,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$render(); } } + + return value; }); }]; @@ -1440,7 +1572,6 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; id="{{name}}" name="favorite"> -
You chose {{my.favorite}}
diff --git a/src/ng/directive/ngUpdateModel.js b/src/ng/directive/ngUpdateModel.js new file mode 100644 index 000000000000..6fb137bd740f --- /dev/null +++ b/src/ng/directive/ngUpdateModel.js @@ -0,0 +1,97 @@ +'use strict'; + +/** + * @ngdoc directive + * @name ng.directive:ngUpdateModelOn + * @restrict A + * + * @description + * The `ngUpdateModelOn` directive changes default behavior of model updates. You can customize + * which events will be bound to the `input` elements so that the model update will + * only be triggered when they occur. + * + * This option will be applicable to those `input` elements that descend from the + * element containing the directive. So, if you use `ngUpdateModelOn` on a `form` + * element, the default behavior will be used on the `input` elements within. + * + * See {@link guide/forms this link} for more information about debouncing and custom + * events. + * + * @element ANY + * @param {string} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events + * that will trigger a model update. If it is not set, it defaults to any inmediate change. If + * the list contains "default", the original behavior is also kept. You can also specify an + * object in which the key is the event and the value the particular debouncing timeout to be + * applied to it. + */ + +var SIMPLEOBJECT_TEST = /^\s*?\{(.*)\}\s*?$/; + +var NgUpdateModelOnController = ['$attrs', '$scope', + function UpdateModelOnController($attrs, $scope) { + + var attr = $attrs['ngUpdateModelOn']; + var updateModelOnValue; + var updateModelDebounceValue; + + if (SIMPLEOBJECT_TEST.test(attr)) { + updateModelDebounceValue = $scope.$eval(attr); + var keys = []; + for(var k in updateModelDebounceValue) { + keys.push(k); + } + updateModelOnValue = keys.join(','); + } + else { + updateModelOnValue = attr; + } + + this.$getEventList = function() { + return updateModelOnValue; + }; + + this.$getDebounceTimeout = function() { + return updateModelDebounceValue; + }; +}]; + +var ngUpdateModelOnDirective = [function() { + return { + restrict: 'A', + controller: NgUpdateModelOnController + }; +}]; + + +/** + * @ngdoc directive + * @name ng.directive:ngUpdateModelDebounce + * @restrict A + * + * @description + * The `ngUpdateModelDebounce` directive allows specifying a debounced timeout to model updates so they + * are not triggerer instantly but after the timer has expired. + * + * If you need to specify different timeouts for each event, you can use + * {@link ng.directive:ngUpdateModelOn ngUpdateModelOn} directive which the object notation. + * + * @element ANY + * @param {integer} ngUpdateModelDebounce Time in milliseconds to wait since the last registered + * content change before triggering a model update. + */ +var NgUpdateModelDebounceController = ['$attrs', + function UpdateModelDebounceController($attrs) { + + var updateModelDefaultTimeoutValue = $attrs['ngUpdateModelDebounce']; + + this.$getDefaultTimeout = function() { + return updateModelDefaultTimeoutValue; + }; +}]; + +var ngUpdateModelDebounceDirective = [function() { + return { + restrict: 'A', + controller: NgUpdateModelDebounceController + }; +}]; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 3783c9edcec7..dbd42cbe9d74 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -499,6 +499,175 @@ describe('input', function() { }); + describe('ng-update-model attributes', function() { + + it('should allow overriding the model update trigger event on text inputs', function() { + compileInput(''); + + changeInputValueTo('a'); + expect(scope.name).toBeUndefined(); + browserTrigger(inputElm, 'blur'); + expect(scope.name).toEqual('a'); + }); + + + it('should bind the element to a list of events on text inputs', function() { + compileInput(''); + + changeInputValueTo('a'); + expect(scope.name).toBeUndefined(); + browserTrigger(inputElm, 'blur'); + expect(scope.name).toEqual('a'); + + changeInputValueTo('b'); + expect(scope.name).toEqual('a'); + browserTrigger(inputElm, 'mousemove'); + expect(scope.name).toEqual('b'); + }); + + + it('should allow keeping the default update behavior on text inputs', function() { + compileInput(''); + + changeInputValueTo('a'); + expect(scope.name).toEqual('a'); + }); + + + it('should allow overriding the model update trigger event on checkboxes', function() { + compileInput(''); + + browserTrigger(inputElm, 'click'); + expect(scope.checkbox).toBe(undefined); + + browserTrigger(inputElm, 'blur'); + expect(scope.checkbox).toBe(true); + + browserTrigger(inputElm, 'click'); + expect(scope.checkbox).toBe(true); + }); + + + it('should allow keeping the default update behavior on checkboxes', function() { + compileInput(''); + + browserTrigger(inputElm, 'click'); + expect(scope.checkbox).toBe(true); + + browserTrigger(inputElm, 'click'); + expect(scope.checkbox).toBe(false); + }); + + + it('should allow overriding the model update trigger event on radio buttons', function() { + compileInput( + '' + + '' + + ''); + + scope.$apply(function() { + scope.color = 'white'; + }); + browserTrigger(inputElm[2], 'click'); + expect(scope.color).toBe('white'); + + browserTrigger(inputElm[2], 'blur'); + expect(scope.color).toBe('blue'); + + }); + + + it('should allow keeping the default update behavior on radio buttons', function() { + compileInput( + '' + + '' + + ''); + + scope.$apply(function() { + scope.color = 'white'; + }); + browserTrigger(inputElm[2], 'click'); + expect(scope.color).toBe('blue'); + }); + + + it('should trigger only after timeout in text inputs', inject(function($timeout) { + compileInput(''); + + changeInputValueTo('a'); + changeInputValueTo('b'); + changeInputValueTo('c'); + expect(scope.name).toEqual(undefined); + $timeout.flush(); + expect(scope.name).toEqual('c'); + })); + + + it('should trigger only after timeout in checkboxes', inject(function($timeout) { + compileInput(''); + + browserTrigger(inputElm, 'click'); + browserTrigger(inputElm, 'click'); + expect(scope.checkbox).toBe(undefined); + $timeout.flush(); + expect(scope.checkbox).toBe(false); + })); + + + it('should allow selecting different debounce timeouts for each event', inject(function($timeout) { + compileInput(''); + + changeInputValueTo('a'); + expect(scope.checkbox).toBe(undefined); + $timeout.flush(4000); + expect(scope.checkbox).toBe(undefined); + $timeout.flush(7000); + expect(scope.name).toEqual('a'); + changeInputValueTo('b'); + browserTrigger(inputElm, 'blur'); + $timeout.flush(4000); + expect(scope.name).toEqual('a'); + $timeout.flush(2000); + expect(scope.name).toEqual('b'); + })); + + + it('should allow selecting different debounce timeouts for each event on checkboxes', inject(function($timeout) { + compileInput(''); + + browserTrigger(inputElm, 'click'); + expect(scope.checkbox).toBe(undefined); + $timeout.flush(8000); + expect(scope.checkbox).toBe(undefined); + $timeout.flush(3000); + expect(scope.checkbox).toBe(true); + browserTrigger(inputElm, 'click'); + browserTrigger(inputElm, 'blur'); + $timeout.flush(3000); + expect(scope.checkbox).toBe(true); + $timeout.flush(3000); + expect(scope.checkbox).toBe(false); + + })); + + + it('should inherit model update settings from ancestor elements', inject(function($timeout) { + var doc = $compile('
' + + '
')(scope); + + var input = doc.find('input').eq(0); + input.val('a'); + expect(scope.name).toEqual(undefined); + browserTrigger(input, 'blur'); + expect(scope.name).toBe(undefined); + $timeout.flush(); + expect(scope.name).toEqual('a'); + dealoc(doc); + })); + + }); + + it('should allow complex reference binding', function() { compileInput('');