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

Commit 2ffb86f

Browse files
Christopher Ruedwesleycho
Christopher Rued
authored andcommitted
feat(typeahead): Add support for should-select
By default behaves as before. If specified, allows a custom function to be defined to determine whether the keydown event should trigger selection based on the `typeahead-should-select` attribute. Closes #5671 Closes #5794
1 parent aec5304 commit 2ffb86f

File tree

3 files changed

+72
-17
lines changed

3 files changed

+72
-17
lines changed

src/typeahead/docs/readme.md

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ This directive works with promises, meaning you can retrieve matches using the `
7777
_(Default: `angular.noop`)_ -
7878
Binding to a variable that indicates if no matching results were found.
7979

80+
* `typeahead-should-select($event)`
81+
<small class="badge">$</small>
82+
_(Default: `null`)_ -
83+
A callback executed when a `keyup` event that might trigger a selection occurs. Selection will only occur if this function returns true.
84+
8085
* `typeahead-on-select($item, $model, $label, $event)`
8186
<small class="badge">$</small>
8287
_(Default: `null`)_ -

src/typeahead/test/typeahead.spec.js

+42
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,48 @@ describe('typeahead tests', function() {
472472
});
473473
});
474474

475+
describe('shouldSelect', function() {
476+
it('should select a match when function returns true', function() {
477+
$scope.shouldSelectFn = function() {
478+
return true;
479+
};
480+
var element = prepareInputEl('<div><input ng-model="result" typeahead-should-select="shouldSelectFn($event)" uib-typeahead="item for item in source | filter:$viewValue"></div>');
481+
var inputEl = findInput(element);
482+
483+
changeInputValueTo(element, 'b');
484+
triggerKeyDown(element, 13);
485+
486+
expect($scope.result).toEqual('bar');
487+
expect(inputEl.val()).toEqual('bar');
488+
expect(element).toBeClosed();
489+
});
490+
it('should not select a match when function returns false', function() {
491+
$scope.shouldSelectFn = function() {
492+
return false;
493+
};
494+
var element = prepareInputEl('<div><input ng-model="result" typeahead-should-select="shouldSelectFn($event)" uib-typeahead="item for item in source | filter:$viewValue"></div>');
495+
var inputEl = findInput(element);
496+
497+
changeInputValueTo(element, 'b');
498+
triggerKeyDown(element, 13);
499+
500+
// no change
501+
expect($scope.result).toEqual('b');
502+
expect(inputEl.val()).toEqual('b');
503+
});
504+
it('should pass key event into select trigger function', function() {
505+
$scope.shouldSelectFn = jasmine.createSpy('shouldSelectFn');//.and.returnValue(true);
506+
var element = prepareInputEl('<div><input ng-model="result" typeahead-should-select="shouldSelectFn($event)" uib-typeahead="item for item in source | filter:$viewValue"></div>');
507+
var inputEl = findInput(element);
508+
509+
changeInputValueTo(element, 'b');
510+
triggerKeyDown(element, 13);
511+
512+
expect($scope.shouldSelectFn.calls.count()).toEqual(1);
513+
expect($scope.shouldSelectFn.calls.argsFor(0)[0].which).toEqual(13);
514+
});
515+
});
516+
475517
describe('selecting a match', function() {
476518
it('should select a match on enter', function() {
477519
var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');

src/typeahead/typeahead.js

+25-17
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
4242
originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
4343
minLength = !newVal && newVal !== 0 ? 1 : newVal;
4444
});
45-
45+
4646
//minimal wait time after last character typed before typeahead kicks-in
4747
var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
4848

@@ -55,6 +55,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
5555
//binding to a variable that indicates if matches are being retrieved asynchronously
5656
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
5757

58+
//a function to determine if an event should cause selection
59+
var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) {
60+
var evt = vals.$event;
61+
return evt.which === 13 || evt.which === 9;
62+
};
63+
5864
//a callback executed when a match is selected
5965
var onSelectCallback = $parse(attrs.typeaheadOnSelect);
6066

@@ -370,13 +376,15 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
370376
return;
371377
}
372378

379+
var shouldSelect = isSelectEvent(originalScope, {$event: evt});
380+
373381
/**
374382
* if there's nothing selected (i.e. focusFirst) and enter or tab is hit
375383
* or
376384
* shift + tab is pressed to bring focus to the previous element
377385
* then clear the results
378386
*/
379-
if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13) || evt.which === 9 && !!evt.shiftKey) {
387+
if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) {
380388
resetMatches();
381389
scope.$digest();
382390
return;
@@ -385,36 +393,36 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
385393
evt.preventDefault();
386394
var target;
387395
switch (evt.which) {
388-
case 9:
389-
case 13:
390-
scope.$apply(function () {
391-
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
392-
$$debounce(function() {
393-
scope.select(scope.activeIdx, evt);
394-
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
395-
} else {
396-
scope.select(scope.activeIdx, evt);
397-
}
398-
});
399-
break;
400-
case 27:
396+
case 27: // escape
401397
evt.stopPropagation();
402398

403399
resetMatches();
404400
originalScope.$digest();
405401
break;
406-
case 38:
402+
case 38: // up arrow
407403
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
408404
scope.$digest();
409405
target = popUpEl.find('li')[scope.activeIdx];
410406
target.parentNode.scrollTop = target.offsetTop;
411407
break;
412-
case 40:
408+
case 40: // down arrow
413409
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
414410
scope.$digest();
415411
target = popUpEl.find('li')[scope.activeIdx];
416412
target.parentNode.scrollTop = target.offsetTop;
417413
break;
414+
default:
415+
if (shouldSelect) {
416+
scope.$apply(function() {
417+
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
418+
$$debounce(function() {
419+
scope.select(scope.activeIdx, evt);
420+
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
421+
} else {
422+
scope.select(scope.activeIdx, evt);
423+
}
424+
});
425+
}
418426
}
419427
});
420428

0 commit comments

Comments
 (0)