One-off rendering of an angular template string

前端 未结 2 555
暗喜
暗喜 2021-01-06 23:44

I am writing a directive to integrate SlickGrid with my angular app. I want to be able to configure SlickGrid columns with an angular template (instead of a formatter functi

2条回答
  •  一个人的身影
    2021-01-07 00:43

    Ok so I needed to do pretty much the same thing, and came up with a solution that could be considered a bit of a hack (but there's no other way AFAIK, since SlickGrid only deals with html string, not html/jquery objects).

    In a nutshell, it involves compiling the template in the formatter (as you did), but in addition to that, stores the generated object (not the HTML string) into a dictionnary, and use it to replace the cell content by using asyncPostRender (http://mleibman.github.io/SlickGrid/examples/example10-async-post-render.html).

    Here is the part of the link function that is of interest here:

    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");
                }
            };
        }
    });
    

    The column can be defined as such:

    { name: '', template: '', width:80}
    

    The context.user will be properly interpolated (thanks to $interpolate) and the ng-click will be working thanks to $compile and the fact that we use the real object and not the HTML on the asyncPostRender.

    This is the full directive, followed by the HTML and the controller:

    Directive:

    (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: '
    ', 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); } }; }); })();

    The HTML:

    
    

    The controller:

    $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: '', width:80}
    ];
    
    // SlickGrid Options
    $scope.options = {
        fullWidthRows: true,
        editable: true,
        selectable: true,
        enableCellNavigation: true,
        rowHeight:30
    };
    

    Important:

    on the rebind() method, notice the

    scope.options.enableAsyncPostRender = true;
    

    This is very important to have that, otherwise the asyncPostRender is never called.

    Also, for the sake of completeness, here is the GuidGenerator service:

    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();
            };
        });
    

提交回复
热议问题