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