Skip to content

Commit

Permalink
feat: add titleFormatter to Tree Data
Browse files Browse the repository at this point in the history
- this basically runs a Formatter into the Tree Formatter
- also remove `indentedChildValuePrefix` that can easily be done in this new titleFormatter
  • Loading branch information
ghiscoding committed May 11, 2021
1 parent bb3d488 commit 8bf32ca
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 70 deletions.
25 changes: 13 additions & 12 deletions examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,23 @@ export class Example5 {
columnId: 'title',
parentPropName: 'parentId',
// this is optional, you can define the tree level property name that will be used for the sorting/indentation, internally it will use "__treeLevel"
// levelPropName: 'indent',

// you can add an optional prefix to all the child values
indentedChildValuePrefix: '<span class="mdi mdi-subdirectory-arrow-right mdi-v-align-sub color-se-secondary"></span>',
levelPropName: 'treeLevel',
indentMarginLeft: 15,

// you can optionally sort by a different column and/or sort direction
// this is the recommend approach, unless you are 100% that your original array is already sorted (in most cases it's not)
initialSort: {
columnId: 'title',
direction: 'ASC'
}
},
// we can also add a custom Formatter just for the title text portion
titleFormatter: (_row, _cell, value, _def, dataContext) => {
let prefix = '';
if (dataContext.treeLevel > 0) {
prefix = `<span class="mdi mdi-subdirectory-arrow-right mdi-v-align-sub color-se-secondary"></span>`;
}
return `${prefix}<span class="bold">${value}</span><span style="font-size:11px; margin-left: 15px;">(parentId: ${dataContext.parentId})</span>`;
},
},
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
presets: {
Expand All @@ -152,7 +157,7 @@ export class Example5 {
addNewRow() {
const newId = this.sgb.dataset.length;
const parentPropName = 'parentId';
const treeLevelPropName = '__treeLevel'; // if undefined in your options, the default prop name is "__treeLevel"
const treeLevelPropName = 'treeLevel'; // if undefined in your options, the default prop name is "__treeLevel"
const newTreeLevel = 1;
// find first parent object and add the new item as a child
const childItemFound = this.sgb.dataset.find((item) => item[treeLevelPropName] === newTreeLevel);
Expand All @@ -162,7 +167,7 @@ export class Example5 {
const newItem = {
id: newId,
parentId: parentItemFound.id,
title: this.formatTitle(newId, parentItemFound.id),
title: `Task ${newId}`,
duration: '1 day',
percentComplete: 99,
start: new Date(),
Expand Down Expand Up @@ -227,7 +232,7 @@ export class Example5 {

item['id'] = i;
item['parentId'] = parentId;
item['title'] = this.formatTitle(i, parentId);
item['title'] = `Task ${i}`;
item['duration'] = '5 days';
item['percentComplete'] = Math.round(Math.random() * 100);
item['start'] = new Date(randomYear, randomMonth, randomDay);
Expand All @@ -239,8 +244,4 @@ export class Example5 {
}
return data;
}

formatTitle(taskId: number, parentId: number) {
return `<span style="font-weight:500">Task ${taskId}</span> <span style="font-size:11px; margin-left: 15px;">(parentId: ${parentId})</span>`;
}
}
67 changes: 45 additions & 22 deletions packages/common/src/formatters/__tests__/treeFormatter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('Tree Formatter', () => {
{ id: 5, firstName: 'Sponge', lastName: 'Bob', fullName: 'Sponge Bob', email: 'sponge.bob@cartoon.com', address: { zip: 888888 }, parentId: 2, indent: 3, __collapsed: true },
];
mockGridOptions = {
treeDataOptions: { levelPropName: 'indent', indentedChildValuePrefix: '' }
treeDataOptions: { levelPropName: 'indent' }
} as GridOption;
jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions);
});
Expand Down Expand Up @@ -62,7 +62,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[0]);

const output = treeFormatter(1, 1, dataset[0]['firstName'], {} as Column, dataset[0], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle"></span>John`);
expect(output).toEqual({
addClasses: 'slick-tree-level-0',
text: `<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle"></span><span class="slick-tree-title" level="0">John</span>`
});
});

it('should return a span without any icon and 15px indentation of a tree level 1', () => {
Expand All @@ -71,7 +74,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:15px;"></span><span class="slick-group-toggle"></span>Jane`);
expect(output).toEqual({
addClasses: 'slick-tree-level-1',
text: `<span style="display:inline-block; width:15px;"></span><span class="slick-group-toggle"></span><span class="slick-tree-title" level="1">Jane</span>`
});
});

