iamisti/md-data-table

View on GitHub
app/modules/main/directives/mdtTableDirective.js

Summary

Maintainability
C
1 day
Test Coverage
(function(){
    'use strict';

    /**
     * @ngdoc directive
     * @name mdtTable
     * @restrict E
     *
     * @description
     * The base HTML tag for the component.
     *
     * @param {object=} tableCard when set table will be embedded within a card, with data manipulation tools available
     *      at the top and bottom.
     *
     *      Properties:
     *
     *      - `{boolean=}` `visible` - enable/disable table card explicitly
     *      - `{string}` `title` - the title of the card
     *      - `{boolean=}` `columnSelector` - enables the column selection for the table (you can disable certain columns from the list selection, using `exclude-from-column-selector`, see the related docs)
     *      - `{array=}` `actionIcons` - (not implemented yet)
     *
     * @param {boolean=} selectableRows when set each row will have a checkbox
     * @param {boolean=} virtualRepeat when set, virtual scrolling will be applied to the table. You must set a fixed
     *      height to the `.md-virtual-repeat-container` class in order to make it work properly. Since virtual
     *      scrolling is working with fixed height.
     * @param {String=} alternateHeaders some table cards may require headers with actions instead of titles.
     *      Two possible approaches to this are to display persistent actions, or a contextual header that activates
     *      when items are selected
     *
     *      Assignable values are:
     *
     *      - 'contextual' - when set table will have kind of dynamic header. E.g.: When selecting rows, the header will
     *        change and it'll show the number of selected rows and a delete icon on the right.
     *      - 'persistentActions' - (not implemented yet)
     *
     * @param {function(rows)=} deleteRowCallback callback function when deleting rows.
     *      At default an array of the deleted row's data will be passed as the argument.
     *      When `table-row-id` set for the deleted row then that value will be passed.
     *
     * @param {function(rows)=} selectedRowCallback callback function when selecting rows.
     *      At default an array of the selected row's data will be passed as the argument.
     *      When `table-row-id` set for the selected row then that value will be passed.
     *
     * @param {boolean=} animateSortIcon sort icon will be animated on change
     * @param {boolean=} rippleEffect ripple effect will be applied on the columns when clicked (not implemented yet)
     * @param {boolean=} paginatedRows if set then basic pagination will applied to the bottom of the table.
     *
     *      Properties:
     *
     *      - `{boolean=}` `isEnabled` - enables pagination
     *      - `{array}` `rowsPerPageValues` - set page sizes. Example: [5,10,20]
     *
     * @param {object=} mdtRow passing rows data through this attribute will initialize the table with data. Additional
     *      benefit instead of using `mdt-row` element directive is that it makes possible to listen on data changes.
     *
     *      Properties:
     *
     *      - `{array}` `data` - the input data for rows
     *      - `{integer|string=}` `table-row-id-key` - the uniq identifier for a row
     *      - `{function(rowData)=}` `table-row-class-name` - callback to specify the class name of a row
     *      - `{array}` `column-keys` - specifying property names for the passed data array. Makes it possible to
     *        configure which property assigned to which column in the table. The list should provided at the same order
     *        as it was specified inside `mdt-header-row` element directive.
     *
     * @param {function(page, pageSize, options)=} mdtRowPaginator providing the data for the table by a function. Should set a
     *      function which returns a promise when it's called. When the function is called, these parameters will be
     *      passed: `page` and `pageSize` which can help implementing an ajax-based paging, and `options` which is providing
     *      more information.
     *
     *      Currently these are available from options:
     *      - `array` `columnFilter` - an array of the filtered column sets (todo: create a demo for it)
     *      - `array` `columnSort` - an array of the sorted column sets. You can inspect which column did you
     *                               sorted and if its in asc or desc order (todo: create a demo for it)
     *
     * @param {string=} mdtRowPaginatorErrorMessage overrides default error message when promise gets rejected by the
     *      paginator function.
     *
     * @param {string=} mdtRowPaginatorNoResultsMessage overrides default 'no results' message.
     *
     * @param {function(loadPageCallback)=} mdtTriggerRequest provide a callback function for manually triggering an
     *      ajax request. Can be useful when you want to populate the results in the table manually. (e.g.: having a
     *      search field in your page which then can trigger a new request in the table to show the results based on
     *      that filter.
     *
     * @param {object=} mdtTranslations accepts various key-value pairs for custom translations.
     *
     * @param {boolean=} mdtLoadingIndicator if set then loading indicator can be customised
     *
     *      Properties:
     *
     *      - `{string=}` `color` - passing a css compatible format as a color will set the color for the loading indicator
     *
     * @example
     * <h2>`mdt-row` attribute:</h2>
     *
     * When column names are: `Product name`, `Creator`, `Last Update`
     * The passed data row's structure: `id`, `item_name`, `update_date`, `created_by`
     *
     * Then the following setup will parse the data to the right columns:
     * <pre>
     *     <mdt-table
     *         mdt-row="{
     *             'data': controller.data,
     *             'table-row-id-key': 'id',
     *             'column-keys': ['item_name', 'update_date', 'created_by']
     *         }">
     *
     *         <mdt-header-row>
     *             <mdt-column>Product name</mdt-column>
     *             <mdt-column>Creator</mdt-column>
     *             <mdt-column>Last Update</mdt-column>
     *         </mdt-header-row>
     *     </mdt-table>
     * </pre>
     */
    function mdtTableDirective(TableDataStorageFactory,
                               EditCellFeature,
                               SelectableRowsFeature,
                               PaginationFeature,
                               ColumnSelectorFeature,
                               _){
        return {
            restrict: 'E',
            templateUrl: '/main/templates/mdtTable.html',
            transclude: true,
            scope: {
                tableCard: '=',
                selectableRows: '=',
                alternateHeaders: '=',
                deleteRowCallback: '&',
                selectedRowCallback: '&',
                saveRowCallback: '&',
                animateSortIcon: '=',
                rippleEffect: '=',
                paginatedRows: '=',
                mdtRow: '=',
                mdtRowPaginator: '&?',
                mdtRowPaginatorErrorMessage:'@',
                mdtRowPaginatorNoResultsMessage:'@',
                virtualRepeat: '=',
                mdtTriggerRequest: '&?',
                mdtTranslations: '=?',
                mdtLoadingIndicator: '=?'
            },
            controller: function mdtTable($scope){
                var vm = this;

                $scope.rippleEffectCallback = function(){
                    return $scope.rippleEffect ? $scope.rippleEffect : false;
                };

                _setDefaultTranslations();
                _initTableStorage();

                PaginationFeature.initFeature($scope, vm);
                ColumnSelectorFeature.initFeature($scope, vm);

                _processData();

                // initialization of the storage service
                function _initTableStorage(){
                    vm.dataStorage = TableDataStorageFactory.getInstance();
                }

                // set translations or fallback to a default value
                function _setDefaultTranslations(){
                    $scope.mdtTranslations = $scope.mdtTranslations || {};

                    $scope.mdtTranslations.rowsPerPage = $scope.mdtTranslations.rowsPerPage || 'Rows per page:';

                    $scope.mdtTranslations.largeEditDialog = $scope.mdtTranslations.largeEditDialog || {};
                    $scope.mdtTranslations.largeEditDialog.saveButtonLabel = $scope.mdtTranslations.largeEditDialog.saveButtonLabel || 'Save';
                    $scope.mdtTranslations.largeEditDialog.cancelButtonLabel = $scope.mdtTranslations.largeEditDialog.cancelButtonLabel || 'Cancel';
                }

                // fill storage with values if set
                function _processData(){
                    if(_.isEmpty($scope.mdtRow)) {
                        return;
                    }

                    //local search/filter
                    if (angular.isUndefined($scope.mdtRowPaginator)) {
                        $scope.$watch('mdtRow', function (mdtRow) {
                            vm.dataStorage.storage = [];

                            _addRawDataToStorage(mdtRow['data']);
                        }, true);
                    }else{
                        //if it's used for 'Ajax pagination'
                    }
                }

                function _addRawDataToStorage(data){
                    var rowId;
                    var columnValues = [];
                    _.each(data, function(row){
                        rowId = _.get(row, $scope.mdtRow['table-row-id-key']);
                        columnValues = [];

                        _.each($scope.mdtRow['column-keys'], function(columnKey){
                            columnValues.push({
                                attributes: {
                                    editableField: false
                                },
                                rowId: rowId,
                                columnKey: columnKey,
                                value: _.get(row, columnKey)
                            });
                        });

                        vm.dataStorage.addRowData(rowId, columnValues);
                    });
                }
            },
            link: function($scope, element, attrs, ctrl, transclude){
                $scope.dataStorage = ctrl.dataStorage;

                _injectContentIntoTemplate();

                _initEditCellFeature();
                _initSelectableRowsFeature();

                PaginationFeature.startFeature(ctrl);
                ColumnSelectorFeature.initFeatureHeaderValues($scope.dataStorage.header, ctrl.columnSelectorFeature);

                function _injectContentIntoTemplate(){
                    transclude(function (clone) {
                        var headings = [];
                        var body = [];
                        var customCell = [];

                        // Use plain JS to append content
                        _.each(clone, function (child) {

                            if ( child.classList !== undefined ) {
                                if ( child.classList.contains('theadTrRow')) {
                                    headings.push(child);
                                }
                                else if( child.classList.contains('customCell') ) {
                                    customCell.push(child);
                                }
                                else {
                                    body.push(child);
                                }
                            } else {
                                body.push(child);
                            }
                        });

                        var reader = element[0].querySelector('.mdtTable-reader');

                        _.each(headings, function (heading) {
                            reader.appendChild( heading );
                        });

                        _.each(body, function (item) {
                            reader.appendChild( item );
                        });
                    });
                }

                function _initEditCellFeature(){
                    //TODO: make it possible to only register feature if there is at least
                    // one column which requires it.
                    // for that we need to change the place where we register edit-row.
                    // Remove mdt-row attributes --> do it in mdt-row attribute directive on mdtTable
                    EditCellFeature.addRequiredFunctions($scope, ctrl);
                }

                function _initSelectableRowsFeature(){
                    SelectableRowsFeature.getInstance({
                        $scope: $scope,
                        ctrl: ctrl
                    });
                }
            }
        };
    }

    angular
        .module('mdDataTable')
        .directive('mdtTable', mdtTableDirective);
}());