角度模板字符串的一次性渲染

时间:2013-08-22 23:01:13

标签: angularjs client-side-templating

我正在编写一个指令,将SlickGrid与我的角度应用程序集成。我希望能够使用角度模板(而不是格式化程序函数)配置SlickGrid列。为了实现这一点,我需要指令来动态创建将HTML作为字符串返回的格式化函数。

我的方法是创建一个临时范围,将模板链接到该范围,捕获html,然后销毁范围。这有效,但抱怨$digest already in progress。有没有办法以这种方式渲染角度模板,与全局$ digest循环隔离?

BTW:我尝试使用$ interpolate,效果很好,但不支持ng-repeat或其他指令。

var columnsConfig = [
  {
    id: "name", 
    name: "Name", 
    field: "name", 
    template: '<a href="{{context.url}}">{{value}}</a>'
  },
  {
    id: "members", 
    name: "Members", 
    field: "members", 
    template: '<div ng-repeat="m in value">{{m}}</div>'
  }
];

myModule.directive('SlickGrid', ['$compile', function($compile) {
  return {
    restrict: 'E',
    scope: {
      model: '='
    },
    link: function(scope, element, attrs) {
      var columns = angular.copy(columnsConfig);

      // Special Sauce: Allow columns to have an angular template
      // in place of a regular slick grid formatter function
      angular.forEach(columns, function(column){
        var linker;

        if (angular.isDefined(column.template)) {
          linker = $compile(angular.element('<div>' + column.template + '</div>'));
          delete column.template;

          column.formatter = function(row, cell, value, columnDef, dataContext) {
            var cellScope = scope.$new(true);
            cellScope.value = value;
            cellScope.context = dataContext;

            var e = linker(cellScope);
            cellScope.$apply();
            cellScope.$destroy();

            return e.html();
          };
        }
      });

      var options = {
        enableColumnReorder: false,
        enableTextSelectionOnCells: true,
        autoHeight: true
      };

      var dataView = new Slick.Data.DataView();
      var grid = new Slick.Grid(element, dataView, columns, options);

      dataView.onRowCountChanged.subscribe(function (e, args) {
        grid.updateRowCount();
        grid.render();
      });

      dataView.onRowsChanged.subscribe(function (e, args) {
        grid.invalidateRows(args.rows);
        grid.render();
      });

      scope.$watch('model', function(data) {
        if (angular.isArray(data)) {
          dataView.setItems(data);
        }
      });
    }
  };
}]);

2 个答案:

答案 0 :(得分:4)

好的,所以我需要做同样的事情,并想出一个可以被认为是一个黑客攻击的解决方案(但是没有其他方式AFAIK,因为SlickGrid只处理html字符串,而不是html / jquery对象)。

简而言之,它涉及在格式化程序中编译模板(正如您所做的那样),但除此之外,将生成的对象(不是HTML字符串)存储到字典中,并使用它使用 asyncPostRender http://mleibman.github.io/SlickGrid/examples/example10-async-post-render.html)替换单元格内容。

以下是链接功能的一部分:

var cols = angular.copy(scope.columns);
var templates = new Array();

// Special Sauce: Allow columns to have an angular template
// in place of a regular slick grid formatter function
angular.forEach(cols, function (col) {

    if (angular.isDefined(col.template)) {

        col.formatter = function (row, cell, value, columnDef, dataContext) {

            // Create a new scope, for each cell
            var cellScope = scope.$parent.$new(false);
            cellScope.value = value;
            cellScope.context = dataContext;

            // Interpolate (i.e. turns {{context.myProp}} into its value)
            var interpolated = $interpolate(col.template)(cellScope);

            // Compile the interpolated string into an angular object
            var linker = $compile(interpolated);
            var o = linker(cellScope);

            // Create a guid to identify this object
            var guid = guidGenerator.create();

            // Set this guid to that object as an attribute
            o.attr("guid", guid);

            // Store that Angular object into a dictionary
            templates[guid] = o;

            // Returns the generated HTML: this is just so the grid displays the generated template right away, but if any event is bound to it, they won't work just yet
            return o[0].outerHTML;
        };

        col.asyncPostRender = function(cellNode, row, dataContext, colDef) {

            // From the cell, get the guid generated on the formatter above
            var guid = $(cellNode.firstChild).attr("guid");

            // Get the actual Angular object that matches that guid
            var template = templates[guid];

            // Remove it from the dictionary to free some memory, we only need it once
            delete templates[guid];

            if (template) {
                // Empty the cell node...
                $(cellNode).empty();
                // ...and replace its content by the object (visually this won't make any difference, no flicker, but this one has event bound to it!)
                $(cellNode).append(template);

            } else {
                console.log("Error: template not found");
            }
        };
    }
});

