One-off rendering of an angular template string

前端 未结 2 532
暗喜
暗喜 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:27

    I haven't tried to use a template, but I use the formatter in angular.

    In the columns definition I used a string for the formatter:

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

    In the directive (or service [It depends of your architecture of your slickgrid implementation]) you could use for example:

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

    I think when you use the same way in a directive to implement a template it just runs fine.
    Btw: You could implement slick.grid.editors the same way...

    Statement to the Comment from 'Simple As Could Be': In my experience when you use a directive with a css class (Columns definition: cssClass) you have to use $compile everytime an event happen (onScroll, aso)... The performance is terrible with this solution...

    My solution of implementing formatters and editors in angular is not great but there is no big performance bottleneck.

    0 讨论(0)
  • 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: '<button ng-click="delete(context)" class="btn btn-danger btn-mini">Delete {{context.user}}</button>', 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: '<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);
                }
            };
        });
    
    })();
    

    The HTML:

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

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

    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();
            };
        });
    
    0 讨论(0)
提交回复
热议问题