From e07ebf56f0703cc00090708745b8f362b5e8f02e 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 | 244 +++++++++++++++++++++++------- src/ng/directive/ngUpdateModel.js | 97 ++++++++++++ test/ng/directive/inputSpec.js | 169 +++++++++++++++++++++ 6 files changed, 501 insertions(+), 54 deletions(-) create mode 100644 src/ng/directive/ngUpdateModel.js diff --git a/angularFiles.js b/angularFiles.js index 1647ba48481a..ffe6dc7ef936 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -63,6 +63,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 0b91fc61f8ee..62ce6744f9d0 100644 --- a/docs/content/guide/forms.ngdoc +++ b/docs/content/guide/forms.ngdoc @@ -180,6 +180,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 0c02adeca685..67da68e7b5b5 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -48,6 +48,8 @@ ngValueDirective, ngAttributeAliasDirectives, ngEventDirectives, + ngUpdateModelOnDirective, + ngUpdateModelDebounceDirective, $AnchorScrollProvider, $AnimateProvider, @@ -183,6 +185,8 @@ function publishExternalAPI(angular){ ngChange: ngChangeDirective, required: requiredDirective, ngRequired: requiredDirective, + ngUpdateModelOn: ngUpdateModelOnDirective, + ngUpdateModelDebounce: ngUpdateModelDebounceDirective, ngValue: ngValueDirective }). directive({ diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index c31bb4004947..43a8353e7812 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 @@ -117,6 +123,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 @@ -192,6 +203,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 @@ -268,6 +284,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 @@ -334,6 +355,9 @@ var inputType = { * interaction with the input element. * @param {string} ngValue Angular expression which sets the value to which the expression should * be set when selected. + * @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 @@ -384,6 +408,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 @@ -454,7 +483,7 @@ function addNativeHtml5Validators(ctrl, validatorName, element) { } } -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { +function textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) { var validity = element.prop('validity'); // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. @@ -472,8 +501,26 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } - var listener = function() { - if (composing) return; + 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 update = function() { var value = element.val(); // By default we will trim the value @@ -498,42 +545,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 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 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 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 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 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); - ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; @@ -593,8 +672,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); @@ -638,8 +717,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) { return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); @@ -649,8 +728,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) { return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); @@ -660,18 +739,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) { - element.on('click', function() { + // Get update model details from controllers + var eventList = (isDefined(updModOnCtrl)) ? updModOnCtrl.$getEventList() : 'click'; + + 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() { @@ -682,17 +774,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() { @@ -852,14 +988,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); } } }; 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 e3e50e02a69e..9b0c90a266dd 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -608,6 +608,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('');