it('should return a span without any icon and 30px indentation of a tree level 2', () => {
Expand All @@ -80,7 +86,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeFormatter(1, 1, dataset[2]['firstName'], {} as Column, dataset[2], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:30px;"></span><span class="slick-group-toggle"></span>Bob`);
expect(output).toEqual({
addClasses: 'slick-tree-level-2',
text: `<span style="display:inline-block; width:30px;"></span><span class="slick-group-toggle"></span><span class="slick-tree-title" level="2">Bob</span>`
});
});

it('should return a span with expanded icon and 15px indentation of a tree level 1 when current item is greater than next item', () => {
Expand All @@ -89,7 +98,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);

const output = treeFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:15px;"></span><span class="slick-group-toggle expanded"></span>Jane`);
expect(output).toEqual({
addClasses: 'slick-tree-level-1',
text: `<span style="display:inline-block; width:15px;"></span><span class="slick-group-toggle expanded"></span><span class="slick-tree-title" level="1">Jane</span>`
});
});

it('should return a span with collapsed icon and 0px indentation of a tree level 0 when current item is lower than next item', () => {
Expand All @@ -98,27 +110,29 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeFormatter(1, 1, dataset[3]['firstName'], {} as Column, dataset[3], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span>Barbara`);
expect(output).toEqual({
addClasses: 'slick-tree-level-0',
text: `<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span><span class="slick-tree-title" level="0">Barbara</span>`
});
});

it('should return a span with expanded icon and 15px indentation of a tree level 1 with a value prefix when provided', () => {
mockGridOptions.treeDataOptions.indentedChildValuePrefix = '<span class="mdi mdi-subdirectory-arrow-right"></span>';
mockGridOptions.treeDataOptions.levelPropName = 'indent';
mockGridOptions.treeDataOptions.titleFormatter = (_row, _cell, value, _def, dataContext) => {
if (dataContext.indent > 0) {
return `<span class="mdi mdi-subdirectory-arrow-right"></span>${value}`;
}
return value || '';
};
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);

const output = treeFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:15px;"></span><span class="slick-group-toggle expanded"></span><span class="mdi mdi-subdirectory-arrow-right"></span>Jane`);
});

it('should return a span with collapsed icon and 30px indentation of a tree level 2 when current item is lower than next item', () => {
mockGridOptions.treeDataOptions.indentedChildValuePrefix = '<span class="mdi mdi-subdirectory-arrow-right"></span>';
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[5]);

const output = treeFormatter(1, 1, dataset[2]['firstName'], {} as Column, dataset[2], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:30px;"></span><span class="slick-group-toggle collapsed"></span><span class="mdi mdi-subdirectory-arrow-right"></span>Bob`);
const output = treeFormatter(1, 1, { ...dataset[1]['firstName'], indent: 1 }, { field: 'firstName' } as Column, dataset[1], gridStub);
expect(output).toEqual({
addClasses: 'slick-tree-level-1',
text: `<span style="display:inline-block; width:15px;"></span><span class="slick-group-toggle expanded"></span><span class="slick-tree-title" level="1"><span class="mdi mdi-subdirectory-arrow-right"></span>Jane</span>`
});
});

