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

feat(groupBy): Define group by expression in repeat #1715

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
10 changes: 6 additions & 4 deletions docs/assets/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 : "[email protected]"};
vm.peopleAsync = [];

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/demo-bootstrap.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

<ui-select ng-model="ctrl.person.selected" theme="bootstrap">
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="'country'" repeat="item in ctrl.people | filter: $select.search">
<ui-select-choices repeat="item in ctrl.people | filter: $select.search group by item.country">
<span ng-bind-html="item.name | highlight: $select.search"></span>
<small ng-bind-html="item.email | highlight: $select.search"></small>
</ui-select-choices>
Expand Down
10 changes: 5 additions & 5 deletions docs/examples/demo-group-by.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<h1>Group By</h1>
<p>Selected: {{ctrl.person.selected}}</p>

<h3>Grouped using a string <small><code>group-by="'country'"</code></small></h3>
<h3>Grouped using a string <small><code>"group by person.country"</code></small></h3>
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="'country'" repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search} group by person.country">
<div ng-bind-html="person.name | highlight: $select.search"></div>
<small>
email: {{person.email}}
Expand All @@ -17,17 +17,17 @@ <h3>Grouped using a string <small><code>group-by="'country'"</code></small></h3>
</ui-select-choices>
</ui-select>

<h3>Grouped using a function <small><code>group-by="ctrl.someGroupFn"</code></small></h3>
<h3>Grouped using a function <small><code>"group by ctrl.someGroupFn(person)"</code></small></h3>
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="ctrl.someGroupFn" repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search} group by ctrl.someGroupFn(person)">
<div ng-bind-html="person.name | highlight: $select.search"></div>
<small>
email: {{person.email}}
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
</small>
</ui-select-choices>
</ui-select>
</ui-select>

<h3>Regular</h3>
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
Expand Down
26 changes: 22 additions & 4 deletions docs/examples/demo-group-filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,37 @@ <h1>Group Filtering</h1>
<p>Selected: {{country.selected}}</p>


<h3> Filter groups by array <small><code>group-filter="['Z','B','C']"</code></small></h3>
<h3> Filter groups by name <small><code>group by ctrl.firstLetterGroupFn(country) | filter:{name: 'Z'}</code></small></h3>
<ui-select ng-model="ctrl.country.selected" theme="select2" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="ctrl.firstLetterGroupFn" group-filter="['Z','B','C']" repeat="country in ctrl.countries | filter: $select.search">
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search group by ctrl.firstLetterGroupFn(country) | filter:{name: 'Z'}">
<span ng-bind-html="country.name | highlight: $select.search"></span>
<small ng-bind-html="country.code | highlight: $select.search"></small>
</ui-select-choices>
</ui-select>

<h3>Filter groups using a function <small><code>group-filter="reverseOrderFilterFn"</code></small></h3>
<h3> Filter groups by string <small><code>group by ctrl.firstLetterGroupFn(country) | uisGroupFilter:'Z'</code></small></h3>
<ui-select ng-model="ctrl.country.selected" theme="select2" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="ctrl.firstLetterGroupFn" group-filter="reverseOrderFilterFn" repeat="country in ctrl.countries | filter: $select.search">
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search group by ctrl.firstLetterGroupFn(country) | uisGroupFilter:'Z'">
<span ng-bind-html="country.name | highlight: $select.search"></span>
<small ng-bind-html="country.code | highlight: $select.search"></small>
</ui-select-choices>
</ui-select>

<h3> Filter groups by array <small><code>group by ctrl.firstLetterGroupFn(country) | uisGroupFilter:['A', 'B', 'C']</code></small></h3>
<ui-select ng-model="ctrl.country.selected" theme="select2" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search group by ctrl.firstLetterGroupFn(country) | uisGroupFilter:['A', 'B', 'C']">
<span ng-bind-html="country.name | highlight: $select.search"></span>
<small ng-bind-html="country.code | highlight: $select.search"></small>
</ui-select-choices>
</ui-select>

<h3>Filter groups using a function <small><code>group by ctrl.firstLetterGroupFn(country) | reverseOrderFilterFn</code></small></h3>
<ui-select ng-model="ctrl.country.selected" theme="select2" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search group by ctrl.firstLetterGroupFn(country) | reverseOrderFilterFn">
<span ng-bind-html="country.name | highlight: $select.search"></span>
<small ng-bind-html="country.code | highlight: $select.search"></small>
</ui-select-choices>
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/demo-multiple-selection.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ <h3>Array of objects with single property binding</h3>
<h3>Array of objects (with groupBy)</h3>
<ui-select multiple ng-model="ctrl.multipleDemo.selectedPeopleWithGroupBy" theme="bootstrap" ng-disabled="ctrl.disabled" close-on-select="false" style="width: 800px;" title="Choose a person">
<ui-select-match placeholder="Select person...">{{$item.name}} &lt;{{$item.email}}&gt;</ui-select-match>
<ui-select-choices group-by="someGroupFn" repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search} group by someGroupFn(person)">
<div ng-bind-html="person.name | highlight: $select.search"></div>
<small>
email: {{person.email}}
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/demo-select2-with-bootstrap.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

<ui-select ng-model="ctrl.person.selected" theme="select2" class="form-control" title="Choose a person">
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="'group'" repeat="item in ctrl.people | filter: $select.search">
<ui-select-choices repeat="item in ctrl.people | filter: $select.search group by item.group">
<span ng-bind-html="item.name | highlight: $select.search"></span>
<small ng-bind-html="item.email | highlight: $select.search"></small>
</ui-select-choices>
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/demo-selectize-with-bootstrap.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

<ui-select ng-model="ctrl.person.selected" theme="selectize" title="Choose a person">
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices group-by="'group'" repeat="item in ctrl.people | filter: $select.search">
<ui-select-choices repeat="item in ctrl.people | filter: $select.search group by item.group">
<span ng-bind-html="item.name | highlight: $select.search"></span>
<small ng-bind-html="item.email | highlight: $select.search"></small>
</ui-select-choices>
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/demo-tagging.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h3>Simple String Tags <small>(Predictive Search Model &amp; No Labels)</small><
<h3>Object Tags <small>(with grouping)</small></h3>
<ui-select multiple tagging="ctrl.tagTransform" ng-model="ctrl.multipleDemo.selectedPeople" theme="bootstrap" ng-disabled="ctrl.disabled" style="width: 800px;" title="Choose a person">
<ui-select-match placeholder="Select person...">{{$item.name}} &lt;{{$item.email}}&gt;</ui-select-match>
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}" group-by="'country'">
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search} group by person.country">
<div ng-if="person.isTag" ng-bind-html="(person.name | highlight: $select.search) +' (new)'"></div>
<div ng-if="!person.isTag" ng-bind-html="person.name + person.isTag| highlight: $select.search"></div>
<small>
Expand Down
19 changes: 19 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
13 changes: 5 additions & 8 deletions src/uiSelectChoicesDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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;
Expand Down
70 changes: 35 additions & 35 deletions src/uiSelectController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
42 changes: 33 additions & 9 deletions src/uisRepeatParserService.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,19 @@ 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
// 3 Key on (key,value)
// 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);
}

Expand All @@ -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');
Expand All @@ -75,9 +104,4 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE
};

};

self.getGroupNgRepeatExpression = function() {
return '$group in $select.groups';
};

}]);
Loading