From dc7d7733b3989eba372d4e912b0d3189b59d23ca Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Wed, 15 Nov 2017 17:55:02 -0600 Subject: [PATCH] feat(Excel export): Excel export (#6199) * Initial checkin of export to excel. No examples or unit tests * Eliminate $$hashKey and uidPrefix = 'uiGrid' from export * Sheet name cannot be empty string or it causes export errors. Default to 'Sheet1' * Additional languages * Add documentation * fix syntax causing lint errors. Check if not a tree when exporting to prevent error * Updated example with colors * Update unit tests and fix the lodash version for import to remove ambiguity. --- bower.json | 13 +- grunt/ngdocs.js | 5 +- misc/tutorial/206_exporting_data.ngdoc | 8 + .../410_excel_exporting_data_and_header.ngdoc | 152 ++++++++ src/features/exporter/js/exporter.js | 329 +++++++++++++++++- src/features/exporter/test/exporter.spec.js | 23 +- src/js/i18n/cs.js | 3 + src/js/i18n/da.js | 3 + src/js/i18n/de.js | 3 + src/js/i18n/en.js | 3 + src/js/i18n/es.js | 3 + src/js/i18n/fi.js | 3 + src/js/i18n/fr.js | 3 + src/js/i18n/he.js | 3 + src/js/i18n/hy.js | 3 + src/js/i18n/it.js | 3 + src/js/i18n/nl.js | 3 + src/js/i18n/no.js | 3 + src/js/i18n/sk.js | 3 + 19 files changed, 557 insertions(+), 12 deletions(-) create mode 100644 misc/tutorial/410_excel_exporting_data_and_header.ngdoc diff --git a/bower.json b/bower.json index e02b3d5990..35182f98ef 100644 --- a/bower.json +++ b/bower.json @@ -22,12 +22,21 @@ "test", "tests" ], - "main": ["./ui-grid.css", "./ui-grid.js"], + "main": [ + "./ui-grid.css", + "./ui-grid.js" + ], "devDependencies": { "google-code-prettify": "~1.0.2", "jquery": "~1.8.0", "lodash": "~2.4.1", "pdfmake": "~0.1.8", - "csv-js": "https://github.com/c0bra/CSV-JS.git#~1.0.2" + "csv-js": "https://github.com/c0bra/CSV-JS.git#~1.0.2", + "excel-builder-js": "excelbuilder#^2.0.2", + "jszip": "~2.6.1" + }, + "resolutions": { + "jszip": "2.6.1", + "lodash": "~2.4.1" } } diff --git a/grunt/ngdocs.js b/grunt/ngdocs.js index 4293a44d28..9b02e37685 100644 --- a/grunt/ngdocs.js +++ b/grunt/ngdocs.js @@ -23,7 +23,10 @@ module.exports = { '//ajax.googleapis.com/ajax/libs/angularjs/<%= latestAngular %>/angular-animate.js', 'bower_components/csv-js/csv.js', 'bower_components/pdfmake/build/pdfmake.js', - 'bower_components/pdfmake/build/vfs_fonts.js' + 'bower_components/pdfmake/build/vfs_fonts.js', + 'bower_components/lodash/dist/lodash.min.js', + 'bower_components/jszip/dist/jszip.min.js', + 'bower_components/excel-builder-js/dist/excel-builder.dist.js' ], hiddenScripts: [ '//ajax.googleapis.com/ajax/libs/angularjs/<%= latestAngular %>/angular-animate.js', diff --git a/misc/tutorial/206_exporting_data.ngdoc b/misc/tutorial/206_exporting_data.ngdoc index b256ff8468..4865e60302 100644 --- a/misc/tutorial/206_exporting_data.ngdoc +++ b/misc/tutorial/206_exporting_data.ngdoc @@ -13,6 +13,12 @@ directive on your grid. If you want to export as PDF you need to have installed available through:
  bower install pdfmake  
+If you want to export as Excel you need to have installed Excel-Builder, +available through: +
  bower install lodash
+  bower install jszip#2.6.1
+  bower install excelbuilder  
+ The options and API for exporter can be found at {@link api/ui.grid.exporter ui.grid.exporter}, and - {@link api/ui.grid.exporter.api:ColumnDef columnDef} @@ -73,6 +79,8 @@ In this example we use the native grid menu buttons, and we show both the pdf an exporterPdfPageSize: 'LETTER', exporterPdfMaxGridWidth: 500, exporterCsvLinkElement: angular.element(document.querySelectorAll(".custom-csv-link-location")), + exporterExcelFilename: 'myFile.xlsx', + exporterExcelSheetName: 'Sheet1', onRegisterApi: function(gridApi){ $scope.gridApi = gridApi; } diff --git a/misc/tutorial/410_excel_exporting_data_and_header.ngdoc b/misc/tutorial/410_excel_exporting_data_and_header.ngdoc new file mode 100644 index 0000000000..e4738c0dba --- /dev/null +++ b/misc/tutorial/410_excel_exporting_data_and_header.ngdoc @@ -0,0 +1,152 @@ +@ngdoc overview +@name Tutorial: 410 Exporting Data with Fonts, Colors and Header in Excel +@description + + + +The exporter feature allows data to be exported from the grid in +excel format with a header. The exporter can export all data, visible data or selected data. + +To use the exporter you need to include the ui-grid-exporter directive on +your grid. If you want to export selected rows you must include the ui-grid-selection +directive on your grid. + +If you want to export as Excel you need to have installed Excel-Builder which also uses lodash and jszip, +available through: +
  bower install lodash
+  bower install jszip#2.6.1
+  bower install excelbuilder  
+ +The options and API for exporter can be found at {@link api/ui.grid.exporter ui.grid.exporter}, and + +- {@link api/ui.grid.exporter.api:ColumnDef columnDef} +- {@link api/ui.grid.exporter.api:GridOptions gridOptions} +- {@link api/ui.grid.exporter.api:PublicApi publicApi} + +The exporter adds menu items to the grid menu, to use the native UI you need to enable +the grid menu using the gridOption `enableGridMenu` + +Note that the option to export selected data is only visible if you have data selected. + +Additionally, information about the Excel Builder may be found at https://github.com/stephenliberty/excel-builder.js. + +If the example below we show how to make a custom header based on a callback method and + + +@example +In this example we use the native grid menu buttons, and we show the pdf, csv and excel export options. + + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.selection', 'ui.grid.exporter']); + + app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { + + $scope.formatters = {}; + + $scope.gridOptions = { + columnDefs: [ + { field: 'name' }, + { field: 'gender', visible: false}, + { field: 'company' } + ], + enableGridMenu: true, + enableSelectAll: true, + exporterCsvFilename: 'myFile.csv', + exporterPdfDefaultStyle: {fontSize: 9}, + exporterPdfTableStyle: {margin: [30, 30, 30, 30]}, + exporterPdfTableHeaderStyle: {fontSize: 10, bold: true, italics: true, color: 'red'}, + exporterPdfHeader: { text: "My Header", style: 'headerStyle' }, + exporterPdfFooter: function ( currentPage, pageCount ) { + return { text: currentPage.toString() + ' of ' + pageCount.toString(), style: 'footerStyle' }; + }, + exporterPdfCustomFormatter: function ( docDefinition ) { + docDefinition.styles.headerStyle = { fontSize: 22, bold: true }; + docDefinition.styles.footerStyle = { fontSize: 10, bold: true }; + return docDefinition; + }, + exporterPdfOrientation: 'portrait', + exporterPdfPageSize: 'LETTER', + exporterPdfMaxGridWidth: 500, + exporterCsvLinkElement: angular.element(document.querySelectorAll(".custom-csv-link-location")), + exporterExcelFilename: 'myFile.xlsx', + exporterExcelSheetName: 'Sheet1', + exporterExcelCustomFormatters: function ( grid, workbook, docDefinition ) { + + var stylesheet = workbook.getStyleSheet(); + var aFormatDefn = { + "font": { "size": 9, "fontName": "Calibri", "bold": true }, + "alignment": { "wrapText": true } + }; + var formatter = stylesheet.createFormat(aFormatDefn); + // save the formatter + $scope.formatters['bold'] = formatter; + + aFormatDefn = { + "font": { "size": 9, "fontName": "Calibri" }, + "fill": { "type": "pattern", "patternType": "solid", "fgColor": "FFFFC7CE" }, + "alignment": { "wrapText": true } + }; + formatter = stylesheet.createFormat(aFormatDefn); + // save the formatter + $scope.formatters['red'] = formatter; + + Object.assign(docDefinition.styles , $scope.formatters); + + return docDefinition; + }, + exporterExcelHeader: function (grid, workbook, sheet, docDefinition) { + // this can be defined outside this method + var stylesheet = workbook.getStyleSheet(); + var aFormatDefn = { + "font": { "size": 9, "fontName": "Calibri", "bold": true }, + "alignment": { "wrapText": true } + }; + var formatterId = stylesheet.createFormat(aFormatDefn); + + sheet.mergeCells('B1', 'C1'); + var cols = []; + cols.push({ value: '' }); + cols.push({ value: 'My header that is long enough to wrap', metadata: {style: formatterId.id} }); + sheet.data.push(cols); + }, + exporterFieldFormatCallback: function(grid, row, gridCol, cellValue) { + // set metadata on export data to set format id. See exportExcelHeader config above for example of creating + // a formatter and obtaining the id + var formatterId = null; + if (cellValue && cellValue.startsWith('W')) { + formatterId = $scope.formatters['red'].id; + } + + if (formatterId) { + return {metadata: {style: formatterId}}; + } else { + return null; + } + }, + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + } + }; + + $http.get('/data/100.json') + .success(function(data) { + $scope.gridOptions.data = data; + }); + + }]); + + + +
+
+
+
+ + + .grid { + width: 500px; + height: 400px; + } + +
diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index a7341b0eb1..4d589d4a27 100755 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -1,4 +1,5 @@ /* global pdfMake */ +/* global ExcelBuilder */ /* global console */ (function () { @@ -132,6 +133,21 @@ */ pdfExport: function (rowTypes, colTypes) { service.pdfExport(grid, rowTypes, colTypes); + }, + /** + * @ngdoc function + * @name excelExport + * @methodOf ui.grid.exporter.api:PublicApi + * @description Exports rows from the grid in excel format, + * the data exported is selected based on the provided options + * @param {string} rowTypes which rows to export, valid values are + * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE, + * uiGridExporterConstants.SELECTED + * @param {string} colTypes which columns to export, valid values are + * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE + */ + excelExport: function (rowTypes, colTypes) { + service.excelExport(grid, rowTypes, colTypes); } } } @@ -420,6 +436,14 @@ */ gridOptions.exporterMenuPdf = gridOptions.exporterMenuPdf !== undefined ? gridOptions.exporterMenuPdf : true; + /** + * @ngdoc object + * @name exporterMenuExcel + * @propertyOf ui.grid.exporter.api:GridOptions + * @description Add excel export menu items to the ui-grid grid menu, if it's present. Defaults to true. + */ + gridOptions.exporterMenuExcel = gridOptions.exporterMenuExcel !== undefined ? gridOptions.exporterMenuExcel : true; + /** * @ngdoc object * @name exporterPdfCustomFormatter @@ -506,6 +530,48 @@ */ gridOptions.exporterFieldCallback = gridOptions.exporterFieldCallback ? gridOptions.exporterFieldCallback : function( grid, row, col, value ) { return value; }; + /** + * @ngdoc function + * @name exporterFieldFormatCallback + * @propertyOf ui.grid.exporter.api:GridOptions + * @description A function to call for each field before exporting it. Allows + * general object to be return to modify the format of a cell in the case of + * excel exports + * + * The method is called once for each field exported, and provides the grid, the + * gridCol and the GridRow for you to use as context in massaging the data. + * + * @param {Grid} grid provides the grid in case you have need of it + * @param {GridRow} row the row from which the data comes + * @param {GridCol} col the column from which the data comes + * @param {object} value the value for your massaging + * @returns {object} you must return the massaged value ready for exporting + * + * @example + *
+           *   gridOptions.exporterFieldCallback = function ( grid, row, col, value ){
+           *     if ( col.name === 'status' ){
+           *       value = decodeStatus( value );
+           *     }
+           *     return value;
+           *   }
+           * 
+ */ + gridOptions.exporterFieldFormatCallback = gridOptions.exporterFieldFormatCallback ? gridOptions.exporterFieldFormatCallback : function( grid, row, col, value ) { return null; }; + + /** + * @ngdoc object + * @name exporterFieldApplyFilters + * @propertyOf ui.grid.exporter.api:GridOptions + * @description Defaults to false, which leads to filters being evaluated on export * + * + * @example + *
+           *   gridOptions.exporterFieldApplyFilters = true;
+           * 
+ */ + gridOptions.exporterFieldApplyFilters = gridOptions.exporterFieldApplyFilters === true; + /** * @ngdoc function * @name exporterAllDataFn @@ -540,7 +606,7 @@ * } * */ - if ( gridOptions.exporterAllDataFn == null && gridOptions.exporterAllDataPromise ) { + if ( gridOptions.exporterAllDataFn === null && gridOptions.exporterAllDataPromise ) { gridOptions.exporterAllDataFn = gridOptions.exporterAllDataPromise; } }, @@ -617,6 +683,37 @@ ( grid.api.selection && grid.api.selection.getSelectedRows().length > 0 ); }, order: grid.options.exporterMenuItemOrder + 5 + }, + { + title: i18nService.getSafeText('gridMenu.exporterAllAsExcel'), + action: function ($event) { + this.grid.api.exporter.excelExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL ); + }, + shown: function() { + return this.grid.options.exporterMenuExcel && this.grid.options.exporterMenuAllData; + }, + order: grid.options.exporterMenuItemOrder + 6 + }, + { + title: i18nService.getSafeText('gridMenu.exporterVisibleAsExcel'), + action: function ($event) { + this.grid.api.exporter.excelExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE ); + }, + shown: function() { + return this.grid.options.exporterMenuExcel && this.grid.options.exporterMenuVisibleData; + }, + order: grid.options.exporterMenuItemOrder + 7 + }, + { + title: i18nService.getSafeText('gridMenu.exporterSelectedAsExcel'), + action: function ($event) { + this.grid.api.exporter.excelExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE ); + }, + shown: function() { + return this.grid.options.exporterMenuExcel && this.grid.options.exporterMenuSelectedData && + ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 ); + }, + order: grid.options.exporterMenuItemOrder + 8 } ]); }, @@ -715,14 +812,16 @@ } columns.forEach( function( gridCol, index ) { - if ( gridCol.colDef.exporterSuppressExport !== true && + // $$hashKey check since when grouping and sorting programmatically this ends up in export. Filtering it out + if ( gridCol.colDef.exporterSuppressExport !== true && gridCol.field !== '$$hashKey' && grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){ - headers.push({ + var headerEntry = { name: gridCol.field, displayName: grid.options.exporterHeaderFilter ? ( grid.options.exporterHeaderFilterUseName ? grid.options.exporterHeaderFilter(gridCol.name) : grid.options.exporterHeaderFilter(gridCol.displayName) ) : gridCol.displayName, width: gridCol.drawnWidth ? gridCol.drawnWidth : gridCol.width, - align: gridCol.colDef.type === 'number' ? 'right' : 'left' - }); + align: gridCol.colDef.align ? gridCol.colDef.align : (gridCol.colDef.type === 'number' ? 'right' : 'left') + }; + headers.push(headerEntry); } }); @@ -754,6 +853,63 @@ *
Defaults to true */ + + /** + * @ngdoc function + * @name getRowsFromNode + * @methodOf ui.grid.exporter.service:uiGridExporterService + * @description Gets rows from a node. If the node is grouped it will + * recurse down into the children to get to the raw data element + * which is a row without children (a leaf). + * @param {Node} aNode the tree node on the grid + * @returns {Array} an array of leaf nodes + */ + getRowsFromNode: function(aNode) { + var rows = []; + for (var i = 0; i