Skip to content
This repository was archived by the owner on Jun 1, 2025. It is now read-only.

Commit 23cf70a

Browse files
Ghislain BeaulacGhislain Beaulac
authored andcommitted
fix(formatters): update some Formatter behaviors and add unit testing
- update some Formatters and add new functionality to some of them, mainly: complexObjectFormatter, hyperlinkFormatter - delete "hyperlinkUriPrefixFormatter" since the "hyperlinkFormatter" now covers all behaviors under 1 formatter - a few formatters were behaving incorrectly in some cases, while adding unit tests, we tweaked Formatters to go along the tests
1 parent 921f280 commit 23cf70a

17 files changed

+357
-64
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Column } from '../models';
2+
import { complexObjectFormatter } from './complexObjectFormatter';
3+
4+
describe('the ComplexObject Formatter', () => {
5+
const allRoles = [{ roleId: 0, name: 'Administrator' }, { roleId: 1, name: 'Regular User' }];
6+
7+
const dataset = [
8+
{ id: 0, firstName: 'John', lastName: 'Smith', email: '[email protected]', role: allRoles[0] },
9+
{ id: 1, firstName: 'Jane', lastName: 'Doe', email: '[email protected]', role: allRoles[1] },
10+
{ id: 2, firstName: 'Bob', lastName: 'Cane', email: '[email protected]', role: null },
11+
];
12+
13+
it('should throw an error when omitting to pass "complexFieldLabel" to "params"', () => {
14+
expect(() => complexObjectFormatter(0, 0, 'anything', {} as Column, {}))
15+
.toThrowError('For the Formatters.complexObject to work properly');
16+
});
17+
18+
it('should return empty string when no column definition is provided', () => {
19+
const result = complexObjectFormatter(0, 0, 'anything', null as Column, {});
20+
expect(result).toBe('');
21+
});
22+
23+
it('should return original input value when the "field" property does not include a not ".", neither "complexFieldLabel"', () => {
24+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'role' } as Column, {});
25+
expect(result).toBe('anything');
26+
});
27+
28+
it('should return original input value when the "field" property was not found in the data context object', () => {
29+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'invalid.object' } as Column, dataset[2]);
30+
expect(result).toBe('anything');
31+
});
32+
33+
it('should return original input value when the "complexFieldLabel" does not include a not "." within its string', () => {
34+
const params = { complexFieldLabel: 'name' };
35+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'role', params } as Column, {});
36+
expect(result).toBe('anything');
37+
});
38+
39+
it('should return original input value when the "complexFieldLabel" was not found in the data context object', () => {
40+
const params = { complexFieldLabel: 'invalid.object' };
41+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'role', params } as Column, dataset[2]);
42+
expect(result).toBe('anything');
43+
});
44+
45+
it('should return the value from the complex object when "field" property with dot notation was found in the data context object', () => {
46+
const expectedOutput = 'Administrator';
47+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'role.name' } as Column, dataset[0]);
48+
expect(result).toBe(expectedOutput);
49+
});
50+
51+
it('should return the value from the complex object when "complexFieldLabel" property with dot notation was found in the data context object', () => {
52+
const params = { complexFieldLabel: 'role.name' };
53+
const expectedOutput = 'Administrator';
54+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'role', params } as Column, dataset[0]);
55+
expect(result).toBe(expectedOutput);
56+
});
57+
58+
it('should return the value from the complex object when "complexFieldLabel" is not dot notation but has a "labelKey" was found in the data context object', () => {
59+
const params = { complexFieldLabel: 'role' };
60+
const expectedOutput = 'Administrator';
61+
const result = complexObjectFormatter(0, 0, 'anything', { field: 'role', labelKey: 'name', params } as Column, dataset[0]);
62+
expect(result).toBe(expectedOutput);
63+
});
64+
});
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Column } from './../models/column.interface';
22
import { Formatter } from './../models/formatter.interface';
33

