diff --git a/docs/assets/demo.js b/docs/assets/demo.js
index 9d6702c4c..c6580aafc 100644
--- a/docs/assets/demo.js
+++ b/docs/assets/demo.js
@@ -39,7 +39,13 @@ app.filter('propsFilter', function() {
return out;
};
});
+app.filter('reverseOrderFilterFn', function() {
+ return function(items) {
+ if(!angular.isArray(items)) return items;
+ return items.slice().reverse();
+ };
+});
app.controller('DemoCtrl', function ($scope, $http, $timeout, $interval) {
var vm = this;
@@ -86,10 +92,6 @@ app.controller('DemoCtrl', function ($scope, $http, $timeout, $interval) {
return item.name[0];
};
- vm.reverseOrderFilterFn = function(groups) {
- return groups.reverse();
- };
-
vm.personAsync = {selected : "wladimir@email.com"};
vm.peopleAsync = [];
diff --git a/docs/examples/demo-bootstrap.html b/docs/examples/demo-bootstrap.html
index 0785a3007..5d966f735 100644
--- a/docs/examples/demo-bootstrap.html
+++ b/docs/examples/demo-bootstrap.html
@@ -25,7 +25,7 @@
{{$select.selected.name}}
-
+
diff --git a/docs/examples/demo-group-by.html b/docs/examples/demo-group-by.html
index 986bec15c..303aab607 100644
--- a/docs/examples/demo-group-by.html
+++ b/docs/examples/demo-group-by.html
@@ -5,10 +5,10 @@
Group By
Selected: {{ctrl.person.selected}}
- Grouped using a string group-by="'country'"
+ Grouped using a string "group by person.country"
{{$select.selected.name}}
-
+
email: {{person.email}}
@@ -17,17 +17,17 @@ Grouped using a string group-by="'country'"
- Grouped using a function group-by="ctrl.someGroupFn"
+ Grouped using a function "group by ctrl.someGroupFn(person)"
{{$select.selected.name}}
-
+
email: {{person.email}}
age:
-
+
Regular
diff --git a/docs/examples/demo-group-filter.html b/docs/examples/demo-group-filter.html
index c3e727fd1..bd864e893 100644
--- a/docs/examples/demo-group-filter.html
+++ b/docs/examples/demo-group-filter.html
@@ -7,19 +7,37 @@ Group Filtering
Selected: {{country.selected}}
- Filter groups by array group-filter="['Z','B','C']"
+ Filter groups by name group by ctrl.firstLetterGroupFn(country) | filter:{name: 'Z'}
{{$select.selected.name}}
-
+
- Filter groups using a function group-filter="reverseOrderFilterFn"
+ Filter groups by string group by ctrl.firstLetterGroupFn(country) | uisGroupFilter:'Z'
{{$select.selected.name}}
-
+
+
+
+
+
+
+ Filter groups by array group by ctrl.firstLetterGroupFn(country) | uisGroupFilter:['A', 'B', 'C']
+
+ {{$select.selected.name}}
+
+
+
+
+
+
+ Filter groups using a function group by ctrl.firstLetterGroupFn(country) | reverseOrderFilterFn
+
+ {{$select.selected.name}}
+
diff --git a/docs/examples/demo-multiple-selection.html b/docs/examples/demo-multiple-selection.html
index 799c8430d..39ea0232b 100644
--- a/docs/examples/demo-multiple-selection.html
+++ b/docs/examples/demo-multiple-selection.html
@@ -62,7 +62,7 @@ Array of objects with single property binding
Array of objects (with groupBy)
{{$item.name}} <{{$item.email}}>
-
+
email: {{person.email}}
diff --git a/docs/examples/demo-select2-with-bootstrap.html b/docs/examples/demo-select2-with-bootstrap.html
index 98602e846..48ccb09e2 100644
--- a/docs/examples/demo-select2-with-bootstrap.html
+++ b/docs/examples/demo-select2-with-bootstrap.html
@@ -40,7 +40,7 @@
{{$select.selected.name}}
-
+
diff --git a/docs/examples/demo-selectize-with-bootstrap.html b/docs/examples/demo-selectize-with-bootstrap.html
index cf7528c59..ff2e2b4aa 100644
--- a/docs/examples/demo-selectize-with-bootstrap.html
+++ b/docs/examples/demo-selectize-with-bootstrap.html
@@ -45,7 +45,7 @@
{{$select.selected.name}}
-
+
diff --git a/docs/examples/demo-tagging.html b/docs/examples/demo-tagging.html
index a6ae2eb6c..3b963af84 100644
--- a/docs/examples/demo-tagging.html
+++ b/docs/examples/demo-tagging.html
@@ -31,7 +31,7 @@ Simple String Tags (Predictive Search Model & No Labels)<
Object Tags (with grouping)
{{$item.name}} <{{$item.email}}>
-
+
diff --git a/src/common.js b/src/common.js
index 45d88acfc..3ad8beab4 100644
--- a/src/common.js
+++ b/src/common.js
@@ -128,7 +128,26 @@ var uis = angular.module('ui.select', [])
}
};
})
+.filter('uisGroupFilter', ['$filter', function($filter) {
+ return function(groups, definition) {
+ if(!angular.isArray(groups)) return groups;
+
+ var filterDefintion = [];
+
+ if(angular.isString(definition)) {
+ filterDefintion = [definition];
+ } else if(angular.isArray(definition)) {
+ filterDefintion = definition;
+ }
+
+ if(filterDefintion.length === 0) return groups;
+
+ return $filter('filter')(groups, function(group) {
+ return filterDefintion.indexOf(group.name) > -1;
+ });
+ };
+}])
/**
* Highlights text that matches $select.search.
*
diff --git a/src/uiSelectChoicesDirective.js b/src/uiSelectChoicesDirective.js
index a1c3967c7..50067d192 100644
--- a/src/uiSelectChoicesDirective.js
+++ b/src/uiSelectChoicesDirective.js
@@ -21,23 +21,20 @@ uis.directive('uiSelectChoices',
if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
// var repeat = RepeatParser.parse(attrs.repeat);
- var groupByExp = tAttrs.groupBy;
- var groupFilterExp = tAttrs.groupFilter;
+ var parserResult = RepeatParser.parse(tAttrs.repeat);
- if (groupByExp) {
+ if (parserResult.groupByExp) {
var groups = tElement.querySelectorAll('.ui-select-choices-group');
if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length);
- groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression());
+ groups.attr('ng-repeat', '$group in $select.groups ' + ( parserResult.groupByFilter || ''));
}
- var parserResult = RepeatParser.parse(tAttrs.repeat);
-
var choices = tElement.querySelectorAll('.ui-select-choices-row');
if (choices.length !== 1) {
throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
}
- choices.attr('ng-repeat', parserResult.repeatExpression(groupByExp))
+ choices.attr('ng-repeat', parserResult.repeatExpression(parserResult.groupByExp))
.attr('ng-if', '$select.open'); //Prevent unnecessary watches when dropdown is closed
@@ -54,7 +51,7 @@ uis.directive('uiSelectChoices',
return function link(scope, element, attrs, $select) {
- $select.parseRepeatAttr(attrs.repeat, groupByExp, groupFilterExp); //Result ready at $select.parserResult
+ $select.parseRepeatAttr(attrs.repeat); //Result ready at $select.parserResult
$select.disableChoiceExpression = attrs.uiDisableChoice;
$select.onHighlightCallback = attrs.onHighlight;
diff --git a/src/uiSelectController.js b/src/uiSelectController.js
index b7dc2801c..f810b18dc 100644
--- a/src/uiSelectController.js
+++ b/src/uiSelectController.js
@@ -94,17 +94,17 @@ uis.controller('uiSelectCtrl',
}
}
- function _groupsFilter(groups, groupNames) {
- var i, j, result = [];
- for(i = 0; i < groupNames.length ;i++){
- for(j = 0; j < groups.length ;j++){
- if(groups[j].name == [groupNames[i]]){
- result.push(groups[j]);
- }
- }
- }
- return result;
- }
+ // function _groupsFilter(groups, groupNames) {
+ // var i, j, result = [];
+ // for(i = 0; i < groupNames.length ;i++){
+ // for(j = 0; j < groups.length ;j++){
+ // if(groups[j].name == [groupNames[i]]){
+ // result.push(groups[j]);
+ // }
+ // }
+ // }
+ // return result;
+ // }
// When the user clicks on ui-select, displays the dropdown list
ctrl.activate = function(initSearchValue, avoidReset) {
@@ -169,43 +169,43 @@ uis.controller('uiSelectCtrl',
})[0];
};
- ctrl.parseRepeatAttr = function(repeatAttr, groupByExp, groupFilterExp) {
+ ctrl.parseRepeatAttr = function(repeatAttr) {
+
function updateGroups(items) {
- var groupFn = $scope.$eval(groupByExp);
ctrl.groups = [];
- angular.forEach(items, function(item) {
- var groupName = angular.isFunction(groupFn) ? groupFn(item) : item[groupFn];
- var group = ctrl.findGroupByName(groupName);
- if(group) {
- group.items.push(item);
- }
- else {
- ctrl.groups.push({name: groupName, items: [item]});
+
+ var groupingFn = ctrl.parserResult.getGroupingFn($scope);
+
+ angular.forEach(items, function (item) {
+ var itemGroupName = groupingFn(item);
+ var group = ctrl.findGroupByName(itemGroupName);
+ if (!group) {
+ group = { name: itemGroupName, items: [] };
+ ctrl.groups.push(group);
}
+
+ group.items.push(item);
});
- if(groupFilterExp){
- var groupFilterFn = $scope.$eval(groupFilterExp);
- if( angular.isFunction(groupFilterFn)){
- ctrl.groups = groupFilterFn(ctrl.groups);
- } else if(angular.isArray(groupFilterFn)){
- ctrl.groups = _groupsFilter(ctrl.groups, groupFilterFn);
- }
+
+ if(ctrl.parserResult.groupByFilter) {
+ // Prevent filtered groups from adding to ctrl.items
+ var filteredGroups = $scope.$eval('$select.groups ' + ctrl.parserResult.groupByFilter);
+ var filteredItems = filteredGroups.map(function(g) { return g.items; });
+ ctrl.items = [].concat.apply([], filteredItems);
+ } else {
+ ctrl.items = items;
}
- ctrl.items = [];
- ctrl.groups.forEach(function(group) {
- ctrl.items = ctrl.items.concat(group.items);
- });
}
function setPlainItems(items) {
ctrl.items = items;
}
- ctrl.setItemsFn = groupByExp ? updateGroups : setPlainItems;
-
ctrl.parserResult = RepeatParser.parse(repeatAttr);
+ ctrl.isGrouped = !!ctrl.parserResult.groupByExp;
+
+ ctrl.setItemsFn = ctrl.isGrouped ? updateGroups : setPlainItems;
- ctrl.isGrouped = !!groupByExp;
ctrl.itemProperty = ctrl.parserResult.itemName;
//If collection is an Object, convert it to Array
diff --git a/src/uisRepeatParserService.js b/src/uisRepeatParserService.js
index 58ffa19c6..7c8e70871 100644
--- a/src/uisRepeatParserService.js
+++ b/src/uisRepeatParserService.js
@@ -26,8 +26,8 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE
// If an array is used as collection
// if (isObjectCollection){
- // 000000000000000000000000000000111111111000000000000000222222222222220033333333333333333333330000444444444444444444000000000000000055555555555000000000000000000000066666666600000000
- match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(\s*[\s\S]+?)?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+ // 0000000000000000000000000000001111111110000000000000002222222222222200333333333333333333333300004444444444444444440000000000000000555555555550000000000000000000000666666666000000000000000000000777777777000
+ match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(\s*[\s\S]+?)?(?:\s+track\s+by\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s*$/);
// 1 Alias
// 2 Item
@@ -35,9 +35,10 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE
// 4 Value on (key,value)
// 5 Source expression (including filters)
// 6 Track by
+ // 7 Group by expression (including filters)
if (!match) {
- throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
+ throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_][ group by _property_or_func_[ |_group_filter_exp]] ]' but got '{0}'.",
expression);
}
@@ -58,12 +59,40 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE
}
}
+ var itemName = match[4] || match[2];
+
+ var groupByExp = match[7],
+ groupByFilters = '',
+ getGroupingFn;
+
+ if(match[7]) {
+ // match all after | but not after ||
+ var groupFilterMatch = match[7].match(/^\s*(?:[\s\S]+?)(?:[^\|]|\|\|)+([\s\S]*)\s*$/);
+ if(groupFilterMatch && groupFilterMatch[1].trim()) {
+ // TODO: Consider checking for enclosing parenthesis?
+ groupByFilters = groupFilterMatch[1];
+ groupByExp = groupByExp.replace(groupByFilters, '').trim();
+ }
+ var parsedGroupingExp = $parse(groupByExp);
+ // Creates a getter function that can run against an arbitary item
+ getGroupingFn = function($scope) {
+ return function(item) {
+ var locals = {};
+ locals[itemName] = item;
+ return parsedGroupingExp($scope, locals);
+ };
+ };
+ }
+
return {
- itemName: match[4] || match[2], // (lhs) Left-hand side,
+ itemName: itemName, // (lhs) Left-hand side,
keyName: match[3], //for (key, value) syntax
source: $parse(source),
filters: filters,
trackByExp: match[6],
+ groupByExp: groupByExp,
+ groupByFilter: groupByFilters,
+ getGroupingFn: getGroupingFn,
modelMapper: $parse(match[1] || match[4] || match[2]),
repeatExpression: function (grouped) {
var expression = this.itemName + ' in ' + (grouped ? '$group.items' : '$select.items');
@@ -75,9 +104,4 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE
};
};
-
- self.getGroupNgRepeatExpression = function() {
- return '$group in $select.groups';
- };
-
}]);
diff --git a/test/select.spec.js b/test/select.spec.js
index 5b735689f..9cf8e4f9c 100644
--- a/test/select.spec.js
+++ b/test/select.spec.js
@@ -69,12 +69,21 @@ describe('ui-select tests', function() {
beforeEach(module('ngSanitize', 'ui.select', 'wrapperDirective', 'testValidator'));
beforeEach(function() {
- module(function($provide) {
+ module(function($provide, $filterProvider) {
$provide.factory('uisOffset', function() {
return function(el) {
return {top: 100, left: 200, width: 300, height: 400};
};
});
+
+ $filterProvider.register('invertOrder', function() {
+ return function(groups) {
+ if(!angular.isArray(groups)) return;
+ return groups.slice().sort(function(groupA, groupB){
+ return groupA.name.toLocaleLowerCase() < groupB.name.toLocaleLowerCase();
+ });
+ };
+ });
});
});
@@ -87,17 +96,10 @@ describe('ui-select tests', function() {
uisRepeatParser = _uisRepeatParser_;
scope.selection = {};
- scope.getGroupLabel = function(person) {
+ scope.getGroupByLabel = function(person) {
return person.age % 2 ? 'even' : 'odd';
};
- scope.filterInvertOrder = function(groups) {
- return groups.sort(function(groupA, groupB){
- return groupA.name.toLocaleLowerCase() < groupB.name.toLocaleLowerCase();
- });
- };
-
-
scope.people = [
{ name: 'Adam', email: 'adam@email.com', group: 'Foo', age: 12 },
{ name: 'Amalie', email: 'amalie@email.com', group: 'Foo', age: 12 },
@@ -384,6 +386,70 @@ describe('ui-select tests', function() {
});
+ describe("parsing group by expresson", function() {
+ it('should parse a simple expression', function() {
+
+ var parserResult = uisRepeatParser.parse('person in people group by person.country');
+ expect(parserResult.itemName).toBe('person');
+ expect(parserResult.groupByExp).toBe('person.country');
+ });
+
+ it('should parse simple grouping function expression', function() {
+
+ var parserResult = uisRepeatParser.parse('person in people group by someGroupingFn(person)');
+ expect(parserResult.itemName).toBe('person');
+ expect(parserResult.groupByExp).toBe('someGroupingFn(person)');
+ });
+
+ it('should parse any grouping filters', function() {
+
+ var parserResult = uisRepeatParser.parse('person in people group by person.country | filter:{name: "usa"}');
+ expect(parserResult.itemName).toBe('person');
+ expect(parserResult.groupByExp).toBe('person.country');
+ expect(parserResult.groupByFilter).toBe('| filter:{name: "usa"}');
+ });
+
+ it('should parse fallback grouping expression', function() {
+
+ var parserResult = uisRepeatParser.parse('person in people group by (person.firstName || person.lastName)');
+ expect(parserResult.itemName).toBe('person');
+ expect(parserResult.groupByExp).toBe('(person.firstName || person.lastName)');
+ });
+
+ it('should parse group by in a complex expression', function() {
+
+ var parserResult = uisRepeatParser.parse('person.name as person in (peopleNothing || people) track by person.id group by person.country');
+ expect(parserResult.itemName).toBe('person');
+ expect(parserResult.groupByExp).toBe('person.country');
+ });
+
+ it('should get group name using simple group by property', function() {
+
+ var person = {name: 'Wladimir', country: 'usa'};
+
+ var parserResult = uisRepeatParser.parse('person in people group by person.country');
+ var groupingFn = parserResult.getGroupingFn({});
+ expect(groupingFn(person)).toBe('usa');
+ });
+
+ it('should call grouping function if specified', function() {
+ var repeatScope = { someGroupingFn: function(item) {
+ // group by reversed country (just so we know the function has been called)
+ return item.country.split('').reverse().join('');
+ }};
+
+ var person = {name: 'Wladimir', country: 'usa'};
+
+ var parserResult = uisRepeatParser.parse('person in people group by someGroupingFn(person)');
+ var groupingFn = parserResult.getGroupingFn(repeatScope);
+ expect(groupingFn(person)).toBe('asu');
+ });
+ });
+
+
+
+
+
it('should not leak memory', function() {
var cacheLenght = Object.keys(angular.element.cache).length;
createUiSelect().remove();
@@ -974,7 +1040,7 @@ describe('ui-select tests', function() {
return compileTemplate(
' \
{{$select.selected.name}} \
- \
+ \
\
\
\
@@ -1026,7 +1092,7 @@ describe('ui-select tests', function() {
return compileTemplate(
' \
{{$select.selected.name}} \
- \
+ \
\
\
'
@@ -1045,7 +1111,7 @@ describe('ui-select tests', function() {
return compileTemplate('\
\
{{$select.selected.name}} \
- \
+ \
\
\
'
@@ -1064,8 +1130,7 @@ describe('ui-select tests', function() {
return compileTemplate('\
\
{{$select.selected.name}} \
- \
+ \
\
\
'
@@ -1109,7 +1174,7 @@ describe('ui-select tests', function() {
\
'
);
- }).toThrow(new Error('[ui.select:iexp] Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'incorrect format people\'.'));
+ }).toThrow();
});
it('should throw when no ui-select-match found', function() {
@@ -1745,6 +1810,7 @@ describe('ui-select tests', function() {
function createUiSelectMultiple(attrs) {
var attrsHtml = '',
+ groupByHtml = '',
choicesAttrsHtml = '',
matchesAttrsHtml = '';
if (attrs !== undefined) {
@@ -1756,14 +1822,14 @@ describe('ui-select tests', function() {
if (attrs.taggingTokens !== undefined) { attrsHtml += ' tagging-tokens="' + attrs.taggingTokens + '"'; }
if (attrs.taggingLabel !== undefined) { attrsHtml += ' tagging-label="' + attrs.taggingLabel + '"'; }
if (attrs.inputId !== undefined) { attrsHtml += ' input-id="' + attrs.inputId + '"'; }
- if (attrs.groupBy !== undefined) { choicesAttrsHtml += ' group-by="' + attrs.groupBy + '"'; }
+ if (attrs.groupBy !== undefined) { groupByHtml += ' group by ' + attrs.groupBy; }
if (attrs.lockChoice !== undefined) { matchesAttrsHtml += ' ui-lock-choice="' + attrs.lockChoice + '"'; }
}
return compileTemplate(
' \
{{$item.name}} <{{$item.email}}> \
- \
+ \
\
\
\
@@ -2509,7 +2575,7 @@ describe('ui-select tests', function() {
expect(el.find('.ui-select-choices-row-inner').size()).toBe(0);
});
- it('should allow creating tag in multi select mode with tagging and group-by enabled', function() {
+ it('should allow creating tag in multi select mode with tagging and group by enabled', function() {
scope.taggingFunc = function (name) {
return {
name: name,
@@ -2519,7 +2585,7 @@ describe('ui-select tests', function() {
};
};
- var el = createUiSelectMultiple({tagging: 'taggingFunc', groupBy: "'age'"});
+ var el = createUiSelectMultiple({tagging: 'taggingFunc', groupBy: "person.age"});
showChoicesForSearch(el, 'amal');
expect(el.find('.ui-select-choices-row-inner').size()).toBe(2);
@@ -2880,4 +2946,36 @@ describe('ui-select tests', function() {
});
});
+ describe('uisGroupFilter filter', function() {
+ var groupFilter;
+ var testArray = [
+ {name: 'group1'},
+ {name: 'group2'},
+ {name: 'group3'},
+ {name: 'group4'},
+ {name: 'group5'}
+ ];
+
+ beforeEach(function() {
+ groupFilter = $injector.get('uisGroupFilterFilter');
+ });
+
+ it('should return all items if no filter arguments are give', function() {
+ expect(groupFilter(testArray)).toEqual(testArray);
+ });
+
+ it('should, if given a string, return only groups with that matching name property', function () {
+ expect(groupFilter(testArray, 'group1')).toEqual([{ name: 'group1' }]);
+ });
+
+ it('should, if given an array, return any groups with a name property specified in the array', function () {
+ expect(groupFilter(testArray, ['group1', 'group2'])).toEqual([{ name: 'group1' }, { name: 'group2' }]);
+ });
+
+ it('should, if given an array, return any groups with a name property specified in the array (incl. dupes)', function () {
+ var testArray2 = testArray.slice();
+ testArray2.push({ name: 'group2' });
+ expect(groupFilter(testArray2, ['group1', 'group2'])).toEqual([{ name: 'group1' }, { name: 'group2' }, { name: 'group2' }]);
+ });
+ });
});