Skip to content
This repository has been archived by the owner on Nov 22, 2021. It is now read-only.

Commit

Permalink
feat(autocomplete): Implemented debounce delay
Browse files Browse the repository at this point in the history
Added a debounce delay option to the autocomplete directive so it
doesn't call the source function too many times within a short period
of time.

Closes #19.
  • Loading branch information
mbenford committed Nov 30, 2013
1 parent d62909f commit 1a6527f
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 40 deletions.
59 changes: 41 additions & 18 deletions src/auto-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,37 @@
* @description
* Provides autocomplete support for the tagsInput directive.
*
* @param {expression} source Callback that will be called for every keystroke and will be provided with the current
* input's value. Must return a promise.
* @param {expression} source Expression to evaluate upon changing the input content. The input value is available as $text.
* The result of the expression must be a promise that resolves to an array of strings.
*/
angular.module('tags-input').directive('autoComplete', function($document) {
function SuggestionList(loadFn) {
var self = {};
angular.module('tags-input').directive('autoComplete', function($document, $interpolate, $timeout) {
function initializeOptions(scope, attrs, options) {
var converters = {};
converters[String] = function(value) { return value; };
converters[Number] = function(value) { return parseInt(value, 10); };
converters[Boolean] = function(value) { return value === 'true'; };
converters[RegExp] = function(value) { return new RegExp(value); };

scope.options = {};

angular.forEach(options, function(value, key) {
var interpolatedValue = attrs[key] && $interpolate(attrs[key])(scope.$parent),
converter = converters[options[key].type];

scope.options[key] = interpolatedValue ? converter(interpolatedValue) : options[key].defaultValue;
});
}

function SuggestionList(loadFn, options) {
var self = {}, debouncedLoadId;

self.reset = function() {
self.items = [];
self.visible = false;
self.index = -1;
self.selected = null;

$timeout.cancel(debouncedLoadId);
};
self.show = function() {
self.selected = null;
Expand All @@ -29,16 +48,15 @@ angular.module('tags-input').directive('autoComplete', function($document) {
self.visible = false;
};
self.load = function(text) {
if (self.selected === text) {
return;
}

loadFn({ $text: text }).then(function(items) {
self.items = items;
if (items.length > 0) {
self.show();
}
});
$timeout.cancel(debouncedLoadId);
debouncedLoadId = $timeout(function() {
loadFn({ $text: text }).then(function(items) {
self.items = items;
if (items.length > 0) {
self.show();
}
});
}, options.debounceDelay, false);
};
self.selectNext = function() {
self.select(++self.index);
Expand Down Expand Up @@ -76,10 +94,15 @@ angular.module('tags-input').directive('autoComplete', function($document) {
'</div>',
link: function(scope, element, attrs, tagsInputCtrl) {
var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down],
suggestionList = new SuggestionList(scope.source),
suggestionList, tagsInput, input;

initializeOptions(scope, attrs, {
debounceDelay: { type: Number, defaultValue: 100 }
});

tagsInput = tagsInputCtrl.registerAutocomplete(),
input = tagsInput.input;
suggestionList = new SuggestionList(scope.source, scope.options);
tagsInput = tagsInputCtrl.registerAutocomplete();
input = tagsInput.input;

scope.suggestionList = suggestionList;

Expand Down
102 changes: 81 additions & 21 deletions test/auto-complete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
'use strict';

describe('autocomplete-directive', function () {
var $compile, $scope, $q,
parentCtrl, element, input, suggestionList, deferred, inputChangeHandler, onTagAddedHandler;
var $compile, $scope, $q, $timeout,
parentCtrl, element, isolateScope, input, suggestionList, deferred, inputChangeHandler, onTagAddedHandler;

beforeEach(function () {
module('tags-input');

inject(function($rootScope, _$compile_, _$q_) {
inject(function($rootScope, _$compile_, _$q_, _$timeout_) {
$scope = $rootScope;
$compile = _$compile_;
$q = _$q_;
$timeout = _$timeout_;
});

deferred = $q.defer();
Expand All @@ -21,7 +22,7 @@ describe('autocomplete-directive', function () {
});

function compile() {
var parent, tagsInput;
var parent, tagsInput, options;

input = angular.element('<input type="text">');
input.changeValue = jasmine.createSpy();
Expand All @@ -39,13 +40,15 @@ describe('autocomplete-directive', function () {

spyOn(parentCtrl, 'registerAutocomplete').andReturn(tagsInput);

element = angular.element('<auto-complete source="loadItems($text)"></auto-complete>');
options = jQuery.makeArray(arguments).join(' ');
element = angular.element('<auto-complete source="loadItems($text)" ' + options + '></auto-complete>');
parent.append(element);

$compile(element)($scope);
$scope.$digest();

suggestionList = element.isolateScope().suggestionList;

isolateScope = element.isolateScope();
suggestionList = isolateScope.suggestionList;
}

function resolve(items) {
Expand Down Expand Up @@ -87,6 +90,7 @@ describe('autocomplete-directive', function () {

function loadSuggestions(items) {
suggestionList.load('');
$timeout.flush();
resolve(items);
}

Expand Down Expand Up @@ -223,25 +227,12 @@ describe('autocomplete-directive', function () {
suggestionList.select(0);

// Act
element.isolateScope().addSuggestion();
isolateScope.addSuggestion();

// Assert
expect(suggestionList.selected).toBeNull();
});

it('calls the load function for every key pressed passing the input content', function() {
// Act
changeInputValue('A');
changeInputValue('AB');
changeInputValue('ABC');

// Assert
expect($scope.loadItems.callCount).toBe(3);
expect($scope.loadItems.calls[0].args[0]).toBe('A');
expect($scope.loadItems.calls[1].args[0]).toBe('AB');
expect($scope.loadItems.calls[2].args[0]).toBe('ABC');
});

it('does not call the load function after adding the selected suggestion to the input field', function() {
// Arrange
loadSuggestions(['Item1', 'Item2']);
Expand Down Expand Up @@ -500,6 +491,75 @@ describe('autocomplete-directive', function () {
expect(event.isPropagationStopped()).toBe(false);
});
});

describe('debounce-delay option', function () {
it('doesn\'t call the load function immediately', function () {
// Arrange
compile('debounce-delay="100"');

// Act
changeInputValue('A');
changeInputValue('AB');
changeInputValue('ABC');

// Assert
expect($scope.loadItems).not.toHaveBeenCalled();
});

it('calls the load function only after a delay has passed', function() {
// Arrange
compile('debounce-delay="100"');

// Act
changeInputValue('A');
changeInputValue('AB');
changeInputValue('ABC');

$timeout.flush();

// Assert
expect($scope.loadItems).toHaveBeenCalledWith('ABC');
});

it('doesn\'t call the load function when the reset method is called', function() {
// Arrange
compile();
changeInputValue('A');

// Act
suggestionList.reset();
$timeout.flush();

// Assert
expect($scope.loadItems).not.toHaveBeenCalled();

});

it('initializes the option to 100 milliseconds', function () {
// Arrange/Act
compile();

// Assert
expect(isolateScope.options.debounceDelay).toBe(100);
});

it('sets the option given a static string', function() {
// Arrange/Act
compile('debounce-delay="1000"');

// Assert
expect(isolateScope.options.debounceDelay).toBe(1000);
});

it('sets the option given an interpolated string', function() {
// Arrange/Act
$scope.value = 1000;
compile('debounce-delay="{{ value }}"');

// Assert
expect(isolateScope.options.debounceDelay).toBe(1000);
});
});
});

})();
3 changes: 2 additions & 1 deletion test/test-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</head>
<body ng-controller="Ctrl">
<tags-input ng-model="tags" placeholder="{{ placeholder.value }}">
<auto-complete source="loadItems"></auto-complete>
<auto-complete source="loadItems($text)" debounce-delay="100"></auto-complete>
</tags-input>

<script type="text/javascript">
Expand All @@ -17,6 +17,7 @@
$scope.tags = ['some', 'cool', 'tags'];
$scope.placeholder = {value: "New tag" };
$scope.loadItems = function(text) {
console.log(text);
var deferred = $q.defer();
deferred.resolve(['Item1', 'Item2', 'Item3']);
return deferred.promise;
Expand Down

0 comments on commit 1a6527f

Please sign in to comment.