4-
export const complexObjectFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => {
4+
export const complexObjectFormatter: Formatter = (row: number, cell: number, cellValue: any, columnDef: Column, dataContext: any) => {
55
if (!columnDef) {
66
return '';
77
}
@@ -11,14 +11,20 @@ export const complexObjectFormatter: Formatter = (row: number, cell: number, val
1111

1212
if (!complexFieldLabel) {
1313
throw new Error(`For the Formatters.complexObject to work properly, you need to tell it which property of the complex object to use.
14-
You can provide via 2 ways:
14+
There are 3 ways to provide it:
1515
1- via the generic "params" with a "complexFieldLabel" property on your Column Definition, example: this.columnDefs = [{ id: 'user', field: 'user', params: { complexFieldLabel: 'user.firstName' } }]
16-
2- via the field name that includes a dot notation, example: this.columnDefs = [{ id: 'user', field: 'user.firstName'}] `);
16+
2- via the generic "params" with a "complexFieldLabel" and a "labelKey" property on your Column Definition, example: this.columnDefs = [{ id: 'user', field: 'user', labelKey: 'firstName' params: { complexFieldLabel: 'user' } }]
17+
3- via the field name that includes a dot notation, example: this.columnDefs = [{ id: 'user', field: 'user.firstName'}] `);
1718
}
1819

19-
if (columnDef.labelKey) {
20+
if (columnDef.labelKey && dataContext.hasOwnProperty(complexFieldLabel)) {
2021
return dataContext[complexFieldLabel] && dataContext[complexFieldLabel][columnDef.labelKey];
2122
}
2223

23-
return complexFieldLabel.split('.').reduce((obj, i) => (obj ? obj[i] : ''), dataContext);
24+
// when complexFieldLabel includes the dot ".", we will do the split and get the value from the complex object
25+
// however we also need to make sure that the complex objet exist, else we'll return the cell value (original value)
26+
if (typeof complexFieldLabel === 'string' && complexFieldLabel.indexOf('.') > 0) {
27+
return complexFieldLabel.split('.').reduce((obj, i) => (obj && obj.hasOwnProperty(i) ? obj[i] : cellValue), dataContext);
28+
}
29+
return cellValue;
2430
};

src/app/modules/angular-slickgrid/formatters/deleteIconFormatter.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('the Delete Icon Formatter', () => {
77
const result = deleteIconFormatter(0, 0, value, {} as Column, {});
88
expect(result).toBe('<i class="fa fa-trash pointer delete-icon" aria-hidden="true"></i>');
99
});
10+
1011
it('should return the Font Awesome Trash icon when input is filled with any string', () => {
1112
const value = 'anything';
1213
const result = deleteIconFormatter(0, 0, value, {} as Column, {});

src/app/modules/angular-slickgrid/formatters/editIconFormatter.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('the Edit Icon Formatter', () => {
77
const result = editIconFormatter(0, 0, value, {} as Column, {});
88
expect(result).toBe('<i class="fa fa-pencil pointer edit-icon" aria-hidden="true"></i>');
99
});
10+
1011
it('should return the Font Awesome Pencil icon when input is filled with any string', () => {
1112
const value = 'anything';
1213
const result = editIconFormatter(0, 0, value, {} as Column, {});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Column } from '../models';
2+
import { hyperlinkFormatter } from './hyperlinkFormatter';
3+
4+
describe('the Hyperlink Formatter', () => {
5+
it('should return empty string when value is not an hyperlink and is empty', () => {
6+
const result = hyperlinkFormatter(0, 0, '', {} as Column, {});
7+
expect(result).toBe('');
8+
});
9+
10+
it('should return original value when value is not an hyperlink', () => {
11+
const result = hyperlinkFormatter(0, 0, 'anything', {} as Column, {});
12+
expect(result).toBe('anything');
13+
});
14+
15+
it('should return original value when URL passed through the generic params "hyperlinkUrl" is not a valid hyperlink', () => {
16+
const hyperlinkUrl1 = '';
17+
const inputValue = 'Company Name';
18+
const result1 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl1 } } as Column, {});
19+
expect(result1).toBe(inputValue);
20+
});
21+
22+
it('should not permit sanitize/remove any bad script code', () => {
23+
const inputValue = 'http://<script>alert("test")</script>company.com';
24+
const sanitizedValue = 'http://company.com';
25+
const result = hyperlinkFormatter(0, 0, inputValue, {} as Column, {});
26+
expect(result).toBe(`<a href="${sanitizedValue}">${sanitizedValue}</a>`);
27+
});
28+
29+
it('should return original value when value is not a valid hyperlink', () => {
30+
const inputValue1 = 'http:/something.com';
31+
const inputValue2 = 'https//something.com';
32+
const inputValue3 = 'ftpp://something.com';
33+
34+
const result1 = hyperlinkFormatter(0, 0, inputValue1, {} as Column, {});
35+
const result2 = hyperlinkFormatter(0, 0, inputValue2, {} as Column, {});
36+
const result3 = hyperlinkFormatter(0, 0, inputValue3, {} as Column, {});
37+
38+
expect(result1).toBe(inputValue1);
39+
expect(result2).toBe(inputValue2);
40+
expect(result3).toBe(inputValue3);
41+
});
42+
43+
it('should return an href link when input value is a valid hyperlink', () => {
44+
const inputValue1 = 'http://something.com';
45+
const inputValue2 = 'https://something.com';
46+
const inputValue3 = 'ftp://something.com';
47+
48+
const result1 = hyperlinkFormatter(0, 0, inputValue1, {} as Column, {});
49+
const result2 = hyperlinkFormatter(0, 0, inputValue2, {} as Column, {});
50+
const result3 = hyperlinkFormatter(0, 0, inputValue3, {} as Column, {});
51+
52+
53+
expect(result1).toBe(`<a href="${inputValue1}">${inputValue1}</a>`);
54+
expect(result2).toBe(`<a href="${inputValue2}">${inputValue2}</a>`);
55+
expect(result3).toBe(`<a href="${inputValue3}">${inputValue3}</a>`);
56+
});
57+
58+
it('should return an href link with a different text when input value is a valid hyperlink and has the generic params "hyperlinkText" provided', () => {
59+
const inputValue1 = 'http://something.com';
60+
const inputValue2 = 'https://something.com';
61+
const inputValue3 = 'ftp://something.com';
62+
const linkText = 'Company Website';
63+
64+
const result1 = hyperlinkFormatter(0, 0, inputValue1, { params: { hyperlinkText: linkText } } as Column, {});
65+
const result2 = hyperlinkFormatter(0, 0, inputValue2, { params: { hyperlinkText: linkText } } as Column, {});
66+
const result3 = hyperlinkFormatter(0, 0, inputValue3, { params: { hyperlinkText: linkText } } as Column, {});
67+
68+
69+
expect(result1).toBe(`<a href="${inputValue1}">${linkText}</a>`);
70+
expect(result2).toBe(`<a href="${inputValue2}">${linkText}</a>`);
71+
expect(result3).toBe(`<a href="${inputValue3}">${linkText}</a>`);
72+
});
73+
74+
it('should return an href link with a different url than value it is provided as a valid hyperlink through the generic params "hyperlinkUrl"', () => {
75+
const hyperlinkUrl1 = 'http://something.com';
76+
const hyperlinkUrl2 = 'https://something.com';
77+
const hyperlinkUrl3 = 'ftp://something.com';
78+
const inputValue = 'Company Name';
79+
80+
const result1 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl1 } } as Column, {});
81+
const result2 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl2 } } as Column, {});
82+
const result3 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl3 } } as Column, {});
83+
84+
85+
expect(result1).toBe(`<a href="${hyperlinkUrl1}">${inputValue}</a>`);
86+
expect(result2).toBe(`<a href="${hyperlinkUrl2}">${inputValue}</a>`);
87+
expect(result3).toBe(`<a href="${hyperlinkUrl3}">${inputValue}</a>`);
88+
});
89+
90+
it('should return an href link when hyperlink URL & Text are provided through the generic params "hyperlinkUrl" and "hyperlinkText"', () => {
91+
const hyperlinkUrl1 = 'http://something.com';
92+
const hyperlinkUrl2 = 'https://something.com';
93+
const hyperlinkUrl3 = 'ftp://something.com';
94+
const linkText1 = 'Company ABC';
95+
const linkText2 = 'Company DEF';
96+
const linkText3 = 'Company XYZ';
97+
const inputValue = 'anything';
98+
99+
const result1 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl1, hyperlinkText: linkText1 } } as Column, {});
100+
const result2 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl2, hyperlinkText: linkText2 } } as Column, {});
101+
const result3 = hyperlinkFormatter(0, 0, inputValue, { params: { hyperlinkUrl: hyperlinkUrl3, hyperlinkText: linkText3 } } as Column, {}); 3
102+
103+
expect(result1).toBe(`<a href="${hyperlinkUrl1}">${linkText1}</a>`);
104+
expect(result2).toBe(`<a href="${hyperlinkUrl2}">${linkText2}</a>`);
105+
expect(result3).toBe(`<a href="${hyperlinkUrl3}">${linkText3}</a>`);
106+
});
107+
});
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
import { Column } from './../models/column.interface';
22
import { Formatter } from './../models/formatter.interface';
3+
import * as DOMPurify_ from 'dompurify';
4+
const DOMPurify = DOMPurify_; // patch to fix rollup to work
35

6+
/**
7+
* Takes an hyperlink cell value and transforms it into a real hyperlink, given that the value starts with 1 of these (http|ftp|https).
8+
* The structure will be "<a href="hyperlink">hyperlink</a>"
9+
*
10+
* You can optionally change the hyperlink text displayed by using the generic params "hyperlinkText" in the column definition
11+
* For example: { id: 'link', field: 'link', params: { hyperlinkText: 'Company Website' } } will display "<a href="link">Company Website</a>"
12+
*
13+
* You can also optionally provide the hyperlink URL by using the generic params "hyperlinkUrl" in the column definition
14+
* For example: { id: 'link', field: 'link', params: { hyperlinkText: 'Company Website', hyperlinkUrl: 'http://www.somewhere.com' } } will display "<a href="http://www.somewhere.com">Company Website</a>"
15+
*/
416
export const hyperlinkFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => {
5-
if (value && typeof value === 'string') {
6-
const matchUrl = value.match(/^(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:\/~\+#]*[\w\-\@?^=%&amp;\/~\+#])?/i);
7-
if (matchUrl && Array.isArray(matchUrl)) {
8-
return `<a href="${matchUrl[0]}">' + value + '</a>`;
9-
}
17+
const columnParams = columnDef && columnDef.params || {};
18+
19+
let displayedText = columnParams.hyperlinkText ? columnParams.hyperlinkText : value;
20+
displayedText = DOMPurify.sanitize(displayedText || '');
21+
22+
let outputLink = columnParams.hyperlinkUrl ? columnParams.hyperlinkUrl : value;
23+
outputLink = DOMPurify.sanitize(outputLink || '');
24+
25+
const matchUrl = outputLink.match(/^(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:\/~\+#]*[\w\-\@?^=%&amp;\/~\+#])?/i);
26+
27+
if (matchUrl && Array.isArray(matchUrl) && matchUrl.length > 0) {
28+
const finalUrl = matchUrl[0];
29+
return `<a href="${finalUrl}">${displayedText}</a>`;
1030
}
11-
return '';
31+
32+
return value;
1233
};

src/app/modules/angular-slickgrid/formatters/hyperlinkUriPrefixFormatter.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/app/modules/angular-slickgrid/formatters/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { dollarColoredFormatter } from './dollarColoredFormatter';
2121
import { dollarFormatter } from './dollarFormatter';
2222
import { editIconFormatter } from './editIconFormatter';
2323
import { hyperlinkFormatter } from './hyperlinkFormatter';
24-
import { hyperlinkUriPrefixFormatter } from './hyperlinkUriPrefixFormatter';
2524
import { infoIconFormatter } from './infoIconFormatter';
2625
import { italicFormatter } from './italicFormatter';
2726
import { lowercaseFormatter } from './lowercaseFormatter';
@@ -133,12 +132,14 @@ export const Formatters = {
133132
/** Displays a Font-Awesome edit icon (fa-pencil) */
134133
editIcon: editIconFormatter,
135134

136-
/** Takes an hyperlink cell value and transforms it into a real hyperlink, given that the value starts with 1 of these (http|ftp|https). The structure will be "<a href="hyperlink">hyperlink</a>" */
135+
/**
136+
* Takes an hyperlink cell value and transforms it into a real hyperlink, given that the value starts with 1 of these (http|ftp|https).
137+
* The structure will be "<a href="hyperlink">hyperlink</a>"
138+
* You can optionally change the hyperlink text displayed by using the generic params "hyperlinkText" in the column definition
139+
* For example: { id: 'link', field: 'link', params: { hyperlinkText: 'Company Website' } } will display "<a href="link">Company Website</a>"
140+
*/
137141
hyperlink: hyperlinkFormatter,
138142

139-
/** Takes an hyperlink URI prefix (passed in column definition "params.uriPrefix") and adds the cell value. The structure will be "<a href="uriPrefix">value</a>" */
140-
hyperlinkUriPrefix: hyperlinkUriPrefixFormatter,
141-
142143
/** Displays a Font-Awesome edit icon (fa-info-circle) */
143144
infoIcon: infoIconFormatter,
144145

src/app/modules/angular-slickgrid/formatters/infoIconFormatter.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('the Info Icon Formatter', () => {
77
const result = infoIconFormatter(0, 0, value, {} as Column, {});
88
expect(result).toBe('<i class="fa fa-info-circle pointer info-icon" aria-hidden="true"></i>');
99
});
10+
1011
it('should return the Font Awesome Info icon when input is filled with any string', () => {
1112
const value = 'anything';
1213
const result = infoIconFormatter(0, 0, value, {} as Column, {});

src/app/modules/angular-slickgrid/formatters/percentCompleteBarFormatter.spec.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,31 @@ import { Column } from '../models';
22
import { percentCompleteBarFormatter } from './percentCompleteBarFormatter';
33

44
describe('the Percent Complete Formatter', () => {
5-
it('should return an empty string when no value is provided', async () => {
5+
it('should return an empty string when no value is provided', () => {
66
const output = percentCompleteBarFormatter(1, 1, '', {} as Column, {});
77
expect(output).toBe('');
88
});
99

10-
it('should return empty string when non-numeric value is provided', async () => {
10+
it('should return empty string when non-numeric value is provided', () => {
1111
const output = percentCompleteBarFormatter(1, 1, 'hello', {} as Column, {});
1212
expect(output).toBe('');
1313
});
1414

15-
it('should display a red color bar formatter when number 0 is provided', async () => {
15+
it('should display a red color bar formatter when number 0 is provided', () => {
1616
const input = 0;
1717
const color = 'red';
1818
const output = percentCompleteBarFormatter(1, 1, input, {} as Column, {});
1919
expect(output).toBe(`<span class="percent-complete-bar" style="background:${color}; width:${input}%"></span>`);
2020
});
2121

22-
it('should display a red color bar when value is a negative number', async () => {
22+
it('should display a red color bar when value is a negative number', () => {
2323
const input = -15;
2424
const color = 'red';
2525
const output = percentCompleteBarFormatter(1, 1, input, {} as Column, {});
2626
expect(output).toBe(`<span class="percent-complete-bar" style="background:${color}; width:${input}%"></span>`);
2727
});
2828

29-
it('should display a silver color bar when value is between 30 and 69', async () => {
29+
it('should display a silver color bar when value is between 30 and 69', () => {
3030
const input1 = 30;
3131
const input2 = 69;
3232
const color = 'silver';
@@ -36,10 +36,17 @@ describe('the Percent Complete Formatter', () => {
3636
expect(output2).toBe(`<span class="percent-complete-bar" style="background:${color}; width:${input2}%"></span>`);
3737
});
3838

39-
it('should display a green color bar when value greater or equal to 70 and is a type string', async () => {
39+
it('should display a green color bar when value greater or equal to 70 and is a type string', () => {
4040
const input = '70';
4141
const color = 'green';
4242
const output = percentCompleteBarFormatter(1, 1, input, {} as Column, {});
4343
expect(output).toBe(`<span class="percent-complete-bar" style="background:${color}; width:${input}%"></span>`);
4444
});
45+
46+
it('should display a green color bar with percentage of 100% when number is greater than 100 is provided', () => {
47+
const input = 125;
48+
const color = 'green';
49+
const output = percentCompleteBarFormatter(1, 1, input, {} as Column, {});
50+
expect(output).toBe(`<span class="percent-complete-bar" style="background:${color}; width:100%"></span>`);
51+
});
4552
});

0 commit comments

Comments
 (0)