uniquejava / blog

My notes regarding the vibrating frontend :boom and the plain old java :rofl.
Creative Commons Zero v1.0 Universal
11 stars 5 forks source link

jQuery DataTables and Editor - the evil #124

Open uniquejava opened 7 years ago

uniquejava commented 7 years ago

https://datatables.net/ and https://editor.datatables.net/

Compare jsgrid and datatables Getting Started Page

DataTables Getting Started

jsgrid Getting Started

I think you should already make up your mind now. the former very complex and hard to know what it says

The latter, simple and to the point, the clear interface includes all the functions that a table editor requires.

吐槽 Complaint

DataTables所有的列都要指定fields和columns (N多重复代码) data, visible, className要定义在column里边, name, type要定义在fields里边, WTF?

jsgrid只要定义fields: name, visible和type等一个field轻松搞定, no brain-fuck.

DataTables的下拉框真的难用, 文档在tech notes里边(难找吧!) https://datatables.net/manual/tech-notes/11 下拉字段是下面这样的形式 fields.push({name: 'FieldName', type: 'select', options: [{label:'', value:''}, ...] }); 其中options必须是这种指定格式. 需要提前转换, jsgrid没有这个限制(无需转换)..

DataTables的editor还是收费的. 依赖一大堆组合, buttons是一个js, select又是一个js, 到底要下载哪些js分分钟把你搞晕

DataTables出了错竟然是弹出alert(这也太不友好了, 在console.log中打印出来不更好) 关键是warning类型的没有必要弹alert.

如何禁掉这个alert在此: https://datatables.net/forums/discussion/28726/how-to-disable-the-warning-message

小技巧

访问nested array, 像这样: friends.0.name, friends.1.name

references

https://datatables.net/forums/discussion/29339/change-ajax-url

https://editor.datatables.net/reference/option/ajax.data

https://editor.datatables.net/reference/field/select

uniquejava commented 6 years ago

angular-datatables

angular-datatables官方文档: http://l-lin.github.io/angular-datatables/archives/

主要有两种模式, angular wayajax.

Angular way就是在html中用ng-repeat构建表格, 需要给table加上<table datatable="ng">这个属性值.

Ajax是在html中定义一个没有thead和tbody的table ,加上<table datatable="" dt-columns="vm.dtColumns">. 然后所有的字段在vm.dtComlumns用js手工构建.

如果要使用bootstrap的样式, 见http://l-lin.github.io/angular-datatables/archives/#!/bootstrapIntegration 这部分.

如果要使用分页功能, 一开始会被withDOM('frtip')这样的代码吓倒(这些诡异的字符是什么意思, WTF??). withDOM的文档: https://datatables.net/reference/option/dom

如果要使用server side分页, 不能用angular way, 只能用ajax. DataTables要求服务端响应如下数据格式

{
    "draw": 1,
    "recordsTotal": 57,
    "recordsFiltered": 5,
    "data": [...]
}

可以在callback中做一个数据格式变换, 参见下面我封装的utils.js 详见: https://stackoverflow.com/questions/25211553/datatables-custom-response-handling

如果要使用scroller插件, 也不能用angular way, 只能用ajax, 不然在窗口resize的时候固定表头不会随窗口动态调整宽度.

uniquejava commented 6 years ago

在angularjs run中设置DataTables默认值

app.run(runBlock);

/** @ngInject */
function runBlock($log, $rootScope, $state, DTDefaultOptions, config) {
  $rootScope.$state = $state;
  $rootScope.config = config;

  DTDefaultOptions.setLanguage({
    "sEmptyTable": "テーブルにデータがありません",
    "sProcessing": '<img src="assets/images/ajax-loader.gif">', // #008A8A
  });
  DTDefaultOptions.setOption('bSort', false);
  DTDefaultOptions.setOption('bFilter', false);
  DTDefaultOptions.setOption('bPaginate', false);
  DTDefaultOptions.setOption('bInfo', false);
}

/** @ngInject */
app.factory('_', ['$window', function ($window) {
  // see https://stackoverflow.com/questions/14968297/use-underscore-inside-angular-controllers
  return $window._;
}]);

ajax

有两种使用方法:

var dtOptions = DTOptionsBuilder.newOptions()
  .withOption('ajax', {
    url: config.api + '/categories/v1',
    method: 'GET'
  }).withDataProp('items');

还有一种, 可以在ajax中使用callback(服务端分页需要)

dtOptions.withOption('ajax', function (data, callback, settings) {
});

我觉得fromFnPromise更好, 因为后者可以使用$http服务, 加入http interceptors后能对请求进行统一处理.

fromFnPromise

