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

Feature minimum input length #336

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 68 additions & 46 deletions src/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
theme: 'bootstrap',
searchEnabled: true,
placeholder: '', // Empty by default, like HTML tag <select>
refreshDelay: 1000 // In milliseconds
refreshDelay: 1000, // In milliseconds
minimumInputLength: 0 // No minimum input length for search
})

// See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
Expand Down Expand Up @@ -152,7 +153,8 @@
ctrl.selected = undefined;
ctrl.open = false;
ctrl.focus = false;
ctrl.focusser = undefined; //Reference to input element used to handle focus events
ctrl.minimumInputLength = undefined; // Minimum input length before search is triggered
ctrl.focusser = undefined; //Reference to input element used to handle focus events
ctrl.disabled = undefined; // Initialized inside uiSelect directive link function
ctrl.searchEnabled = undefined; // Initialized inside uiSelect directive link function
ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function
Expand Down Expand Up @@ -204,6 +206,31 @@
})[0];
};

ctrl.refreshItems = function(items) {
if (items === undefined || items === null) {
// If the user specifies undefined or null => reset the collection
// Special case: items can be undefined if the user did not initialized the collection on the scope
// i.e $scope.addresses = [] is missing
ctrl.items = [];
} else {
if (!angular.isArray(items)) {
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
} else {
if (ctrl.search.length < ctrl.minimumInputLength) {
items = [];
}
if (ctrl.multiple){
//Remove already selected items (ex: while searching)
var filteredItems = items.filter(function(i) {return ctrl.selected.indexOf(i) < 0;});
ctrl.setItemsFn(filteredItems);
}else{
ctrl.setItemsFn(items);
}
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
}
}
};

