【问题标题】:One-off rendering of an angular template string一次性渲染角度模板字符串
【发布时间】:2013-08-22 23:01:12
【问题描述】:

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

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

顺便说一句:我尝试使用 $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);
        }
      });
    }
  };
}]);

【问题讨论】:

  • 这个运气好吗?我也在寻找类似但更复杂的东西,因为我想在我的模板中有一个按钮,这意味着事件处理程序......并且由于格式化程序返回一个 HTML 字符串,所有使用 $compile 构建的事件处理都不会在 HTML 中“渲染”(SlickGrid 应该将元素对象添加到 DOM,而不仅仅是 HTML 字符串......)。换句话说,我认为除非我们修改 slickgrid 代码来处理 DOM/jQuery 对象而不是字符串,否则这是行不通的。

标签: angularjs client-side-templating


【解决方案1】:

好的,所以我需要做几乎相同的事情,并提出了一个可能被认为有点黑客的解决方案(但没有其他方法 AFAIK,因为 SlickGrid 只处理html 字符串,而不是 html/jquery 对象)。

简而言之,它涉及在格式化程序中编译模板(如您所做的那样),但除此之外,将生成的 object(不是 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),并且由于 $compile 以及我们使用真实对象而不是 asyncPostRender 上的 HTML 这一事实,ng-click 将正常工作。

这是完整的指令,后面是 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();
        };
    });

【讨论】:

  • 很棒的答案。我通过使用 asyncPostRender 尝试了类似的操作,但我对单元格的外观并不满意,因为它们开始未格式化,然后逐渐格式化。当为每个单元格触发 asyncPostRender 时,您的解决方案是否会导致任何视觉变化?如果您将 ng-repeat 放在其中一个列模板中会发生什么?
  • 很抱歉没有早点看到您的评论。您不应该看到任何视觉变化,因为代码确实可以正确编译和呈现模板。唯一的问题是当您绑定了某种事件(例如单击按钮)时,如果您在 asyncPostRender 触发之前单击该按钮,则按钮单击将不会触发 :-( 这是时间问题,只会发生如果您的模板有事件,但看不到解决此问题的其他方法。
  • 您可以在单元格上使用类名作为指令的触发器,而不是将模板放在单元格中,然后告诉 Angular 解析模板。然后,您的指令拥有模板,您可以更好地控制未解析内容“FOUT”何时发生。
【解决方案2】:

我没有尝试使用模板,但我使用了 angular 中的格式化程序。

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

// 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...

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

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

【讨论】:

    猜你喜欢
    • 2017-12-27
    • 2012-02-17
    • 1970-01-01
    • 2018-07-29
    • 2014-07-25
    • 1970-01-01
    • 2013-08-11
    • 2018-08-19
    • 2017-11-27
    相关资源
    最近更新 更多