it('should execute "queryFieldNameGetterFn" callback to get field name to use when it is defined', () => {
Expand All @@ -128,7 +142,10 @@ describe('Tree Formatter', () => {

const mockColumn = { id: 'firstName', field: 'firstName', queryFieldNameGetterFn: () => 'fullName' } as Column;
const output = treeFormatter(1, 1, null, mockColumn as Column, dataset[3], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span>Barbara Cane`);
expect(output).toEqual({
addClasses: 'slick-tree-level-0',
text: `<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span><span class="slick-tree-title" level="0">Barbara Cane</span>`
});
});

it('should execute "queryFieldNameGetterFn" callback to get field name and also apply html encoding when output value includes a character that should be encoded', () => {
Expand All @@ -138,7 +155,10 @@ describe('Tree Formatter', () => {

const mockColumn = { id: 'firstName', field: 'firstName', queryFieldNameGetterFn: () => 'fullName' } as Column;
const output = treeFormatter(1, 1, null, mockColumn as Column, dataset[4], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span>Anonymous &lt; Doe`);
expect(output).toEqual({
addClasses: 'slick-tree-level-0',
text: `<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span><span class="slick-tree-title" level="0">Anonymous &lt; Doe</span>`
});
});

it('should execute "queryFieldNameGetterFn" callback to get field name, which has (.) dot notation reprensenting complex object', () => {
Expand All @@ -148,6 +168,9 @@ describe('Tree Formatter', () => {

const mockColumn = { id: 'zip', field: 'zip', queryFieldNameGetterFn: () => 'address.zip' } as Column;
const output = treeFormatter(1, 1, null, mockColumn as Column, dataset[3], gridStub);
expect(output).toBe(`<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span>444444`);
expect(output).toEqual({
addClasses: 'slick-tree-level-0',
text: `<span style="display:inline-block; width:0px;"></span><span class="slick-group-toggle collapsed"></span><span class="slick-tree-title" level="0">444444</span>`
});
});
});
27 changes: 16 additions & 11 deletions packages/common/src/formatters/treeFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { SlickDataView, Formatter } from './../interfaces/index';
import { getDescendantProperty, sanitizeTextByAvailableSanitizer } from '../services/utilities';
import { parseFormatterWhenExist } from '../services';

/** Formatter that must be use with a Tree Data column */
export const treeFormatter: Formatter = (_row, _cell, value, columnDef, dataContext, grid) => {
export const treeFormatter: Formatter = (row, cell, value, columnDef, dataContext, grid) => {
const dataView = grid.getData<SlickDataView>();
const gridOptions = grid.getOptions();
const treeDataOptions = gridOptions?.treeDataOptions;
const treeLevelPropName = treeDataOptions?.levelPropName ?? '__treeLevel';
const collapsedPropName = treeDataOptions?.collapsedPropName ?? '__collapsed';
const indentMarginLeft = treeDataOptions?.indentMarginLeft ?? 15;
const treeLevelPropName = treeDataOptions?.levelPropName ?? '__treeLevel';
let outputValue = value;

if (typeof columnDef.queryFieldNameGetterFn === 'function') {
Expand All @@ -27,22 +29,25 @@ export const treeFormatter: Formatter = (_row, _cell, value, columnDef, dataCont
}

if (dataView?.getItemByIdx) {
const sanitizedOutputValue = sanitizeTextByAvailableSanitizer(gridOptions, outputValue);
const identifierPropName = dataView.getIdPropertyName() ?? 'id';
const treeLevel = dataContext[treeLevelPropName] || 0;
const spacer = `<span style="display:inline-block; width:${indentMarginLeft * treeLevel}px;"></span>`;
const indentSpacer = `<span style="display:inline-block; width:${indentMarginLeft * treeLevel}px;"></span>`;
const idx = dataView.getIdxById(dataContext[identifierPropName]);
const nextItemRow = dataView.getItemByIdx((idx || 0) + 1);
const valuePrefix = treeLevel > 0 ? treeDataOptions?.indentedChildValuePrefix ?? '' : '';
const slickTreeLevelClass = `slick-tree-level-${treeLevel}`;
let toggleClass = '';

if (nextItemRow?.[treeLevelPropName] > treeLevel) {
if (dataContext.__collapsed) {
return `${spacer}<span class="slick-group-toggle collapsed"></span>${valuePrefix}${sanitizedOutputValue}`;
} else {
return `${spacer}<span class="slick-group-toggle expanded"></span>${valuePrefix}${sanitizedOutputValue}`;
}
toggleClass = dataContext?.[collapsedPropName] ? 'collapsed' : 'expanded'; // parent with child will have a toggle icon
}

if (treeDataOptions?.titleFormatter) {
outputValue = parseFormatterWhenExist(treeDataOptions.titleFormatter, row, cell, dataContext, columnDef, grid);
}
return `${spacer}<span class="slick-group-toggle"></span>${valuePrefix}${sanitizedOutputValue}`;
const sanitizedOutputValue = sanitizeTextByAvailableSanitizer(gridOptions, outputValue, { ADD_ATTR: ['target'] });
const spanToggleClass = `slick-group-toggle ${toggleClass}`.trim();
const outputHtml = `${indentSpacer}<span class="${spanToggleClass}"></span><span class="slick-tree-title" level="${treeLevel}">${sanitizedOutputValue}</span>`;
return { addClasses: slickTreeLevelClass, text: outputHtml };
}
return '';
};
12 changes: 7 additions & 5 deletions packages/common/src/interfaces/treeDataOption.interface.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Aggregator } from './aggregator.interface';
// import { Aggregator } from './aggregator.interface';
import { SortDirection, SortDirectionString } from '../enums/index';
import { Formatter } from './formatter.interface';

export interface TreeDataOption {
/** Column Id of which column in the column definitions has the Tree Data, there can only be one with a Tree Data. */
columnId: string;

/** Grouping Aggregators array */
aggregators?: Aggregator[];
// NOT YET IMPLEMENTED
// aggregators?: Aggregator[];

/** Optionally define the initial sort column and direction */
initialSort?: {
Expand All @@ -29,9 +31,6 @@ export interface TreeDataOption {
/** Defaults to "__parentId", object property name used to designate the Parent Id */
parentPropName?: string;

/** Defaults to empty string, add an optional prefix to each of the child values (in other words, add a prefix to all values which have at tree level indentation greater than 0) */
indentedChildValuePrefix?: string;

/** Defaults to "__treeLevel", object property name used to designate the Tree Level depth number */
levelPropName?: string;

Expand All @@ -52,4 +51,7 @@ export interface TreeDataOption {
* and if we add a regular character like a dot then it keeps all tree level indentation spaces
*/
exportIndentationLeadingChar?: string;

/** Optional Title Formatter (allows you to format/style the title text differently) */
titleFormatter?: Formatter;
}
47 changes: 37 additions & 10 deletions packages/common/src/services/export-utilities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { Column, ExcelExportOption, Formatter, SlickGrid, TextExportOption } from '../interfaces/index';

/**
* Goes through every possible ways to find and apply a Formatter when found,
* it will first check if a `exportCustomFormatter` is defined else it will check if there's a regular `formatter` and `exportWithFormatter` is enabled.
* This function is similar to `applyFormatterWhenDefined()` except that it execute any `exportCustomFormatter` while `applyFormatterWhenDefined` does not.
* @param {Number} row - grid row index
* @param {Number} col - grid column index
* @param {Object} dataContext - item data context object
* @param {Object} columnDef - column definition
* @param {Object} grid - Slick Grid object
* @param {Object} exportOptions - Excel or Text Export Options
* @returns formatted string output or empty string
*/
export function exportWithFormatterWhenDefined(row: number, col: number, dataContext: any, columnDef: Column, grid: SlickGrid, exportOptions?: TextExportOption | ExcelExportOption) {
let output = '';
let isEvaluatingFormatter = false;

// first check if there are any export options provided (as Grid Options)
Expand All @@ -14,6 +25,31 @@ export function exportWithFormatterWhenDefined(row: number, col: number, dataCon
isEvaluatingFormatter = !!columnDef.exportWithFormatter;
}

let formatter: Formatter | undefined;
if (dataContext && columnDef.exportCustomFormatter) {
// did the user provide a Custom Formatter for the export
formatter = columnDef.exportCustomFormatter;
} else if (isEvaluatingFormatter && columnDef.formatter) {
// or else do we have a column Formatter AND are we evaluating it?
formatter = columnDef.formatter;
}

return parseFormatterWhenExist(formatter, row, col, dataContext, columnDef, grid);
}

/**
* Takes a Formatter function, execute and return the formatted output
* @param {Function} formatter - formatter function
* @param {Number} row - grid row index
* @param {Number} col - grid column index
* @param {Object} dataContext - item data context object
* @param {Object} columnDef - column definition
* @param {Object} grid - Slick Grid object
* @returns formatted string output or empty string
*/
export function parseFormatterWhenExist(formatter: Formatter<any> | undefined, row: number, col: number, dataContext: any, columnDef: Column, grid: SlickGrid): string {
let output = '';

// does the field have the dot (.) notation and is a complex object? if so pull the first property name
const fieldId = columnDef.field || columnDef.id || '';
let fieldProperty = fieldId;
Expand All @@ -24,15 +60,6 @@ export function exportWithFormatterWhenDefined(row: number, col: number, dataCon

const cellValue = dataContext.hasOwnProperty(fieldProperty) ? dataContext[fieldProperty] : null;

let formatter: Formatter | undefined;
if (dataContext && columnDef.exportCustomFormatter) {
// did the user provide a Custom Formatter for the export
formatter = columnDef.exportCustomFormatter;
} else if (isEvaluatingFormatter && columnDef.formatter) {
// or else do we have a column Formatter AND are we evaluating it?
formatter = columnDef.formatter;
}

if (typeof formatter === 'function') {
const formattedData = formatter(row, col, cellValue, columnDef, dataContext, grid);
output = formattedData as string;
Expand Down
Loading

0 comments on commit 8bf32ca

Please sign in to comment.