Smart Table

Code on github

Introduction

improve the documentation

Smart table is an Angularjs module to easily display data in a table with a set of built in functionalities such filetering, sorting, etc. While developing this module I made sure to focus on this particular points:

Although smart-table is from far the best table module for angular :D, there are other table modules in the angular ecosystem you might be interested in. The approach and philosophy are different and maybe more appropriate to your way of building web application. Among the most popular:

If you want to play around, try this plunker

The basics

improve the documentation

If you want to display data in a table it is really easy. You simply have to add the smart-table tag into your markup (it is an element directive) and bind the rows (see markup tab) attribute to an array somewhere in the scope. In our example we specify the data used to fill the table in a controller.

app.controller('basicsCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];
}]);
<div ng-controller="basicsCtrl">
    <smart-table rows="rowCollection"></smart-table>
</div>

You can also specify the layout you want. That is where we introduce the column configuration. On our markup we bind the columns attribute to an array in which we specify how to display the data. The label property will give the header of the column whereas the map property is the property in our data-model object we want a given column to be bound to. A column will be inserted by column configuration.

The mapping can support multi-level mapping like "myObject.myInnerProperty"

app.controller('basicsCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName'},
        {label: 'same same but different', map: 'firstName'},
        {label: 'Last Name', map: 'lastName'}
    ];
}]);
<div ng-controller="basicsCtrl">
    <smart-table columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Format data

improve the documentation

In our previous examples the data were displayed as raw strings,but what if we want to format date, currency and so on in a proper way ? In the column configuration, you can specify how to format the data for the given column by adding formatFunction and formatParameter properties.

app.controller('sortCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName', formatFunction: function (value, formatParameter) {
            //this only display the first letter
            return value[0];
        }},
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$'}
    ];
}]);
<div ng-controller="formatCtrl">
    <smart-table columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Sort data

improve the documentation

You may also want to sort the data rows. By default the table sort the data rows following the orderBy angular algorithm but you can also use your own algorithm (see below). When you click on one of the column header the orderBy algorithm will sort the data rows according to these rules

You can also disable the sort function for a given column by setting the isSortable property to false (it is true by default)

app.controller('sortCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName', sortPredicate: function (dataRow) {
//predicate as a function (see angular orderby documentation) : it will sort by the string length
            return dataRow.firstName.length;
        } },
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$'},
        {label: 'e-mail', map: 'email', isSortable: false}
    ];
}]);
<div ng-controller="sortCtrl">
    <smart-table columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Use your own algorithm

If you would like to use your own sort algorithm instead of the orderBy from Angular you also can. That is where our table configuration comes. You can bind the table-configuration attribute to an object that will represent the table "global" configuration. One of the global property is the sort algorithm (sortAlgorithm).

function customSortAlgorithm(arrayRef, sortPredicate, reverse) {
//do some stuff
    return sortedArray;
}

Search/filter data

improve the documentation

The table controller API gives you a way to filter the data comparing an input to both a given column values or in the whole table. To display an input textbox for a global search (in the whole table) you just have to set the isGlobalSearchActivated property to true in the table global config object. The algorithm used will be the one used by the filter filter from Angular. Of course, if you want to use your own, you can specify it in the global config (see below)

app.controller('filterCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName'},
        //the headerTemplateUrl property will be explained later, the purpose here is to show we can filter also by column only
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase', headerTemplateUrl: 'assets/template/customHeader.html'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$'},
        {label: 'e-mail', map: 'email', isSortable: false}
    ];
    scope.globalConfig = {
        isGlobalSearchActivated: true
    };
}]);
<div ng-controller="filterCtrl">
    <smart-table config="globalConfig" columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Use your own algorithm

In the same way than for the sort operation you can use you own algorithm, you just have to specify the function you want to use in the global config under the property filterAlgorithm

function customFilter(arrayRef, expression) {
    //do some stuff
    return filteredArray;
}

Select data rows

improve the documentation