列可以这样定义:

{ name: '', template: '<button ng-click="delete(context)" class="btn btn-danger btn-mini">Delete {{context.user}}</button>', width:80}

context.user将被正确插值(感谢$ interpolate)并且ng-click将起作用,这要归功于$ compile以及我们在asyncPostRender上使用真实对象而不是HTML的事实。

这是完整的指令,后跟HTML和控制器:

指令:

(function() {
    'use strict';

    var app = angular.module('xweb.common');

    // Slick Grid Directive
    app.directive('slickGrid', function ($compile, $interpolate, guidGenerator) {
        return {
            restrict: 'E',
            replace: true,
            template: '<div></div>',
            scope: {
                data:'=',
                options: '=',
                columns: '='
            },
            link: function (scope, element, attrs) {

                var cols = angular.copy(scope.columns);
                var templates = new Array();

                // Special Sauce: Allow columns to have an angular template
                // in place of a regular slick grid formatter function
                angular.forEach(cols, function (col) {

                    if (angular.isDefined(col.template)) {

                        col.formatter = function (row, cell, value, columnDef, dataContext) {

                            // Create a new scope, for each cell
                            var cellScope = scope.$parent.$new(false);
                            cellScope.value = value;
                            cellScope.context = dataContext;

                            // Interpolate (i.e. turns {{context.myProp}} into its value)
                            var interpolated = $interpolate(col.template)(cellScope);

                            // Compile the interpolated string into an angular object
                            var linker = $compile(interpolated);
                            var o = linker(cellScope);

                            // Create a guid to identify this object
                            var guid = guidGenerator.create();

                            // Set this guid to that object as an attribute
                            o.attr("guid", guid);

                            // Store that Angular object into a dictionary
                            templates[guid] = o;

                            // Returns the generated HTML: this is just so the grid displays the generated template right away, but if any event is bound to it, they won't work just yet
                            return o[0].outerHTML;
                        };

                        col.asyncPostRender = function(cellNode, row, dataContext, colDef) {

                            // From the cell, get the guid generated on the formatter above
                            var guid = $(cellNode.firstChild).attr("guid");

                            // Get the actual Angular object that matches that guid
                            var template = templates[guid];

                            // Remove it from the dictionary to free some memory, we only need it once
                            delete templates[guid];

                            if (template) {
                                // Empty the cell node...
                                $(cellNode).empty();
                                // ...and replace its content by the object (visually this won't make any difference, no flicker, but this one has event bound to it!)
                                $(cellNode).append(template);

                            } else {
                                console.log("Error: template not found");
                            }
                        };
                    }
                });

                var container = element;
                var slickGrid = null;
                var dataView = new Slick.Data.DataView();

                var bindDataView = function() {
                    templates = new Array();

                    var index = 0;
                    for (var j = 0; j < scope.data.length; j++) {
                        scope.data[j].data_view_id = index;
                        index++;
                    }

                    dataView.setItems(scope.data, 'data_view_id');
                };

                var rebind = function() {

                    bindDataView();

                    scope.options.enableAsyncPostRender = true;

                    slickGrid = new Slick.Grid(container, dataView, cols, scope.options);
                    slickGrid.onSort.subscribe(function(e, args) {
                        console.log('Sort clicked...');

                        var comparer = function(a, b) {
                            return a[args.sortCol.field] > b[args.sortCol.field];
                        };

                        dataView.sort(comparer, args.sortAsc);
                        scope.$apply();
                    });

                    slickGrid.onCellChange.subscribe(function(e, args) {
                        console.log('Cell changed');
                        console.log(e);
                        console.log(args);
                        args.item.isDirty = true;
                        scope.$apply();
                    });
                };

                rebind();

                scope.$watch('data', function (val, prev) {
                    console.log('SlickGrid ngModel updated');
                    bindDataView();
                    slickGrid.invalidate();
                }, true);

                scope.$watch('columns', function (val, prev) {
                    console.log('SlickGrid columns updated');
                    rebind();
                }, true);

                scope.$watch('options', function (val, prev) {
                    console.log('SlickGrid options updated');
                    rebind();
                }, true);
            }
        };
    });

})();

