Skip to content

Commit

Permalink
feat: enhance Excel valueParserCallback with dataContext & new demo (
Browse files Browse the repository at this point in the history
…#350)

* feat(export): enhance `valueParserCallback` with dataContext & new demo
  • Loading branch information
ghiscoding committed Jun 8, 2024
1 parent abd9356 commit e1e6c0d
Show file tree
Hide file tree
Showing 8 changed files with 889 additions and 18 deletions.
107 changes: 90 additions & 17 deletions docs/grid-functionalities/export-to-excel.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,30 @@ Below is a preview of the previous customizations shown above
![image](https://user-images.githubusercontent.com/643976/208590003-b637dcda-5164-42cc-bfad-e921a22c1837.png)
### Cell Format Auto-Detect Disable
##### requires `v3.2.0` or higher
The system will auto-detect the Excel format to use for Date and Number field types, if for some reason you wish to disable it then you provide the excel export options below
```ts
// via column
this.columnDefinitions = [
{
id: 'cost', name: 'Cost', field: 'cost', type: FieldType.number
excelExportOptions: { autoDetectCellFormat: false }
}
];

// OR via grid options (column option always win)
this.gridOptions = {
// ...
excelExportOptions: { autoDetectCellFormat: false }
};
```
### Cell Value Parser
This is not recommended but if you have no other ways, you can also provide a cell value parser function callback to override what the system detected.
This is not recommended but if you have no other ways, you can also provide a `valueParserCallback` callback function to override what the system detected. This callback function is available for both `excelExportOptions` (regular cells) and `groupTotalsExcelExportOptions` (grouping total cells)
> **Note** the original implementation of both `valueParserCallback` had separate arguments but that expanded into way too many arguments than original planned and so I decided to merge them into a single `args` which includes base arguments (`columnDef`, `gridOptions`, `excelFormatId`, `stylesheet`, `dataRowIdx`, and depending on the type you will also have `dataContext` for regular cell OR `groupType` for grouping cell)
```ts
this.columnDefinitions = [
Expand All @@ -354,20 +376,27 @@ this.columnDefinitions = [
groupTotalsFormatter: GroupTotalFormatters.sumTotalsCurrency,
params: { displayNegativeNumberWithParentheses: true, currencyPrefix: '', groupFormatterCurrencyPrefix: '', minDecimal: 2, maxDecimal: 4, groupFormatterPrefix: '<b>Total</b>: ' },
excelExportOptions: {
valueParserCallback: (data, col, excelFormatterId, excelStylesheet) => {
// for version <=5.1
// valueParserCallback: (data, col, excelFormatId, excelStylesheet) => {

// new args signature requires version >=5.1
valueParserCallback: (data, { columnDef, excelFormatId, stylesheet }) => {
// when returned as string, it will skip Excel style format
return `Total: ${data}`;

// to keep Excel style format, you can use detected "excelFormatterId" OR use "excelStylesheet.createFormat()"
// to keep Excel style format, you can use detected "excelFormatId" OR use "excelStylesheet.createFormat()"
return {
value: isNaN(data as number) ? data : +data,
metadata: { style: excelFormatterId } // the excelFormatterId was created internally from the custom format
metadata: { style: excelFormatId } // the excelFormatId was created internally from the custom format
};
}
},
groupTotalsExcelExportOptions: {
valueParserCallback: (totals, columnDef) => {
const groupType = 'sum';
// for version <=5.1
// valueParserCallback: (totals, columnDef) => {

// new args signature requires version >=5.1
valueParserCallback: (totals, { columnDef, groupType }) => {
const fieldName = columnDef.field;
return totals[groupType][fieldName];
},
Expand All @@ -376,21 +405,65 @@ this.columnDefinitions = [
];
```
### Cell Format Auto-Detect Disable
The system will auto-detect the Excel format to use for Date and Number field types, if for some reason you wish to disable it then you provide the excel export options below
By using `valueParserCallback`, there a lot of extra customizations that you can do with it. You could even use Excel Formula to do calculation even based on other fields on your item data context, the code below is calculating Sub-Total and Total. It's a lot of code but it shows the real power customization that exist. If you want to go with even more customization, the new [Example 36](https://ghiscoding.github.io/slickgrid-react/#/example36) even shows you how to summarize Groups with Excel Formulas (but be warned, it does take a fair amount of code and logic to implement by yourself)
```ts
// via column
this.columnDefinitions = [
{
id: 'cost', name: 'Cost', field: 'cost', type: FieldType.number
excelExportOptions: { autoDetectCellFormat: false }
id: 'cost', name: 'Cost', field: 'cost', width: 80,
type: FieldType.number,

// use Formatters in the UI
formatter: Formatters.dollar,
groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollar,

// but use the parser callback to customize our Excel export by using Excel Formulas
excelExportOptions: {
// you can also style the Excel cells (note again that HTML color "#" is escaped as "FF" prefix)
style: {
font: { bold: true, color: 'FF215073' },
format: '$0.00', // currency dollar format
},
width: 12,
valueParserCallback: (_data, { columnDef, excelFormatId, dataRowIdx, dataContext }) => {
// assuming that we want to calculate: (Price * Qty) => Sub-Total
const colOffset = !this.isDataGrouped ? 1 : 0; // col offset of 1x because we skipped 1st column OR 0 offset if we use a Group because the Group column replaces the skip
const rowOffset = 3; // row offset of 3x because: 1x Title, 1x Headers and Excel row starts at 1 => 3
const priceIdx = this.sgb.slickGrid?.getColumnIndex('price') || 0;
const qtyIdx = this.sgb.slickGrid?.getColumnIndex('qty') || 0;
const taxesIdx = this.sgb.slickGrid?.getColumnIndex('taxes') || 0;

// the code below calculates Excel column position dynamically, technically Price is at "B" and Qty is "C"
// Note: if you know the Excel column (A, B, C, ...) then portion of the code below could be skipped (the code below is fully dynamic)
const excelPriceCol = `${String.fromCharCode('A'.charCodeAt(0) + priceIdx - colOffset)}${dataRowIdx + rowOffset}`;
const excelQtyCol = `${String.fromCharCode('A'.charCodeAt(0) + qtyIdx - colOffset)}${dataRowIdx + rowOffset}`;
const excelTaxesCol = `${String.fromCharCode('A'.charCodeAt(0) + taxesIdx - colOffset)}${dataRowIdx + rowOffset}`;

// `value` is our Excel cells to calculat (e.g.: "B4*C4")
// metadata `type` has to be set to "formula" and the `style` is what we defined in `excelExportOptions.style` which is `excelFormatId` in the callback arg

let excelVal = '';
switch (columnDef.id) {
case 'subTotal':
excelVal = `${excelPriceCol}*${excelQtyCol}`; // like "C4*D4"
break;
case 'taxes':
excelVal = (dataContext.taxable)
? `${excelPriceCol}*${excelQtyCol}*${this.taxRate / 100}`
: '';
break;
case 'total':
excelVal = `(${excelPriceCol}*${excelQtyCol})+${excelTaxesCol}`;
break;
}

// use "formula" as "metadata", the "style" is a formatter id that comes from any custom "style" defined outside of our callback
return { value: excelVal, metadata: { type: 'formula', style: excelFormatId } };
}
},
}
];

// OR via grid options (column option always win)
this.gridOptions = {
// ...
excelExportOptions: { autoDetectCellFormat: false }
};
```
#### use Excel Formulas to calculate Totals by using other dataContext props
![image](https://github.com/ghiscoding/slickgrid-universal/assets/643976/871c2d84-33b2-41af-ac55-1f7eadb79cb8)
33 changes: 33 additions & 0 deletions docs/migrations/migration-to-5.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,36 @@ The migration to Tempo should be transparent to most users like you. **However**
### Smaller Size - Full ESM

To compare size, you can take a look at BundlePhobia for the older [v1.4.0](https://bundlephobia.com/package/@slickgrid-universal/common@1.4.0) (when we required jQuery/jQueryUI) and [v5.0.0](https://bundlephobia.com/package/@slickgrid-universal/common@1.4.0) and you'll see that the gzip version went down by 17Kb (or 8.9%) and that's just for 1 dependency of the Slickgrid-Universal workspace (there's a few more installed behind the scene, like the footer component, binding, utils, ...). From that website you can also see that removing MomentJS & Flatpickr had a significant impact especially since the replacements are ESM and tree shakable.

### Excel Export `valueParserCallback`
I would be very suprised if anyone used this advanced feature BUT in case you do, I have decided to make some breaking in v5.1 and higher for the Excel cell value parser after creating the new [Example 36](https://ghiscoding.github.io/slickgrid-react/#/example36) and that is mostly because we now have way too many arguments provided to these value parsers. So, this mean that both `excelExportOptions.valueParserCallback` and `groupTotalsExcelExportOptions.valueParserCallback` signatures changed as shown below.

```diff
this.columnDefinitions = [
{
id: 'cost', name: 'Cost', field: 'cost', width: 80,
excelExportOptions: {
- valueParserCallback: (data, col, excelFormatId, excelStylesheet) => {
+ valueParserCallback: (data, { columnDef, excelFormatId, stylesheet }) => {
return `Total: ${data}`;

// to keep Excel style format, you can use detected "excelFormatId" OR use "stylesheet.createFormat()"
return {
value: isNaN(data as number) ? data : +data,
metadata: { style: excelFormatId } // the excelFormatId was created internally from the custom format
};
}
},
groupTotalsExcelExportOptions: {
- valueParserCallback: (totals, columnDef, groupType, excelFormatId) => {
+ valueParserCallback: (totals, { columnDef, excelFormatId, groupType, stylesheet }) => {
const fieldName = columnDef.field;
//return totals[groupType][fieldName];

// or with metadata
return { value: `totals[groupType][fieldName]`, metadata: { style: excelFormatId } };
},
}
}
];
```
2 changes: 2 additions & 0 deletions src/examples/slickgrid/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import Example32 from './Example32';
import Example33 from './Example33';
import Example34 from './Example34';
import Example35 from './Example35';
import Example36 from './Example36';

const routes: Array<{ path: string; route: string; component: any; title: string; }> = [
{ path: 'example1', route: '/example1', component: <Example1 />, title: '1- Basic Grid / 2 Grids' },
Expand Down Expand Up @@ -70,6 +71,7 @@ const routes: Array<{ path: string; route: string; component: any; title: string
{ path: 'example33', route: '/example33', component: <Example33 />, title: '33- Regular & Custom Tooltip' },
{ path: 'example34', route: '/example34', component: <Example34 />, title: '34- Real-Time Trading Platform' },
{ path: 'example35', route: '/example35', component: <Example35 />, title: '35- Row Based Editing' },
{ path: 'example36', route: '/example36', component: <Example36 />, title: '36- Excel Export Formulas' },
];

export default function Routes() {
Expand Down
1 change: 0 additions & 1 deletion src/examples/slickgrid/Example35.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ class Example35 extends React.Component<Props, State> {

reactGridReady(reactGrid: SlickgridReactInstance) {
this.reactGrid = reactGrid;
this.gridObj = reactGrid.slickGrid;
}

/* Define grid Options and Columns */
Expand Down
Loading

0 comments on commit e1e6c0d

Please sign in to comment.