@@ -16,6 +16,7 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;
16
16
var WEEK_REGEXP = / ^ ( \d { 4 } ) - W ( \d \d ) $ / ;
17
17
var MONTH_REGEXP = / ^ ( \d { 4 } ) - ( \d \d ) $ / ;
18
18
var TIME_REGEXP = / ^ ( \d \d ) : ( \d \d ) $ / ;
19
+ var DEFAULT_REGEXP = / \w d e f a u l t \w / ;
19
20
20
21
var inputType = {
21
22
@@ -877,8 +878,9 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
877
878
}
878
879
}
879
880
880
- function textInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) {
881
+ function textInputType ( scope , element , attr , ctrl , $sniffer , $browser ) {
881
882
var validity = element . prop ( 'validity' ) ;
883
+
882
884
// In composition mode, users are still inputing intermediate text buffer,
883
885
// hold the listener until composition is done.
884
886
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
@@ -895,9 +897,10 @@ function textInputType(scope, element, attr, ctrl, options, $sniffer, $browser)
895
897
} ) ;
896
898
}
897
899
898
- var listener = function ( ) {
900
+ var listener = function ( ev ) {
899
901
if ( composing ) return ;
900
- var value = element . val ( ) ;
902
+ var value = element . val ( ) ,
903
+ event = ev && ev . type ;
901
904
902
905
// By default we will trim the value
903
906
// If the attribute ng-trim exists we will avoid trimming
@@ -912,50 +915,59 @@ function textInputType(scope, element, attr, ctrl, options, $sniffer, $browser)
912
915
// even when the first character entered causes an error.
913
916
( validity && value === '' && ! validity . valueMissing ) ) {
914
917
if ( scope . $$phase ) {
915
- ctrl . $setViewValue ( value ) ;
918
+ ctrl . $setViewValue ( value , event ) ;
916
919
} else {
917
920
scope . $apply ( function ( ) {
918
- ctrl . $setViewValue ( value ) ;
921
+ ctrl . $setViewValue ( value , event ) ;
919
922
} ) ;
920
923
}
921
924
}
922
925
} ;
923
926
924
- // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
925
- // input event on backspace, delete or cut
926
- if ( $sniffer . hasEvent ( 'input' ) ) {
927
- element . on ( 'input' , listener ) ;
928
- } else {
929
- var timeout ;
927
+ // Allow adding/overriding bound events
928
+ if ( ( ctrl . $options . updateOn ) && ( ctrl . $options . updateOn . length ) ) {
929
+ // bind to user-defined events
930
+ element . on ( ctrl . $options . updateOn , listener ) ;
931
+ }
930
932
931
- var deferListener = function ( ) {
932
- if ( ! timeout ) {
933
- timeout = $browser . defer ( function ( ) {
934
- listener ( ) ;
935
- timeout = null ;
936
- } ) ;
937
- }
938
- } ;
933
+ // setup default events if requested
934
+ if ( ! ctrl . $options . updateOn || ( ctrl . $options . updateOnDefault ) ) {
935
+ // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
936
+ // input event on backspace, delete or cut
937
+ if ( $sniffer . hasEvent ( 'input' ) ) {
938
+ element . on ( 'input' , listener ) ;
939
+ } else {
940
+ var timeout ;
939
941
940
- element . on ( 'keydown' , function ( event ) {
941
- var key = event . keyCode ;
942
+ var deferListener = function ( ev ) {
943
+ if ( ! timeout ) {
944
+ timeout = $browser . defer ( function ( ) {
945
+ listener ( ev ) ;
946
+ timeout = null ;
947
+ } ) ;
948
+ }
949
+ } ;
942
950
943
- // ignore
944
- // command modifiers arrows
945
- if ( key === 91 || ( 15 < key && key < 19 ) || ( 37 <= key && key <= 40 ) ) return ;
951
+ element . on ( 'keydown' , function ( event ) {
952
+ var key = event . keyCode ;
946
953
947
- deferListener ( ) ;
948
- } ) ;
954
+ // ignore
955
+ // command modifiers arrows
956
+ if ( key === 91 || ( 15 < key && key < 19 ) || ( 37 <= key && key <= 40 ) ) return ;
949
957
950
- // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
951
- if ( $sniffer . hasEvent ( 'paste' ) ) {
952
- element . on ( 'paste cut' , deferListener ) ;
958
+ deferListener ( ) ;
959
+ } ) ;
960
+
961
+ // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
962
+ if ( $sniffer . hasEvent ( 'paste' ) ) {
963
+ element . on ( 'paste cut' , deferListener ) ;
964
+ }
953
965
}
954
- }
955
966
956
- // if user paste into input using mouse on older browser
957
- // or form autocomplete on newer browser, we need "change" event to catch it
958
- element . on ( 'change' , listener ) ;
967
+ // if user paste into input using mouse on older browser
968
+ // or form autocomplete on newer browser, we need "change" event to catch it
969
+ element . on ( 'change' , listener ) ;
970
+ }
959
971
960
972
ctrl . $render = function ( ) {
961
973
element . val ( ctrl . $isEmpty ( ctrl . $viewValue ) ? '' : ctrl . $viewValue ) ;
@@ -1067,8 +1079,8 @@ function createDateParser(regexp, mapping) {
1067
1079
}
1068
1080
1069
1081
function createDateInputType ( type , regexp , parseDate , format ) {
1070
- return function dynamicDateInputType ( scope , element , attr , ctrl , options , $sniffer , $browser , $filter ) {
1071
- textInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) ;
1082
+ return function dynamicDateInputType ( scope , element , attr , ctrl , $sniffer , $browser , $filter ) {
1083
+ textInputType ( scope , element , attr , ctrl , $sniffer , $browser ) ;
1072
1084
1073
1085
ctrl . $parsers . push ( function ( value ) {
1074
1086
if ( ctrl . $isEmpty ( value ) ) {
@@ -1118,8 +1130,8 @@ function createDateInputType(type, regexp, parseDate, format) {
1118
1130
} ;
1119
1131
}
1120
1132
1121
- function numberInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) {
1122
- textInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) ;
1133
+ function numberInputType ( scope , element , attr , ctrl , $sniffer , $browser ) {
1134
+ textInputType ( scope , element , attr , ctrl , $sniffer , $browser ) ;
1123
1135
1124
1136
ctrl . $parsers . push ( function ( value ) {
1125
1137
var empty = ctrl . $isEmpty ( value ) ;
@@ -1163,8 +1175,8 @@ function numberInputType(scope, element, attr, ctrl, options, $sniffer, $browser
1163
1175
} ) ;
1164
1176
}
1165
1177
1166
- function urlInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) {
1167
- textInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) ;
1178
+ function urlInputType ( scope , element , attr , ctrl , $sniffer , $browser ) {
1179
+ textInputType ( scope , element , attr , ctrl , $sniffer , $browser ) ;
1168
1180
1169
1181
var urlValidator = function ( value ) {
1170
1182
return validate ( ctrl , 'url' , ctrl . $isEmpty ( value ) || URL_REGEXP . test ( value ) , value ) ;
@@ -1174,8 +1186,8 @@ function urlInputType(scope, element, attr, ctrl, options, $sniffer, $browser) {
1174
1186
ctrl . $parsers . push ( urlValidator ) ;
1175
1187
}
1176
1188
1177
- function emailInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) {
1178
- textInputType ( scope , element , attr , ctrl , options , $sniffer , $browser ) ;
1189
+ function emailInputType ( scope , element , attr , ctrl , $sniffer , $browser ) {
1190
+ textInputType ( scope , element , attr , ctrl , $sniffer , $browser ) ;
1179
1191
1180
1192
var emailValidator = function ( value ) {
1181
1193
return validate ( ctrl , 'email' , ctrl . $isEmpty ( value ) || EMAIL_REGEXP . test ( value ) , value ) ;
@@ -1185,19 +1197,29 @@ function emailInputType(scope, element, attr, ctrl, options, $sniffer, $browser)
1185
1197
ctrl . $parsers . push ( emailValidator ) ;
1186
1198
}
1187
1199
1188
- function radioInputType ( scope , element , attr , ctrl , options ) {
1200
+ function radioInputType ( scope , element , attr , ctrl ) {
1189
1201
// make the name unique, if not defined
1190
1202
if ( isUndefined ( attr . name ) ) {
1191
1203
element . attr ( 'name' , nextUid ( ) ) ;
1192
1204
}
1193
1205
1194
- element . on ( 'click' , function ( ) {
1206
+ var listener = function ( ev ) {
1195
1207
if ( element [ 0 ] . checked ) {
1196
1208
scope . $apply ( function ( ) {
1197
- ctrl . $setViewValue ( attr . value ) ;
1209
+ ctrl . $setViewValue ( attr . value , ev && ev . type ) ;
1198
1210
} ) ;
1199
1211
}
1200
- } ) ;
1212
+ } ;
1213
+
1214
+ // Allow adding/overriding bound events
1215
+ if ( ( ctrl . $options . updateOn ) && ( ctrl . $options . updateOn . length ) ) {
1216
+ // bind to user-defined events
1217
+ element . on ( ctrl . $options . updateOn , listener ) ;
1218
+ }
1219
+
1220
+ if ( ! ctrl . $options . updateOn || ( ctrl . $options . updateOnDefault ) ) {
1221
+ element . on ( 'click' , listener ) ;
1222
+ }
1201
1223
1202
1224
ctrl . $render = function ( ) {
1203
1225
var value = attr . value ;
@@ -1207,18 +1229,28 @@ function radioInputType(scope, element, attr, ctrl, options) {
1207
1229
attr . $observe ( 'value' , ctrl . $render ) ;
1208
1230
}
1209
1231
1210
- function checkboxInputType ( scope , element , attr , ctrl , options ) {
1232
+ function checkboxInputType ( scope , element , attr , ctrl ) {
1211
1233
var trueValue = attr . ngTrueValue ,
1212
1234
falseValue = attr . ngFalseValue ;
1213
1235
1214
1236
if ( ! isString ( trueValue ) ) trueValue = true ;
1215
1237
if ( ! isString ( falseValue ) ) falseValue = false ;
1216
1238
1217
- element . on ( 'click' , function ( ) {
1239
+ var listener = function ( ev ) {
1218
1240
scope . $apply ( function ( ) {
1219
- ctrl . $setViewValue ( element [ 0 ] . checked ) ;
1220
- } ) ;
1221
- } ) ;
1241
+ ctrl . $setViewValue ( element [ 0 ] . checked , ev && ev . type ) ;
1242
+ } ) ;
1243
+ } ;
1244
+
1245
+ // Allow adding/overriding bound events
1246
+ if ( ( ctrl . $options . updateOn ) && ( ctrl . $options . updateOn . length ) ) {
1247
+ // bind to user-defined events
1248
+ element . on ( ctrl . $options . updateOn , listener ) ;
1249
+ }
1250
+
1251
+ if ( ! ctrl . $options . updateOn || ( ctrl . $options . updateOnDefault ) ) {
1252
+ element . on ( 'click' , listener ) ;
1253
+ }
1222
1254
1223
1255
ctrl . $render = function ( ) {
1224
1256
element [ 0 ] . checked = ctrl . $viewValue ;
@@ -1383,7 +1415,7 @@ var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sni
1383
1415
require : [ '?ngModel' , '^?ngModelOptions' ] ,
1384
1416
link : function ( scope , element , attr , ctrls ) {
1385
1417
if ( ctrls [ 0 ] ) {
1386
- ( inputType [ lowercase ( attr . type ) ] || inputType . text ) ( scope , element , attr , ctrls [ 0 ] , ctrls [ 1 ] , $sniffer ,
1418
+ ( inputType [ lowercase ( attr . type ) ] || inputType . text ) ( scope , element , attr , ctrls [ 0 ] , $sniffer ,
1387
1419
$browser , $filter ) ;
1388
1420
}
1389
1421
}
@@ -1662,26 +1694,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1662
1694
1663
1695
/**
1664
1696
* @ngdoc method
1665
- * @name ngModel.NgModelController#$setViewValue
1697
+ * @name ngModel.NgModelController#$cancelDebounce
1666
1698
*
1667
1699
* @description
1668
- * Update the view value.
1669
- *
1670
- * This method should be called when the view value changes, typically from within a DOM event handler.
1671
- * For example {@link ng.directive:input input} and
1672
- * {@link ng.directive:select select} directives call it.
1673
- *
1674
- * It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
1675
- * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
1676
- * `$modelValue` and the **expression** specified in the `ng-model` attribute.
1677
- *
1678
- * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
1679
- *
1680
- * Note that calling this function does not trigger a `$digest`.
1700
+ * Cancel a pending debounced update.
1681
1701
*
1682
- * @param {string } value Value from the view.
1702
+ * This method should be called before directly update a debounced model from the scope in
1703
+ * order to prevent unintended future changes of the model value because of a delayed event.
1683
1704
*/
1684
- this . $realSetViewValue = function ( value ) {
1705
+ this . $cancelDebounce = function ( ) {
1706
+ if ( pendingDebounce ) {
1707
+ $timeout . cancel ( pendingDebounce ) ;
1708
+ pendingDebounce = null ;
1709
+ }
1710
+ } ;
1711
+
1712
+ // update the view value
1713
+ this . $$realSetViewValue = function ( value ) {
1685
1714
this . $viewValue = value ;
1686
1715
1687
1716
// change to dirty
@@ -1709,22 +1738,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1709
1738
} ) ;
1710
1739
}
1711
1740
} ;
1741
+
1742
+ /**
1743
+ * @ngdoc method
1744
+ * @name ngModel.NgModelController#$setViewValue
1745
+ *
1746
+ * @description
1747
+ * Update the view value.
1748
+ *
1749
+ * This method should be called when the view value changes, typically from within a DOM event handler.
1750
+ * For example {@link ng.directive:input input} and
1751
+ * {@link ng.directive:select select} directives call it.
1752
+ *
1753
+ * It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
1754
+ * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
1755
+ * `$modelValue` and the **expression** specified in the `ng-model` attribute.
1756
+ *
1757
+ * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
1758
+ *
1759
+ * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
1760
+ * directive is used with a custom debounce for this particular event.
1761
+ *
1762
+ * Note that calling this function does not trigger a `$digest`.
1763
+ *
1764
+ * @param {string } value Value from the view.
1765
+ * @param {string } trigger Event that triggered the update.
1766
+ */
1712
1767
this . $setViewValue = function ( value , trigger ) {
1713
1768
var that = this ;
1714
- trigger = trigger || 'default' ;
1715
- var debounceDelay = ( isObject ( this . $options . debounce ) ? this . $options . debounce [ trigger ] : this . $options . debounce ) || 0 ;
1769
+ var debounceDelay = ( isObject ( this . $options . debounce )
1770
+ ? ( this . $options . debounce [ trigger ] || this . $options . debounce [ 'default' ] || 0 )
1771
+ : this . $options . debounce ) || 0 ;
1716
1772
1717
- if ( pendingDebounce ) {
1718
- $timeout . cancel ( pendingDebounce ) ;
1719
- pendingDebounce = null ;
1720
- }
1773
+ that . $cancelDebounce ( ) ;
1721
1774
if ( debounceDelay ) {
1722
1775
pendingDebounce = $timeout ( function ( ) {
1723
1776
pendingDebounce = null ;
1724
- that . $realSetViewValue ( value ) ;
1777
+ that . $$ realSetViewValue ( value ) ;
1725
1778
} , debounceDelay ) ;
1726
1779
} else {
1727
- that . $realSetViewValue ( value ) ;
1780
+ that . $$ realSetViewValue ( value ) ;
1728
1781
}
1729
1782
} ;
1730
1783
@@ -1737,12 +1790,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1737
1790
// if scope model value and ngModel value are out of sync
1738
1791
if ( ctrl . $modelValue !== value ) {
1739
1792
1740
- // Cancel any pending debounced update
1741
- if ( pendingDebounce ) {
1742
- $timeout . cancel ( pendingDebounce ) ;
1743
- pendingDebounce = null ;
1744
- }
1745
-
1746
1793
var formatters = ctrl . $formatters ,
1747
1794
idx = formatters . length ;
1748
1795
@@ -2155,11 +2202,92 @@ var ngValueDirective = function() {
2155
2202
} ;
2156
2203
} ;
2157
2204
2205
+ /**
2206
+ * @ngdoc directive
2207
+ * @name ngModelOptions
2208
+ *
2209
+ * @description
2210
+ * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of events
2211
+ * that will trigger a model update and/or a debouncing delay so that the actual update only takes place
2212
+ * when a timer expires; this timer will be reset after another change takes place.
2213
+ *
2214
+ * @param {Object= } Object that contains options to apply to the current model. Valid keys are:
2215
+ * - updateOn: string specifying which event should be the input bound to. If an array is supplied instead,
2216
+ * multiple events can be specified. There is a special event called `default` that
2217
+ * matches the default events belonging of the control.
2218
+ * - debounce: integer value which contains the debounce model update value in milliseconds. A value of 0
2219
+ * triggers an immediate update. If an object is supplied instead, you can specify a custom value
2220
+ * for each event. I.e.
2221
+ * `ngModelOptions="{ updateOn: ["default", "blur"], debounce: {'default': 500, 'blur': 0} }"`
2222
+ *
2223
+ * @example
2224
+
2225
+ The following example shows how to override immediate updates. Changes on the inputs within the form will update the model
2226
+ only when the control loses focus (blur event).
2227
+
2228
+ <example name="ngModelOptions-directive-1">
2229
+ <file name="index.html">
2230
+ <script>
2231
+ function Ctrl($scope) {
2232
+ $scope.user = { name: 'say', data: '' };
2233
+ }
2234
+ </script>
2235
+ <div ng-controller="Ctrl">
2236
+ <form>
2237
+ Name:
2238
+ <input type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" name="uName" /><br />
2239
+ Other data:
2240
+ <input type="text" ng-model="user.data" name="uData" /><br />
2241
+ </form>
2242
+ <pre>user.name = <span ng-bind="user.name"></span></pre>
2243
+ </div>
2244
+ </file>
2245
+ <file name="protractor.js" type="protractor">
2246
+ var model = element(by.binding('user.name'));
2247
+ var input = element(by.model('user.name'));
2248
+ var other = element(by.model('user.data'));
2249
+ it('should allow custom events', function() {
2250
+ input.sendKeys(' hello');
2251
+ expect(model.getText()).toEqual('say');
2252
+ other.click();
2253
+ expect(model.getText()).toEqual('say hello');
2254
+ });
2255
+ </file>
2256
+ </example>
2257
+
2258
+ This one shows how to debounce model changes. Model will be updated only 500 milliseconds after last change.
2158
2259
2260
+ <example name="ngModelOptions-directive-2">
2261
+ <file name="index.html">
2262
+ <script>
2263
+ function Ctrl($scope) {
2264
+ $scope.user = { name: 'say' };
2265
+ }
2266
+ </script>
2267
+ <div ng-controller="Ctrl">
2268
+ <form>
2269
+ Name:
2270
+ <input type="text" ng-model="user.name" name="uName" ng-model-options="{ debounce: 500 }" /><br />
2271
+ </form>
2272
+ <pre>user.name = <span ng-bind="user.name"></span></pre>
2273
+ </div>
2274
+ </file>
2275
+ </example>
2276
+ */
2159
2277
var ngModelOptionsDirective = function ( ) {
2160
2278
return {
2161
- controller : function ( $scope , $attrs ) {
2279
+ controller : [ '$scope' , '$attrs' , function ( $scope , $attrs ) {
2280
+ var that = this ;
2162
2281
this . $options = $scope . $eval ( $attrs . ngModelOptions ) ;
2163
- }
2282
+ this . $updateOn = [ ] ;
2283
+ // Allow adding/overriding bound events
2284
+ if ( this . $options . updateOn ) {
2285
+ // look up for default in event list
2286
+ this . $options . updateOnDefault = DEFAULT_REGEXP . test ( this . $options . updateOn ) ;
2287
+ this . $options . updateOn = this . $options . updateOn . replace ( DEFAULT_REGEXP , '' ) ;
2288
+ } else {
2289
+ this . $options . updateOnDefault = true ;
2290
+ }
2291
+ } ]
2164
2292
} ;
2165
2293
} ;
0 commit comments