You can select data row according to two different modes : single or multiple (of course you can disable selection) by setting the global property selectionMode to single or multiple (any other value will be taken as selectionMode='none'). This is the only time the table changes a property value into your model: it add the "isSelected" property to your data model object. On one hand you can consider it "pollutes" your model but on the other hand it allows the parent scope to quickly have a list of the selected items without relying on events. If you are in selectionMode="multiple" you can add a selection checkbox column by setting the global property displaySelectionCheckbox to true

Even though the configuration is dynamic the table does not prevent forbidden states : for example if you switch from selectionMode='single' to 'none' with a selected item, the item will remain selected. It is the responsability of the parent scope/controller to reset the selected item

Keep track of the changes in selection

As mentioned before, whenever a row is selected/unselected the property isSelected change on the particular bound item. You can of course watch your model to see when the selected items change however this is not really efficient regarding the performances. That is why an selectionChange event is emitted every time there is something changed.

app.controller('selectionCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName'},
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$'},
        {label: 'e-mail', map: 'email'}
    ];

    //keep track of selected items
    scope.$on('selectionChange', function (event, args) {
        console.log(args);
    });

    scope.globalConfig = {
        selectionMode: 'multiple',
        displaySelectionCheckbox: true
    };
}]);
<div ng-controller="selectionCtrl">
    <smart-table config="globalConfig" columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Edit cell

improve the documentation

You can make the cells of a given column editable by simply setting the isEditable property to true in this column config. This will allow you to edit directly the property on the data model object. Therefore, you may want to specify which type of input the cell will accept. You just have to set the type property to the the type you want (email, number, etc). If the input is valid the model is updated, otherwise it will keep the previous value

notify an item has been updated

In the same way than for selection changes, you don't want to watch the whole collection to know when a given property of a given item has bee updated. That is why smart-table will raise the updateDataRow event to notify the parent scope when a row has been modified.

app.controller('editCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    //every time a data row is updated
    scope.$on('updateDataRow', function (event, arg) {
        console.log(arg);
    });

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName', isEditable: true},
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date', isEditable: true, type: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$', isEditable: true, type: 'number'},
        {label: 'e-mail', map: 'email', isEditable: true, type: 'email'}
    ];
}]);
<div ng-controller="editCtrl">
    <smart-table config="globalConfig" columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Simple styling

improve the documentation

This allows you to add a class name for the cells in a given column, and for the header as well. You'll have to specify the headerClass and cellClass property in the column configuration

app.controller('styleCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName', headerClass: "firstName-header"},
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase', cellClass: "lastName-cell"},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency'},
        {label: 'e-mail', map: 'email'}
    ];
}]);
.firstName-header{
text-decoration: underline;
}

.lastName-cell{
color: red;
}
<div ng-controller="styleCtrl>
    <smart-table  columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Cell Template

improve the documentation

If you prefer to use a particular template for the cells in a given column, you can do it by specifying the cellTemplateUrl property in the column config this is useful not only for styling purpose but also if you want to enhance the behavior of the cell. The template will be compiled so that if it contains directives they will be considered as angular directives. Moreover, these directives can have access to all the row and column data through the scope

scope.column;//the column object
scope.dataRow;//the data row object
scope.formatedValue;//the formated value (not the raw string)
app.controller('cellTemplateCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName'},
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency'},
        {label: 'e-mail', map: 'email'},
        {label: 'Favourite color', cellTemplateUrl: 'assets/template/custom.html'}
    ];
}]);
<div>
    <smart-table columns="columnCollection" rows="rowCollection"></smart-table>