文档中写的return deferred.promise那种方式已经不能用了, 看了源代码才知道要像下面这样写:

  var dtOptions = DTOptionsBuilder.fromFnPromise(function () {
    return $http.get(config.api + '/categories/v1');
  }).withOption('sAjaxDataProp', 'data.items');

createdRow

为了能在renderWith的返回值中使用angular directive, 需要加上如下createdRow选项, 它会使用$compile服务对renderWith的值进行scope绑定. 这段代码来自: https://stackoverflow.com/questions/36911340/how-to-access-current-row-data-in-angular-datatables-using-templates

dtOptions.withOption('createdRow', function (row, data, dataIndex) {
  // Create a new scope for each row, otherwise, data will
  // not be unique for each row because of data bindings
  var $scope = scope;

  var $newScope = $scope.$new(true);
  $newScope.row = data;

  // Pass any methods you are using in current scope
  $newScope.delete = $scope.delete;

  return $timeout(function () {
    // Recompiling so we can bind Angular directive to the DT
    return $scope.$apply($compile(angular.element(row).contents())($newScope));
  });
});

这样能在renderWith的模板中使用row来访问当前行的数据(不需要使用renderWith传入的参数)

在angular-datatables的文档中使用了更简洁的方式 http://l-lin.github.io/angular-datatables/archives/#!/bindAngularDirective 它没有在sub scope中创建新的变量, 直接结合使用了vm.persons和renderWith传入的参数.

function BindAngularDirectiveCtrl($scope, $compile, DTOptionsBuilder, DTColumnBuilder) {
    var vm = this;
    vm.message = '';
    vm.edit = edit;
    vm.delete = deleteRow;
    vm.dtInstance = {};
    vm.persons = {};
    vm.dtOptions = DTOptionsBuilder.fromSource('data1.json')
        .withPaginationType('full_numbers')
        .withOption('createdRow', createdRow);
    vm.dtColumns = [
        DTColumnBuilder.newColumn('id').withTitle('ID'),
        DTColumnBuilder.newColumn('firstName').withTitle('First name'),
        DTColumnBuilder.newColumn('lastName').withTitle('Last name'),
        DTColumnBuilder.newColumn(null).withTitle('Actions').notSortable()
            .renderWith(actionsHtml)
    ];

    function edit(person) {
        vm.message = 'You are trying to edit the row: ' + JSON.stringify(person);
        // Edit some data and call server to make changes...
        // Then reload the data so that DT is refreshed
        vm.dtInstance.reloadData();
    }
    function deleteRow(person) {
        vm.message = 'You are trying to remove the row: ' + JSON.stringify(person);
        // Delete some data and call server to make changes...
        // Then reload the data so that DT is refreshed
        vm.dtInstance.reloadData();
    }
    function createdRow(row, data, dataIndex) {
        // Recompiling so we can bind Angular directive to the DT
        $compile(angular.element(row).contents())($scope);
    }
    function actionsHtml(data, type, full, meta) {
        vm.persons[data.id] = data;
        return '<button class="btn btn-warning" ng-click="showCase.edit(showCase.persons[' + data.id + '])">' +
            '   <i class="fa fa-edit"></i>' +
            '</button>&nbsp;' +
            '<button class="btn btn-danger" ng-click="showCase.delete(showCase.persons[' + data.id + '])" )"="">' +
            '   <i class="fa fa-trash-o"></i>' +
            '</button>';
    }
}

dt-instance

如果要重绘表格可以使用vm.dtInstance.rerender(), 给vm.dtInstance设置值有两种方法: 见: http://l-lin.github.io/angular-datatables/archives/#!/dtInstances

直接设定一个变量

vm.dtInstance = {};
<table datatable="" dt-options="vm.dtOptions" dt-columns="vm.dtColumns" dt-instance="vm.dtInstance"
       class="table table-bordered table-hover dataTables-example">
</table>

回调函数(可以设置多个instance)

vm.dtInstances = [];
vm.dtInstCallback = function (dtInstance) {
  vm.dtInstances.push(dtInstance);
}
<table datatable="" dt-options="vm.dtOptions" dt-columns="vm.dtColumns" dt-instance="vm.dtInstCallback"
       class="table table-bordered table-hover dataTables-example">
</table>

Fixed table header(scroller)

需要加入依赖 datatables.scroller

  1. angular way: 这种方法写html简单, 但是固定表头不能随窗口改变动态调整大小.
  2. dt-columns, 代码长, 但是不会有resize的问题.
    // Code for fixed table header
    vm.dtInstances = [];

    vm.dtOptions = DTOptionsBuilder.newOptions()
      .withScroller()
      .withOption('scrollY', 152); // 5 rows

    vm.dtInstCallback = function (dtInstance) {
      vm.dtInstances.push(dtInstance);

      // resize fixed table header on window resize
      var onResize = _.throttle(_.bind(dtInstance.rerender, dtInstance), 1000);

      function cleanUp() {
        angular.element($window).off('resize', onResize);
        console.log('cleanUp');
      }

      angular.element($window).on('resize', onResize);
      $scope.$on('$destroy', cleanUp);

    };

