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

Commit

Permalink
feat(typeahead): add ng-model-options debounce support
Browse files Browse the repository at this point in the history
- Adds support for ng-model-options `debounce` option

Closes #4982
  • Loading branch information
wesleycho committed Nov 30, 2015
1 parent 6094e07 commit bd47f6c
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 10 deletions.
4 changes: 4 additions & 0 deletions src/typeahead/docs/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ <h4>Asynchronous results</h4>
<i class="glyphicon glyphicon-remove"></i> No Results Found
</div>

<h4>ngModelOptions support</h4>
<pre>Model: {{ngModelOptionsSelected | json}}</pre>
<input type="text" ng-model="ngModelOptionsSelected" ng-model-options="modelOptions" uib-typeahead="state for state in states | filter:$viewValue | limitTo:8" class="form-control">

<h4>Custom templates for results</h4>
<pre>Model: {{customSelected | json}}</pre>
<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">
Expand Down
18 changes: 18 additions & 0 deletions src/typeahead/docs/demo.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/typeahead/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ The typeahead directives provide several attributes:
:
Assignable angular expression to data-bind to

* `ng-model-options`
:
Options for ng-model (see [ng-model-options directive](https://docs.angularjs.org/api/ng/directive/ngModelOptions)). Currently supports the `debounce` and `getterSetter` options

* `uib-typeahead` <i class="glyphicon glyphicon-eye-open"></i>
:
Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select))
Expand Down
111 changes: 110 additions & 1 deletion src/typeahead/test/typeahead.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -947,8 +947,10 @@ describe('typeahead tests', function() {

expect($scope.test.typeahead.$error.parse).toBeUndefined();
});
});

it('issue #3823 - should support ng-model-options getterSetter', function() {
describe('ng-model-options', function() {
it('should support getterSetter', function() {
function resultSetter(state) {
return state;
}
Expand All @@ -960,6 +962,113 @@ describe('typeahead tests', function() {

expect($scope.result).toBe(resultSetter);
});

describe('debounce as a number', function() {
it('should work with selecting via keyboard', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');
triggerKeyDown(element, 13);

expect($scope.result).not.toBe('Alaska');

$timeout.flush(400);

expect($scope.result).toBe('Alaska');
});

it('should work with select on exact', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');

expect($scope.result).not.toBe('Alaska');

$timeout.flush(400);

expect($scope.result).toBe('Alaska');
});

it('should work with selecting a match via click', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');
var match = $(findMatches(element)[0]).find('a')[0];

$(match).click();
$scope.$digest();

expect($scope.result).not.toBe('Alaska');

$timeout.flush(400);

expect($scope.result).toBe('Alaska');
});
});

describe('debounce as an object', function() {
it('should work with selecting via keyboard', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');
triggerKeyDown(element, 13);

expect($scope.result).not.toBe('Alaska');

$timeout.flush(400);

expect($scope.result).toBe('Alaska');
});

it('should work with select on exact', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');

expect($scope.result).not.toBe('Alaska');

$timeout.flush(400);

expect($scope.result).toBe('Alaska');
});

it('should work with selecting a match via click', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');
var match = $(findMatches(element)[0]).find('a')[0];

$(match).click();
$scope.$digest();

expect($scope.result).not.toBe('Alaska');

$timeout.flush(400);

expect($scope.result).toBe('Alaska');
});

it('should work when blurring and select on blur', function() {
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>');
var inputEl = findInput(element);

changeInputValueTo(element, 'Alaska');
element.blur();
$scope.$digest();

expect($scope.result).not.toBe('Alaska');

$timeout.flush(500);

expect($scope.result).toBe('Alaska');
});
});
});

describe('input formatting', function() {
Expand Down
47 changes: 38 additions & 9 deletions src/typeahead/typeahead.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
'move-in-progress': 'moveInProgress',
query: 'query',
position: 'position',
'assign-is-open': 'assignIsOpen(isOpen)'
'assign-is-open': 'assignIsOpen(isOpen)',
debounce: 'debounceUpdate'
});
//custom item template
if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
Expand Down Expand Up @@ -235,7 +236,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap

//Select the single remaining option if user input matches
if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
scope.select(0);
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
$$debounce(function() {
scope.select(0);
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
} else {
scope.select(0);
}
}

if (showHint) {
Expand Down Expand Up @@ -319,7 +326,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
resetMatches();

scope.assignIsOpen = function (isOpen) {
isOpenSetter(originalScope, isOpen);
isOpenSetter(originalScope, isOpen);
};

scope.select = function(activeIdx) {
Expand Down Expand Up @@ -369,7 +376,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
case 9:
case 13:
scope.$apply(function () {
scope.select(scope.activeIdx);
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
$$debounce(function() {
scope.select(scope.activeIdx);
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
} else {
scope.select(scope.activeIdx);
}
});
break;
case 27:
Expand Down Expand Up @@ -402,7 +415,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
selected = true;
scope.$apply(function() {
scope.select(scope.activeIdx);
if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
$$debounce(function() {
scope.select(scope.activeIdx);
}, scope.debounceUpdate.blur);
} else {
scope.select(scope.activeIdx);
}
});
}
if (!isEditable && modelCtrl.$error.editable) {
Expand Down Expand Up @@ -459,6 +478,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
modelCtrl = _modelCtrl;
ngModelOptions = _ngModelOptions;

scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope);

//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
modelCtrl.$parsers.unshift(function(inputValue) {
Expand Down Expand Up @@ -529,7 +550,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
};
})

.directive('uibTypeaheadPopup', function() {
.directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
return {
scope: {
matches: '=',
Expand All @@ -538,7 +559,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
position: '&',
moveInProgress: '=',
select: '&',
assignIsOpen: '&'
assignIsOpen: '&',
debounce: '&'
},
replace: true,
templateUrl: function(element, attrs) {
Expand All @@ -562,11 +584,18 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap
};

scope.selectMatch = function(activeIdx) {
scope.select({activeIdx: activeIdx});
var debounce = scope.debounce();
if (angular.isNumber(debounce) || angular.isObject(debounce)) {
$$debounce(function() {
scope.select({activeIdx: activeIdx});
}, angular.isNumber(debounce) ? debounce : debounce['default']);
} else {
scope.select({activeIdx: activeIdx});
}
};
}
};
})
}])

.directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
return {
Expand Down

0 comments on commit bd47f6c

Please sign in to comment.