a suite of UI Components for development of web apps
Advanced User Interface Controls and Components
Created: 12 November 2015
Columns in IntegralUI Grid directive for AngularJS by default don't have a menu. Sorting of rows is executed manually from code or by clicking on column header. In following sections of this article, we will show you how to add a custom sorting menu to each column with options to sort rows in ascending or descending order.
In our example above, each column has a drop-down menu with options to sort rows and to change column visibility.
At first, we need to create the template that will be used as a model to replace the default column header. Because we want to add sorting functionality through a dropdown menu to column header, our template we will contain few span elements:
<div ng-app="appModule" ng-controller="appCtrl">
<script type="text/ng-template" id="column-menu.html">
<div class="column-header">
<span>{{obj.text}}</span>
<span iui-class="column-menu-button icons sort-mark {{obj.icon}}"></span>
<span class="column-menu-button" iui-contextmenu="obj.menu"></span>
</div>
</script>
</div>
.icons-medium
{
margin: 0 7px 0 1px;
}
.sort-ascending
{
background-position: -216px -96px;
}
.sort-descending
{
background-position: -216px -120px;
}
.check-mark
{
background-position: -192px -120px;
}
.column-header
{
margin: 0;
padding: 0 3px;
}
.column-menu-button
{
background-image: url(../../../resources/icons.png);
background-repeat: no-repeat;
border: thin solid #bebebe;
display: inline-block;
position: absolute;
top: 5px;
right: 3px;
margin: 0;
padding: 0;
width: 16px;
height: 16px;
cursor: default;
}
.column-menu-button:last-child
{
background-position: -176px -80px;
}
.column-header:hover > .column-menu-button:last-child
{
background-position: -192px -80px;
border-color: white;
}
.sort-mark
{
border-color: transparent;
right: 24px;
}
.sort-mark-down
{
background-position: -32px -80px;
}
.sort-mark-up
{
background-position: -48px -80px;
}
.sort-mark-down-white
{
background-position: -64px -80px;
}
.sort-mark-up-white
{
background-position: -80px -80px;
}
Related: Create Custom Header and Footer
From our template, you can notice that we are using an object (the obj variable) as our data model. This object is created for each column and it is our link to the template.
Next, we need to create the structure of the sorting menu. In case of sorting, our menu have only two options:
// Column menu for sorting grid rows
$scope.menu = {
activate: 'both',
itemIcon: 'icons-medium empty',
items: [
{ icon: 'icons-medium sort-ascending', text: 'Sort Ascending', key: 'SORT_ASCENDING', enabled: $scope.visibleColumns > 0 },
{ icon: 'icons-medium sort-descending', text: 'Sort Descending', key: 'SORT_DESCENDING', enabled: $scope.visibleColumns > 0 }
],
position: 'below'
}
The menu object also specifies how the dropdown menu will open and where it will be positioned. In our case, the menu will open with left and right mouse click, and it will be positioned below the menu button.
Now we have to create a link between columns and the template:
// Objects used by column menu, for each column
$scope.headers = [
{ text: "Title", menu: $scope.menu, icon: 'empty' },
{ text: "Year", menu: $scope.menu, icon: 'empty' },
{ text: "Ratings", menu: $scope.menu, icon: 'empty' }
];
// List of grid columns with template specification
$scope.columns = [
{
id: 2,
headerObj: $scope.headers[0],
headerContent: '
width: 320
},
{
id: 3,
headerObj: $scope.headers[1],
headerContent: '
contentAlignment: "center",
width: 130
},
{
id: 5,
headerObj: $scope.headers[2],
headerContent: '
contentAlignment: "center",
width: 110
}
];
Because each column header will have its own dropdown menu, a custom directive is created which will use the column header template. The template object is also created with a link to the menu object.
// Custom Header Directive
angular
.module("appModule", ["integralui"])
.directive('customHeader', function(){
return {
restrict: 'E',
templateUrl: 'column-menu.html',
scope: {
obj: '='
}
};
});
If we update the grid in current state, each column will display a menu button on the right, which when clicked will show a drop-down menu below. However, still there is no action linked with the menu options.
Whenever a menu option is selected, the itemClick event is fired. We will add a handler to this event and by using the key value for each menu option we can add a different functionality.
var activeColumn = null;
var sortOrder = 'none';
var prevActiveObj = null;
var activeObj = null;
// Handler for itemClick event of the sorting menu
$scope.menu.itemClick = function(e){
if (e.item){
// Remove the sorting icon from the previously active column
if (prevActiveObj && prevActiveObj != activeObj){
prevActiveObj.icon = 'empty';
$scope.$apply();
}
// Suspend the grid layout to increase performance
$gridService.suspendLayout($scope.gridName);
switch (e.item.key){
// In case when Sort Ascending menu option is clicked, sort the grid rows in ascending order
// Also select and update the sorting icon for the active column
case 'SORT_ASCENDING':
if (activeColumn){
sortOrder = 'ascending';
$gridService.selectedColumn($scope.gridName, activeColumn);
$gridService.sort($scope.gridName, activeColumn, sortOrder);
activeObj.icon = 'sort-mark-up-white';
}
break;
// In case when Sort Descending menu option is clicked, sort the grid rows in descending order
// Also select and update the sorting icon for the active column
case 'SORT_DESCENDING':
if (activeColumn){
sortOrder = 'descending';
$gridService.selectedColumn($scope.gridName, activeColumn);
$gridService.sort($scope.gridName, activeColumn, sortOrder);
activeObj.icon = 'sort-mark-down-white';
}
break;
}
// Resume the grid layout and update the grid
$gridService.resumeLayout($scope.gridName);
}
}
In our case, if 'Sort Ascending' option is selected, we will sort the grid rows in ascending order. In similar way, we will implement this for sorting grid rows in descending order.
In code we are using the activeColumn variable to determine which column is active, in other words, from which column the dropdown menu is opened. To know this we are also handling the open event, where we set the value of this variable to point to currently hovered column.
// Handler for open event of the column menu
$scope.menu.open = function(e){
prevActiveObj = activeObj;
// Change the reference of the activeColumn to point to currently hovered column
activeColumn = $gridService.getHoverColumn($scope.gridName);
// Update the object used by the column template, so we can update the sorting icon
activeObj = activeColumn ? $scope.headers[$scope.columns.indexOf(activeColumn)] : null;
}
Although sorting to the grid is applied, there is no indicator to the user that rows are sorted. To solve this, we are showing a sorting icon which displays whether rows are sorted in ascending or descending order, represented by the up or down arrow.
To update the sorting icon when column is selected (white arrow on dark column header), we will handle the afterSelect event. So whenever the column is selected, the sorting icon will also change from dark to white version of it.
// Handle the afterSelect event of the grid and change the sorting icon
$scope.onAfterSelect = function(e){
if (e.object && activeObj){
switch (sortOrder){
case 'ascending':
activeObj.icon = 'sort-mark-up';
if (activeColumn && activeColumn.selected != false)
activeObj.icon += '-white';
break;
case 'descending':
activeObj.icon = 'sort-mark-down';
if (activeColumn && activeColumn.selected != false)
activeObj.icon += '-white';
break;
default:
activeObj.icon = 'empty';
break;
}
$scope.$apply();
}
}
As an addition to our menu, we are also adding options that represent which column are visible. By using these options, we can show or hide a specific column from the dropdown menu.
Because column visibility is changed on demand, the whole menu is updated. For this purpose, we are creating a function that will update the menu structure in whole.
$scope.visibleColumns = 3;
var visibilityMenuItems = [];
// Sorting menu options
var defaultMenuItems = [
{ icon: 'icons-medium sort-ascending', text: 'Sort Ascending', key: 'SORT_ASCENDING', enabled: $scope.visibleColumns > 0 },
{ icon: 'icons-medium sort-descending', text: 'Sort Descending', key: 'SORT_DESCENDING', enabled: $scope.visibleColumns > 0 }
];
// A function that will update the column menu when column visibility changes
var updateMenu = function(){
$scope.menu.items.length = 0;
$scope.visibleColumns = 0;
// Sorting menu options
for (var i = 0; i < defaultMenuItems.length; i++)
$scope.menu.items.push(defaultMenuItems[i]);
// Column visibility menu options
visibilityMenuItems.length = 0;
var menuItem = { type: "separator" };
if ($scope.columns.length > 0){
$scope.menu.items.push(menuItem);
for (var j = 0; j < $scope.columns.length; j++){
menuItem = {
icon: getCheckIcon($scope.columns[j]),
text: $scope.columns[j].headerObj.text
}
if ($scope.columns[j].visible != false)
$scope.visibleColumns++;
visibilityMenuItems.push(menuItem);
$scope.menu.items.push(menuItem);
}
}
$scope.menu.items[1].enabled = $scope.visibleColumns > 0;
$scope.menu.items[2].enabled = $scope.visibleColumns > 0;
// Also update the global grid menu
$scope.gridMenu.items.length = 0;
for (var i = 0; i < visibilityMenuItems.length; i++)
$scope.gridMenu.items.push(visibilityMenuItems[i]);
}
In addition, we need to add code to the itemClick event, which will handle the column visibility:
// A modification to the itemClick event handler for the column menu
// Now includes key handlers for column visibility menu options
$scope.menu.itemClick = function(e){
$gridService.suspendLayout($scope.gridName);
if (e.item){
switch (e.item.key){
// Previous key handlers here
default:
updateColumnVisibility(e.item);
break;
}
updateMenu();
$gridService.resumeLayout($scope.gridName);
}
}
// Returns the check mark icon used for column visibility
var getCheckIcon = function(column){
return column.visible != false ? 'icons-medium check-mark' : null;
}
// Updates the check mark in column menu for each column
var updateColumnVisibility = function(item){
var index = visibilityMenuItems.indexOf(item);
if (index >= 0 && index < $scope.columns.length){
$scope.columns[index].visible = $scope.columns[index].visible == undefined ? false : !$scope.columns[index].visible;
item.icon = getCheckIcon($scope.columns[index]);
}
}
Finally, we have a functional dropdown menu, which sorts the grid rows in ascending and descending order, and also shows or hides columns.
When all columns are hidden, in order to proceed we need to add a context menu to the grid. This menu will only show the visibility status of columns.
We are using the same menu structure as before, only excluding the sorting options. Our global grid menu looks like this:
// Global menu attached to the grid
// Contains only column visibility menu options
$scope.gridMenu = {
itemIcon: 'icons-medium empty',
items: [],
itemClick: function(e){
if (e.item){
$gridService.suspendLayout($scope.gridName);
updateColumnVisibility(e.item);
updateMenu();
$gridService.resumeLayout($scope.gridName);
}
}
}
<div ng-app="appModule" ng-controller="appCtrl">
<iui-treegrid name="{{gridName}}" columns="columns" rows="rows" show-footer="false" allow-focus="false" iui-contextmenu="gridMenu" after-select=onAfterSelect(e)"></iui-treegrid>
</div>
This allows us to continue our operations with the grid when all columns are hidden.
Using templates in Angular Grid directive, we can create columns with custom header showing a menu button which when clicked will display a sorting menu. In this way, we can dynamically change the order by which grid rows are sorted, using menu options.