如果给table加上min-width/width会出现水平滚动条, 这个时候fixed header不会随着滚动条一起滚动, 见: FixedHeader with horizontal scroll http://olgatsib.comcdn.datatables.net/forums/discussion/comment/74360/#Comment_74360

.withOption('bAutoWidth', false)

FixedColumn

uniquejava commented 6 years ago

server side pagination

我这写了一个工具类.

(function () {
  'use strict';

  angular.module('myapp')
    .factory('utils', utils);

  /** @ngInject */
  function utils($http, $compile, $timeout, DTOptionsBuilder, config) {
    return {

      calcRowNumber: function (vm, meta) {
        return (vm.pageParams.page - 1) * vm.pageParams.limit + meta.row + 1;
      },

      useNgDirective: function (dtOptions, scope) {
        // see https://stackoverflow.com/questions/36911340/how-to-access-current-row-data-in-angular-datatables-using-templates
        dtOptions.withOption('createdRow', function (row, data, dataIndex) {
          // Create a new scope for each row, otherwise, data will
          // not be unique for each row because of data bindings
          var $scope = scope;

          var $newScope = $scope.$new(true);
          $newScope.row = data;

          // Pass any methods you are using in current scope
          $newScope.delete = $scope.delete;

          return $timeout(function () {
            // Recompiling so we can bind Angular directive to the DT
            return $scope.$apply($compile(angular.element(row).contents())($newScope));
          });
        });
      },

      enableServerSidePagination: function (vm, options) {
        options = options || {};
        options.limit = options.limit || 10;

        var dtOptions = DTOptionsBuilder.newOptions();
        dtOptions.withOption('ajax', function (data, callback, settings) {
          // make an ajax request using data.start and data.length
          var limit = data.length;
          var page = data.start / limit + 1;

          $http.get(config.api + options.url,
            {params: {page: page, limit: limit}})
            .success(function (res) {
              // map your server's response to the DataTables format and pass it to
              // DataTables' callback
              var pageParams = {
                total: res.total,
                page: page,
                limit: res.limit
              };

              vm.pageParams = pageParams;

              callback({
                recordsTotal: res.total,
                recordsFiltered: res.total,
                data: res.items
              });

            });
        });

        if(options.ng && options.scope) {
          this.useNgDirective(dtOptions, options.scope);
        }

        /*
          see https://datatables.net/reference/option/dom

          l - length changing input control
          f - filtering input
          t - The table!
          i - Table information summary
          p - pagination control
          r - processing display element

         */

        dtOptions
          .withOption('bPaginate', true)
          .withOption('bInfo', true)
          .withDOM('rtip')
          .withOption('processing', true)
          .withOption('serverSide', true)
          .withPaginationType('full_numbers')
          .withDataProp('data')
          .withDisplayLength(options.limit)
          .withBootstrap();

        vm.dtOptions = dtOptions;
      }

    };
  }

})();

用法如下:

vm.dtInstance = {};

// 反注释这行使用服务端分页
//utils.enableServerSidePagination(vm, {url: '/categories/v1', ng: true, scope: $scope});

var dtOptions = DTOptionsBuilder.fromFnPromise(function () {
    return $http.get(config.api + '/categories/v1');
  }).withOption('sAjaxDataProp', 'data.items');

utils.useNgDirective(dtOptions, $scope);

dtOptions.withScroller().withOption('scrollY', 200); 
vm.dtOptions = dtOptions;

vm.dtColumns = [
  // DTColumnBuilder.newColumn(null).withTitle('#').renderWith(function (data, type, full, meta) {
  //   return utils.calcRowNumber(vm, meta);
  // }),

  DTColumnBuilder.newColumn('name').withTitle('Name').renderWith(function (data) {
    return '{{row.name|limitTo: 5}}';
  }),
  DTColumnBuilder.newColumn('createdDate')
    .withClass('text-center')
    .withTitle('Created Date')
    .renderWith(function (data, type, full, meta) {
      return $filter('date')(data, 'yyyy/MM/dd');
    }),
  DTColumnBuilder.newColumn('customProperty').withTitle('Custom Property').notVisible(),
  DTColumnBuilder.newColumn('id').withTitle('Link').renderWith(function () {
    return '<a ui-sref="index.some({id:row.id})">some link</a>';
  })
];
uniquejava commented 6 years ago

datatables callback

在datatables渲染完table后会回调 fnDrawCallback, 可以在withOption中注册这个回调函数.