Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit bd47f6c

Browse files
committed
feat(typeahead): add ng-model-options debounce support
- Adds support for ng-model-options `debounce` option Closes #4982
1 parent 6094e07 commit bd47f6c

File tree

5 files changed

+174
-10
lines changed

5 files changed

+174
-10
lines changed

src/typeahead/docs/demo.html

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ <h4>Asynchronous results</h4>
6464
<i class="glyphicon glyphicon-remove"></i> No Results Found
6565
</div>
6666

67+
<h4>ngModelOptions support</h4>
68+
<pre>Model: {{ngModelOptionsSelected | json}}</pre>
69+
<input type="text" ng-model="ngModelOptionsSelected" ng-model-options="modelOptions" uib-typeahead="state for state in states | filter:$viewValue | limitTo:8" class="form-control">
70+
6771
<h4>Custom templates for results</h4>
6872
<pre>Model: {{customSelected | json}}</pre>
6973
<input type="text" ng-model="customSelected" placeholder="Custom template" uib-typeahead="state as state.name for state in statesWithFlags | filter:{name:$viewValue}" typeahead-template-url="customTemplate.html" class="form-control" typeahead-show-hint="true">

src/typeahead/docs/demo.js

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/typeahead/docs/readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ The typeahead directives provide several attributes:
1717
:
1818
Assignable angular expression to data-bind to
1919

20+
* `ng-model-options`
21+
:
22+
Options for ng-model (see [ng-model-options directive](https://docs.angularjs.org/api/ng/directive/ngModelOptions)). Currently supports the `debounce` and `getterSetter` options
23+
2024
* `uib-typeahead` <i class="glyphicon glyphicon-eye-open"></i>
2125
:
2226
Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select))

src/typeahead/test/typeahead.spec.js

+110-1
Original file line numberDiff line numberDiff line change
@@ -947,8 +947,10 @@ describe('typeahead tests', function() {
947947

948948
expect($scope.test.typeahead.$error.parse).toBeUndefined();
949949
});
950+
});
950951