HTML:

<slick-grid id="slick" class="gridStyle"  data="data" columns="columns" options="options" ></slick-grid>

控制器:

$scope.data = [
            { spreadMultiplier: 1, supAmount: 2, from: "01/01/2013", to: "31/12/2013", user: "jaussan", id: 1000 },
            { spreadMultiplier: 2, supAmount: 3, from: "01/01/2014", to: "31/12/2014", user: "camerond", id: 1001 },
            { spreadMultiplier: 3, supAmount: 4, from: "01/01/2015", to: "31/12/2015", user: "sarkozyn", id: 1002 }
        ];

// SlickGrid Columns definitions
$scope.columns = [
    { name: "Spread Multiplier", field: "spreadMultiplier", id: "spreadMultiplier", sortable: true, width: 100, editor: Slick.Editors.Decimal },
    { name: "Sup Amount", field: "supAmount", id: "supAmount", sortable: true, width: 100, editor: Slick.Editors.Decimal },
    { name: "From", field: "from", id: "from", sortable: true, width: 130, editor: Slick.Editors.Date },
    { name: "To", field: "to", id: "to", sortable: true, width: 130, editor: Slick.Editors.Date },
    { name: "Added By", field: "user", id: "user", sortable: true, width: 200 },
    { name: '', template: '<button ng-click="delete(context)" class="btn btn-danger btn-mini">Delete</button>', width:80}
];

// SlickGrid Options
$scope.options = {
    fullWidthRows: true,
    editable: true,
    selectable: true,
    enableCellNavigation: true,
    rowHeight:30
};

重要

在rebind()方法上,注意

scope.options.enableAsyncPostRender = true;

这一点非常重要,否则永远不会调用asyncPostRender。

此外,为了完整起见,这里是GuidGenerator服务:

app.service('guidGenerator', function() {
        this.create = function () {

            function s4() {
                return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
            }

            function guid() {
                return (s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4());
            }

            return guid();
        };
    });

答案 1 :(得分:0)

我还没有尝试过使用模板,但是我使用的是格式化工具。

在列定义中,我使用了一个字符串作为格式化程序:

// Column definition: 
{id: 'money', name: 'Money', field: 'money', sortable: true, formatter: 'money'}

在指令(或服务[它取决于您的slickgrid实现的架构])中,您可以使用例如:

var val = columns.formatter; // Get the string from the columns definition. Here: 'money'
columns.formatter = that.formatter[val]; // Set the method

// Method in directive or service
this.formatter = {
  //function(row, cell, value, columnDef, dataContext)
  money: function(row, cell, value){
    // Using accounting.js
    return accounting.formatNumber(value, 2, '.', ',');
  }
}

我认为当您在指令中使用相同的方式来实现模板时,它运行正常 顺便说一下:你可以用同样的方式实现slick.grid.editors ......

来自简单的评论声明&#39;: 根据我的经验,当您使用带有css类的指令(列定义:cssClass)时,每次发生事件时都必须使用$ compile(onScroll,aso)......这个解决方案的性能很糟糕......

我在angular中实现格式化程序和编辑器的解决方案并不是很好,但没有很大的性能瓶颈。