</div>
<custom></custom>
app.directive('custom', function () {
    return {
        restrict: 'E',
        //include smart table controller to use its API if needed
        require: '^smartTable',
        template: '<select ng-model="favouriteColor">' +
                '<option value="">--choose favorite color--</option>' +
                '<option value="red">red</option>' +
                '<option value="blue">blue</option>' +
                '<option value="yellow">yellow</option>' +
                '</select>',
        replace: true,
        link: function (scope, element, attrs, ctrl) {
            //can use scope.dataRow, scope.column, scope.formatedValue, and ctrl API

            var allowedColors = ['red', 'yellow', 'blue'];

            scope.$watch('favouriteColor', function (value) {
                if (allowedColors.indexOf(value) != -1) {
                    scope.dataRow.favouriteColor = scope.favouriteColor;
                }
            });
        }
    };
});

In the same way you can assign a custom template to the column header thanks to the column property headerTemplateUrl. See the source code of the previous filter section as an example.

app.controller('filterCtrl', ['$scope', function (scope) {
    scope.rowCollection = [
        {firstName: 'Laurent', lastName: 'Renard', birthDate: new Date('1987-05-21'), balance: 102, email: 'whatever@gmail.com'},
        {firstName: 'Blandine', lastName: 'Faivre', birthDate: new Date('1987-04-25'), balance: -2323.22, email: 'oufblandou@gmail.com'},
        {firstName: 'Francoise', lastName: 'Frere', birthDate: new Date('1955-08-27'), balance: 42343, email: 'raymondef@gmail.com'}
    ];

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName'},
        {label: 'Last Name', map: 'lastName', formatFunction: 'uppercase', headerTemplateUrl: 'assets/template/customHeader.html'},
        {label: 'Birth Date', map: 'birthDate', formatFunction: 'date'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$'},
        {label: 'e-mail', map: 'email'}
    ];
    scope.globalConfig = {
        isGlobalSearchActivated: true
    };
}]);
<div ng-controller="filterCtrl">
    <smart-table config="globalConfig" columns="columnCollection" rows="rowCollection"></smart-table>
</div>
<span>{{column.label}}</span>
<input class="column-filter" type="text" ng-model="searchValue" />
app.directive('columnFilter', function () {
    return {
        restrict: 'C',
        require: '^smartTable',
        link: function (scope, element, attrs, ctrl) {
            scope.searchValue = '';
            scope.$watch('searchValue', function (value) {
                ctrl.search(value, scope.column);
            })
        }
    }
});

Client side Pagination

improve the documentation

If you don't want to display too much data at the time you'll be happy using the pagination feature. It uses the angular.ui-boostrap pagination directive and you'll be able to configure it through some global config properties. First of all, the property isPaginationEnabled must be set to true. Then you can decide the number of pages you want to display with the maxSize property and the number of items you want to display on a page thanks to the itemsByPage property

app.controller('paginationCtrl', ['$scope', function (scope) {
    var
            nameList = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa'],
            familyName = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez'];

    function createRandomItem() {
        var
                firstName = nameList[Math.floor(Math.random() * 4)],
                lastName = familyName[Math.floor(Math.random() * 4)],
                age = Math.floor(Math.random() * 100),
                email = firstName + lastName + '@whatever.com',
                balance = Math.random() * 3000;

        return{
            firstName: firstName,
            lastName: lastName,
            age: age,
            email: email,
            balance: balance
        };
    }

    scope.rowCollection = [];
    for (var j = 0; j < 200; j++) {
        scope.rowCollection.push(createRandomItem());
    }

    scope.columnCollection = [
        {label: 'First Name', map: 'firstName'},
        {label: 'Last Name', map: 'lastName'},
        {label: 'Age', map: 'age'},
        {label: 'Balance', map: 'balance', formatFunction: 'currency', formatParameter: '$'},
        {label: 'e-mail', map: 'email'}
    ];

    scope.globalConfig = {
        isPaginationEnabled: true,
        itemsByPage: 12,
        maxSize: 8
    };
}]);
<div ng-controller="paginationCtrl">
    <smart-table config="globalConfig" columns="columnCollection" rows="rowCollection"></smart-table>
</div>

Configuration Summary

improve the documentation

Here is a summary of available configuration properties for column, and for global configuration

Column

Global config