951-
it('issue #3823 - should support ng-model-options getterSetter', function() {
952+
describe('ng-model-options', function() {
953+
it('should support getterSetter', function() {
952954
function resultSetter(state) {
953955
return state;
954956
}
@@ -960,6 +962,113 @@ describe('typeahead tests', function() {
960962

961963
expect($scope.result).toBe(resultSetter);
962964
});
965+
966+
describe('debounce as a number', function() {
967+
it('should work with selecting via keyboard', function() {
968+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: 400}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>');
969+
var inputEl = findInput(element);
970+
971+
changeInputValueTo(element, 'Alaska');
972+
triggerKeyDown(element, 13);
973+
974+
expect($scope.result).not.toBe('Alaska');
975+
976+
$timeout.flush(400);
977+
978+
expect($scope.result).toBe('Alaska');
979+
});
980+
981+
it('should work with select on exact', function() {
982+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: 400}" uib-typeahead="state as state.name for state in states | filter:$viewvalue" typeahead-select-on-exact="true"></div>');
983+
var inputEl = findInput(element);
984+
985+
changeInputValueTo(element, 'Alaska');
986+
987+
expect($scope.result).not.toBe('Alaska');
988+
989+
$timeout.flush(400);
990+
991+
expect($scope.result).toBe('Alaska');
992+
});
993+
994+
it('should work with selecting a match via click', function() {
995+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: 400}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>');
996+
var inputEl = findInput(element);
997+
998+
changeInputValueTo(element, 'Alaska');
999+
var match = $(findMatches(element)[0]).find('a')[0];
1000+
1001+
$(match).click();
1002+
$scope.$digest();
1003+
1004+
expect($scope.result).not.toBe('Alaska');
1005+
1006+
$timeout.flush(400);
1007+
1008+
expect($scope.result).toBe('Alaska');
1009+
});
1010+
});
1011+
1012+
describe('debounce as an object', function() {
1013+
it('should work with selecting via keyboard', function() {
1014+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>');
1015+
var inputEl = findInput(element);
1016+
1017+
changeInputValueTo(element, 'Alaska');
1018+
triggerKeyDown(element, 13);
1019+
1020+
expect($scope.result).not.toBe('Alaska');
1021+
1022+
$timeout.flush(400);
1023+
1024+
expect($scope.result).toBe('Alaska');
1025+
});
1026+
1027+
it('should work with select on exact', function() {
1028+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue" typeahead-select-on-exact="true"></div>');
1029+
var inputEl = findInput(element);
1030+
1031+
changeInputValueTo(element, 'Alaska');
1032+
1033+
expect($scope.result).not.toBe('Alaska');
1034+
1035+
$timeout.flush(400);
1036+
1037+
expect($scope.result).toBe('Alaska');
1038+
});
1039+
1040+
it('should work with selecting a match via click', function() {
1041+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>');
1042+
var inputEl = findInput(element);
1043+
1044+
changeInputValueTo(element, 'Alaska');
1045+
var match = $(findMatches(element)[0]).find('a')[0];
1046+
1047+
$(match).click();
1048+
$scope.$digest();
1049+
1050+
expect($scope.result).not.toBe('Alaska');
1051+
1052+
$timeout.flush(400);
1053+
1054+
expect($scope.result).toBe('Alaska');
1055+
});
1056+
1057+
it('should work when blurring and select on blur', function() {
1058+
element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue" typeahead-select-on-blur="true"></div>');
1059+
var inputEl = findInput(element);
1060+
1061+
changeInputValueTo(element, 'Alaska');
1062+
element.blur();
1063+
$scope.$digest();
1064+
1065+
expect($scope.result).not.toBe('Alaska');
1066+
1067+
$timeout.flush(500);
1068+
1069+
expect($scope.result).toBe('Alaska');
1070+
});
1071+
});
9631072
});
9641073

9651074
describe('input formatting', function() {

src/typeahead/typeahead.js

+38-9
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
155155
'move-in-progress': 'moveInProgress',
156156
query: 'query',
157157
position: 'position',
158-
'assign-is-open': 'assignIsOpen(isOpen)'
158+
'assign-is-open': 'assignIsOpen(isOpen)',
159+
debounce: 'debounceUpdate'
159160
});
160161
//custom item template
161162
if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
@@ -235,7 +236,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
235236

236237
//Select the single remaining option if user input matches
237238
if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
238-
scope.select(0);
239+
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
240+
$$debounce(function() {
241+
scope.select(0);
242+
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
243+
} else {
244+
scope.select(0);
245+
}
239246
}
240247

241248
if (showHint) {
@@ -319,7 +326,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
319326
resetMatches();
320327

321328
scope.assignIsOpen = function (isOpen) {
322-
isOpenSetter(originalScope, isOpen);
329+
isOpenSetter(originalScope, isOpen);
323330
};
324331

325332
scope.select = function(activeIdx) {
@@ -369,7 +376,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
369376
case 9:
370377
case 13:
371378
scope.$apply(function () {
372-
scope.select(scope.activeIdx);
379+
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
380+
$$debounce(function() {
381+
scope.select(scope.activeIdx);
382+
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
383+
} else {
384+
scope.select(scope.activeIdx);
385+
}
373386
});
374387
break;
375388
case 27:
@@ -402,7 +415,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
402415
if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
403416
selected = true;
404417
scope.$apply(function() {
405-
scope.select(scope.activeIdx);
418+
if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
419+
$$debounce(function() {
420+
scope.select(scope.activeIdx);
421+
}, scope.debounceUpdate.blur);
422+
} else {
423+
scope.select(scope.activeIdx);
424+
}
406425
});
407426
}
408427
if (!isEditable && modelCtrl.$error.editable) {
@@ -459,6 +478,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
459478
modelCtrl = _modelCtrl;
460479
ngModelOptions = _ngModelOptions;
461480

481+
scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope);
482+
462483
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
463484
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
464485
modelCtrl.$parsers.unshift(function(inputValue) {
@@ -529,7 +550,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
529550
};
530551
})
531552

532-
.directive('uibTypeaheadPopup', function() {
553+
.directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
533554
return {
534555
scope: {
535556
matches: '=',
@@ -538,7 +559,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
538559
position: '&',
539560
moveInProgress: '=',
540561
select: '&',
541-
assignIsOpen: '&'
562+
assignIsOpen: '&',
563+
debounce: '&'
542564
},
543565
replace: true,
544566
templateUrl: function(element, attrs) {
@@ -562,11 +584,18 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
562584
};
563585

564586
scope.selectMatch = function(activeIdx) {
565-
scope.select({activeIdx: activeIdx});
587+
var debounce = scope.debounce();
588+
if (angular.isNumber(debounce) || angular.isObject(debounce)) {
589+
$$debounce(function() {
590+
scope.select({activeIdx: activeIdx});
591+
}, angular.isNumber(debounce) ? debounce : debounce['default']);
592+
} else {
593+
scope.select({activeIdx: activeIdx});
594+
}
566595
};
567596
}
568597
};
569-
})
598+
}])
570599

571600
.directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
572601
return {

0 commit comments

Comments
 (0)