ctrl.parseRepeatAttr = function(repeatAttr, groupByExp) {
function updateGroups(items) {
ctrl.groups = [];
Expand All @@ -228,48 +255,25 @@
ctrl.items = items;
}

var setItemsFn = groupByExp ? updateGroups : setPlainItems;
ctrl.setItemsFn = groupByExp ? updateGroups : setPlainItems;

ctrl.parserResult = RepeatParser.parse(repeatAttr);

ctrl.isGrouped = !!groupByExp;
ctrl.itemProperty = ctrl.parserResult.itemName;

// See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
$scope.$watchCollection(ctrl.parserResult.source, function(items) {

if (items === undefined || items === null) {
// If the user specifies undefined or null => reset the collection
// Special case: items can be undefined if the user did not initialized the collection on the scope
// i.e $scope.addresses = [] is missing
ctrl.items = [];
} else {
if (!angular.isArray(items)) {
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
} else {
if (ctrl.multiple){
//Remove already selected items (ex: while searching)
var filteredItems = items.filter(function(i) {return ctrl.selected.indexOf(i) < 0;});
setItemsFn(filteredItems);
}else{
setItemsFn(items);
}
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters

}
}

});
$scope.$watchCollection(ctrl.parserResult.source, ctrl.refreshItems);

if (ctrl.multiple){
//Remove already selected items
//Remove already selected items
$scope.$watchCollection('$select.selected', function(selectedItems){
var data = ctrl.parserResult.source($scope);
if (!selectedItems.length) {
setItemsFn(data);
ctrl.setItemsFn(data);
}else{
var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
setItemsFn(filteredItems);
ctrl.setItemsFn(filteredItems);
}
ctrl.sizeSearchInput();
});
Expand Down Expand Up @@ -308,7 +312,7 @@
};

ctrl.isDisabled = function(itemScope) {

if (!ctrl.open) return;

var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
Expand Down Expand Up @@ -348,7 +352,7 @@

// Closes the dropdown
ctrl.close = function(skipFocusser) {
if (!ctrl.open) return;
if (!ctrl.open) return;
_resetSearchInput();
ctrl.open = false;
if (!ctrl.multiple){
Expand Down Expand Up @@ -388,7 +392,7 @@
return ctrl.placeholder;
};

var containerSizeWatch;
var containerSizeWatch;
ctrl.sizeSearchInput = function(){
var input = _searchInput[0],
container = _searchInput.parent().parent()[0];
Expand All @@ -413,6 +417,14 @@
}, 0, false);
};

ctrl.noResultsMessage = function() {
if (ctrl.search.length < ctrl.minimumInputLength) {
var n = ctrl.minimumInputLength - ctrl.search.length;
return "Please enter " + n + " or more character" + (n == 1 ? "" : "s");
}
return "No matches found";
};

function _handleDropDownSelection(key) {
var processed = true;
switch (key) {
Expand Down Expand Up @@ -446,7 +458,7 @@
// Handles selected options in "multiple" mode
function _handleMatchSelection(key){
var caretPosition = _getCaretPosition(_searchInput[0]),
length = ctrl.selected.length,
length = ctrl.selected.length,
// none = -1,
first = 0,
last = length-1,
Expand All @@ -469,7 +481,7 @@
break;
case KEY.RIGHT:
// Open drop-down
if(!~ctrl.activeMatchIndex || curr === last){
if(!~ctrl.activeMatchIndex || curr === last){
ctrl.activate();
return false;
}
Expand All @@ -492,7 +504,7 @@
return curr;
}
else return false;
}
}
}

newIndex = getNewActiveMatchIndex();
Expand Down Expand Up @@ -523,7 +535,7 @@
if (!processed && ctrl.items.length > 0) {
processed = _handleDropDownSelection(key);
}

if (processed && key != KEY.TAB) {
//TODO Check si el tab selecciona aun correctamente
//Crear test
Expand Down Expand Up @@ -629,7 +641,7 @@

//From model --> view
ngModel.$formatters.unshift(function (inputValue) {
var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
locals = {},
result;
if (data){
Expand Down Expand Up @@ -681,7 +693,7 @@
if(attrs.tabindex){
//tabindex might be an expression, wait until it contains the actual value before we set the focusser tabindex
attrs.$observe('tabindex', function(value) {
//If we are using multiple, add tabindex to the search input
//If we are using multiple, add tabindex to the search input
if($select.multiple){
searchInput.attr("tabindex", value);
} else {
Expand Down Expand Up @@ -736,7 +748,7 @@
if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {
return;
}

$select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input
focusser.val('');
scope.$digest();
Expand All @@ -747,8 +759,13 @@


scope.$watch('searchEnabled', function() {
var searchEnabled = scope.$eval(attrs.searchEnabled);
$select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
var searchEnabled = scope.$eval(attrs.searchEnabled);
$select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
});

scope.$watch('minimumInputLength', function() {
var minimumInputLength = scope.$eval(attrs.minimumInputLength);
$select.minimumInputLength = minimumInputLength !== undefined ? minimumInputLength : uiSelectConfig.minimumInputLength;
});

attrs.$observe('disabled', function() {
Expand Down Expand Up @@ -865,7 +882,7 @@
if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");

return function link(scope, element, attrs, $select, transcludeFn) {

// var repeat = RepeatParser.parse(attrs.repeat);
var groupByExp = attrs.groupBy;

Expand Down Expand Up @@ -894,10 +911,15 @@

$compile(element, transcludeFn)(scope); //Passing current transcludeFn to be able to append elements correctly from uisTranscludeAppend

scope.$watch('$select.search', function(newValue) {
if(newValue && !$select.open && $select.multiple) $select.activate(false, true);
scope.$watch('$select.search', function(newValue, oldValue) {
if ($select.minimumInputLength > 0 && newValue !== oldValue) {
$select.refreshItems($select.parserResult.source(scope));
}
$select.activeIndex = 0;
$select.refresh(attrs.refresh);
if ($select.search.length >= $select.minimumInputLength) {
if (newValue && !$select.open && $select.multiple) $select.activate(false, true);
$select.refresh(attrs.refresh);
}
});

attrs.$observe('refreshDelay', function() {
Expand Down
72 changes: 70 additions & 2 deletions test/select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('ui-select tests', function() {
var $select = el.scope().$select;
$select.open = true;
scope.$digest();
};
}


// Tests
Expand Down Expand Up @@ -1444,7 +1444,6 @@ describe('ui-select tests', function() {

});


it('should run $formatters when changing model directly', function () {

scope.selection.selectedMultiple = ['[email protected]', '[email protected]'];
Expand Down Expand Up @@ -1533,4 +1532,73 @@ describe('ui-select tests', function() {
});

});

describe("min-input-length", function () {

function createUiSelectMinInputLength(len) {
return compileTemplate(
'<ui-select ng-model="selection.selected" \
minimum-input-length="' + len + '"> \
<ui-select-match> \
</ui-select-match> \
<ui-select-choices repeat="person in people | filter: $select.search" \
refresh="fetchFromServer($select.search)" refresh-delay="0"> \
<div ng-bind-html="person.name | highlight: $select.search"></div> \
<div ng-if="person.name==\'Wladimir\'"> \
<span class="only-once">I should appear only once</span>\
</div> \
</ui-select-choices> \
</ui-select>'
);
}

function partial(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
return fn.apply(null, args.concat(Array.prototype.slice.call(arguments, 0)));
}
}

function enterSearch(el, s, value) {
el.scope().$select.search = value;
s.$digest();
}

it("should call refresh when minimum length is setup", function () {
var el = createUiSelectMinInputLength(3);
var doSearch = partial(enterSearch, el, scope);

scope.fetchFromServer = function () {};
spyOn(scope, 'fetchFromServer');

doSearch('ad');
expect(scope.fetchFromServer).not.toHaveBeenCalled();

doSearch('ada');

$timeout.flush();

expect(scope.fetchFromServer.calls.count()).toEqual(1);
expect(scope.fetchFromServer).toHaveBeenCalledWith('ada');
});


it('should not call refresh when under min input length', function () {
var el = createUiSelectMinInputLength(3);
var doSearch = partial(enterSearch, el, scope);

scope.fetchFromServer = function () {};
spyOn(scope, 'fetchFromServer');

doSearch('ad');
doSearch('a');
doSearch('ad');
doSearch('');

$timeout.flush();

expect(scope.fetchFromServer).not.toHaveBeenCalled();
});

});
});