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

Commit 50b95bd

Browse files
authored
feat: allow providing custom date format via base Date Formatter (#496)
1 parent c0960d4 commit 50b95bd

File tree

5 files changed

+152
-59
lines changed

5 files changed

+152
-59
lines changed

docs/column-functionalities/formatters.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ A good example of a `Formatter` could be a timestamp or a `Date` object that we
5555
* `Formatters.dateTimeShortUs`: Takes a Date object and displays it as an US Date+Time (without seconds) format (MM/DD/YYYY HH:mm:ss)
5656
* `Formatters.dateTimeUsAmPm` : Takes a Date object and displays it as an US Date+Time+(am/pm) format (MM/DD/YYYY hh:mm:ss a)
5757
* `Formatters.dateUtc` : Takes a Date object and displays it as a TZ format (YYYY-MM-DDThh:mm:ssZ)
58+
* `Formatters.date`: Base Date Formatter, this formatter is a bit different compare to other date formatter since this one requires the user to provide a custom format in `params.dateFormat`
59+
- for example: `{ type: 'date', formatter: Formatters.date, params: { dateFormat: 'MMM DD, YYYY' }}`
5860
* `Formatters.decimal`: Display the value as x decimals formatted, defaults to 2 decimals. You can pass "minDecimal" and/or "maxDecimal" to the "params" property.
5961
* `Formatters.dollar`: Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value.
6062
* `Formatters.dollarColored`: Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value, change color of text to red/green on negative/positive value

docs/column-functionalities/sorting.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- [Custom Sort Comparer](#custom-sort-comparer)
55
- [Update Sorting Dynamically](#update-sorting-dynamically)
66
- [Dynamic Query Field](#dynamic-query-field)
7+
- [Sorting Dates](#sorting-dates)
78
- [Pre-Parse Date Columns for better perf](#pre-parse-date-columns-for-better-perf)
89

910
### Demo
@@ -139,6 +140,14 @@ queryFieldNameGetterFn: (dataContext) => {
139140
},
140141
```
141142

143+
### Sorting Dates
144+
145+
Date sorting should work out of the box as long as you provide the correct column field type. Note that there are various field types that can be provided and they all do different things. For the Sorting to work properly, you need to make sure to use the correct type for Date parsing to work accordingly. Below is a list of column definition types that you can provide:
146+
147+
- `type`: input/output of date fields, or in other words, parsing/formatting dates with the field `type` provided
148+
- `outputType`: when a `type` is provided for parsing (i.e. from your dataset), you could use a different `outputType` to format your date differently
149+
- `saveOutputType`: if you already have a `type` and an `outputType` but you wish to save your date (i.e. save to DB) in yet another format
150+
142151
### Pre-Parse Date Columns for better perf
143152
##### requires v5.8.0 and higher
144153

@@ -154,11 +163,11 @@ The summary, is that we get a 10x boost **but** not only that, we also get an ex
154163

155164
#### Usage
156165

157-
You can use the `preParseDateColumns` grid option, it can be either set as either `boolean` or a `string` but there's big distinction between the 2 approaches (both approaches will mutate the dataset).
166+
You can use the `preParseDateColumns` grid option, it can be set as either a `boolean` or a `string` but there's a big distinction between the 2 approaches as shown below (note that both approaches will mutate the dataset).
158167
1. `string` (i.e. set to `"__"`, it will parse a `"start"` date string and assign it as a `Date` object to a new `"__start"` prop)
159168
2. `boolean` (i.e. parse `"start"` date string and reassign it as a `Date` object on the same `"start"` prop)
160169

161-
> **Note** this option **does not work** with Backend Services because it simply has no effect.
170+
> **Note** this option **does not work** with the Backend Service API because it simply has no effect.
162171
163172
For example if our dataset has 2 columns named "start" and "finish", then pre-parse the dataset,
164173

@@ -184,7 +193,7 @@ data = [
184193
]
185194
```
186195

187-
Which approach to choose? Both have pros and cons, overwriting the same props might cause problems with the column `type` that you use, you will have to give it a try yoursel. On the other hand, with the other approach, it will duplicate all date properties and take a bit more memory usage and when changing cells we'll need to make sure to keep these props in sync, however you will likely have less `type` issues.
196+
Which approach to choose? Both have pros and cons, overwriting the same props might cause problems with the column `type` that you use, you will have to give it a try yourself. On the other hand, with the other approach, it will duplicate all date properties and take a bit more memory usage and when changing cells we'll need to make sure to keep these props in sync, however you will likely have less `type` issues.
188197

189198
What happens when we do any cell changes (for our use case, it would be Create/Update), for any Editors we simply subscribe to the `onCellChange` change event and we re-parse the date strings when detected. We also subscribe to certain CRUD functions as long as they come from the `GridService` then all is fine... However, if you use the DataView functions directly then we have no way of knowing when to parse because these functions from the DataView don't have any events. Lastly, if we overwrite the entire dataset, we will also detect this (via an internal flag) and the next time you sort a date then the pre-parse kicks in again.
190199

src/examples/slickgrid/Example40.tsx

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { format as dateFormatter } from '@formkit/tempo';
2+
import { ExcelExportService } from '@slickgrid-universal/excel-export';
23
import React, { useEffect, useRef, useState } from 'react';
34
import {
45
Aggregators,
56
type Column,
67
FieldType,
8+
Filters,
79
Formatters,
810
type GridOption,
911
type Grouping,
@@ -16,6 +18,7 @@ import {
1618
} from '../../slickgrid-react';
1719

1820
import './example39.scss';
21+
import { randomNumber } from './utilities';
1922

2023
const FETCH_SIZE = 50;
2124

@@ -43,11 +46,63 @@ const Example40: React.FC = () => {
4346
function defineGrid() {
4447
const columnDefinitions: Column[] = [
4548
{ id: 'title', name: 'Title', field: 'title', sortable: true, minWidth: 100, filterable: true },
46-
{ id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
47-
{ id: 'percentComplete', name: '% Complete', field: 'percentComplete', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
48-
{ id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, exportWithFormatter: true, filterable: true },
49-
{ id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, exportWithFormatter: true, filterable: true },
50-
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', sortable: true, minWidth: 100, filterable: true, formatter: Formatters.checkmarkMaterial }
49+
{
50+
id: 'duration',
51+
name: 'Duration (days)',
52+
field: 'duration',
53+
sortable: true,
54+
minWidth: 100,
55+
filterable: true,
56+
type: FieldType.number,
57+
},
58+
{
59+
id: 'percentComplete',
60+
name: '% Complete',
61+
field: 'percentComplete',
62+
sortable: true,
63+
minWidth: 100,
64+
filterable: true,
65+
type: FieldType.number,
66+
},
67+
{
68+
id: 'start',
69+
name: 'Start',
70+
field: 'start',
71+
type: FieldType.date,
72+
outputType: FieldType.dateIso, // for date picker format
73+
formatter: Formatters.date,
74+
exportWithFormatter: true,
75+
params: { dateFormat: 'MMM DD, YYYY' },
76+
sortable: true,
77+
filterable: true,
78+
filter: {
79+
model: Filters.compoundDate,
80+
},
81+
},
82+
{
83+
id: 'finish',
84+
name: 'Finish',
85+
field: 'finish',
86+
type: FieldType.date,
87+
outputType: FieldType.dateIso, // for date picker format
88+
formatter: Formatters.date,
89+
exportWithFormatter: true,
90+
params: { dateFormat: 'MMM DD, YYYY' },
91+
sortable: true,
92+
filterable: true,
93+
filter: {
94+
model: Filters.compoundDate,
95+
},
96+
},
97+
{
98+
id: 'effort-driven',
99+
name: 'Effort Driven',
100+
field: 'effortDriven',
101+
sortable: true,
102+
minWidth: 100,
103+
filterable: true,
104+
formatter: Formatters.checkmarkMaterial,
105+
},
51106
];
52107
const gridOptions: GridOption = {
53108
autoResize: {
@@ -59,6 +114,8 @@ const Example40: React.FC = () => {
59114
enableGrouping: true,
60115
editable: false,
61116
rowHeight: 33,
117+
enableExcelExport: true,
118+
externalResources: [new ExcelExportService()],
62119
};
63120

64121
setColumnDefinitions(columnDefinitions);
@@ -126,38 +183,35 @@ const Example40: React.FC = () => {
126183
}
127184

128185
function newItem(idx: number) {
129-
const randomYear = 2000 + Math.floor(Math.random() * 10);
130-
const randomMonth = Math.floor(Math.random() * 11);
131-
const randomDay = Math.floor((Math.random() * 29));
132-
const randomPercent = Math.round(Math.random() * 100);
133-
134186
return {
135187
id: idx,
136188
title: 'Task ' + idx,
137189
duration: Math.round(Math.random() * 100) + '',
138-
percentComplete: randomPercent,
139-
start: new Date(randomYear, randomMonth + 1, randomDay),
140-
finish: new Date(randomYear + 1, randomMonth + 1, randomDay),
141-
effortDriven: (idx % 5 === 0)
190+
percentComplete: randomNumber(1, 12),
191+
start: new Date(2020, randomNumber(1, 11), randomNumber(1, 28)),
192+
finish: new Date(2022, randomNumber(1, 11), randomNumber(1, 28)),
193+
effortDriven: idx % 5 === 0,
142194
};
143195
}
144196

145197
function onSortReset(shouldReset: boolean) {
146198
shouldResetOnSortRef.current = shouldReset;
147199
}
148200

149-
function refreshMetrics(args: OnRowCountChangedEventArgs) {
201+
function handleOnRowCountChanged(args: OnRowCountChangedEventArgs) {
150202
if (reactGridRef.current && args?.current >= 0) {
203+
// we probably want to re-sort the data when we get new items
204+
reactGridRef.current?.dataView?.reSort();
205+
206+
// update metrics
151207
const itemCount = reactGridRef.current?.dataView?.getFilteredItemCount() || 0;
152208
setMetrics({ ...metrics, itemCount, totalItemCount: args.itemCount || 0 });
153209
}
154210
}
155211

156212
function setFiltersDynamically() {
157213
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
158-
reactGridRef.current?.filterService.updateFilters([
159-
{ columnId: 'percentComplete', searchTerms: ['50'], operator: '>=' },
160-
]);
214+
reactGridRef.current?.filterService.updateFilters([{ columnId: 'start', searchTerms: ['2020-08-25'], operator: '<=' }]);
161215
}
162216

163217
function setSortingDynamically() {
@@ -242,7 +296,7 @@ const Example40: React.FC = () => {
242296
gridOptions={gridOptionsRef.current}
243297
dataset={dataset}
244298
onReactGridCreated={$event => reactGridReady($event.detail)}
245-
onRowCountChanged={$event => refreshMetrics($event.detail.args)}
299+
onRowCountChanged={$event => handleOnRowCountChanged($event.detail.args)}
246300
onSort={_ => handleOnSort()}
247301
onScroll={$event => handleOnScroll($event.detail.args)}
248302
/>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function randomNumber(min: number, max: number, floor = true) {
2+
const number = Math.random() * (max - min + 1) + min;
3+
return floor ? Math.floor(number) : number;
4+
}
5+
6+
export function zeroPadding(input: string | number) {
7+
const number = parseInt(input as string, 10);
8+
return number < 10 ? `0${number}` : number;
9+
}

test/cypress/e2e/example40.cy.ts

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,37 @@ describe('Example 40 - Infinite Scroll from JSON data', () => {
1818
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
1919
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains(/[0-9]/);
2020
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains(/[0-9]/);
21-
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/);
22-
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/);
23-
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).find('.mdi.mdi-check').should('have.length', 1);
21+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains('2020');
22+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains('2022');
23+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`)
24+
.find('.mdi.mdi-check')
25+
.should('have.length', 1);
2426
});
2527

2628
it('should scroll to bottom of the grid and expect next batch of 50 items appended to current dataset for a total of 100 items', () => {
27-
cy.get('[data-test="totalItemCount"]')
28-
.should('have.text', '50');
29+
cy.get('[data-test="totalItemCount"]').should('have.text', '50');
2930

30-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
31-
.scrollTo('bottom');
31+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom');
3232

33-
cy.get('[data-test="totalItemCount"]')
34-
.should('have.text', '100');
33+
cy.get('[data-test="totalItemCount"]').should('have.text', '100');
3534
});
3635

3736
it('should scroll to bottom of the grid again and expect 50 more items for a total of now 150 items', () => {
38-
cy.get('[data-test="totalItemCount"]')
39-
.should('have.text', '100');
37+
cy.get('[data-test="totalItemCount"]').should('have.text', '100');
4038

41-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
42-
.scrollTo('bottom');
39+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom');
4340

44-
cy.get('[data-test="totalItemCount"]')
45-
.should('have.text', '150');
41+
cy.get('[data-test="totalItemCount"]').should('have.text', '150');
4642
});
4743

4844
it('should disable onSort for data reset and expect same dataset length of 150 items after sorting by Title', () => {
4945
cy.get('[data-test="onsort-off"]').click();
5046

51-
cy.get('[data-id="title"]')
52-
.click();
47+
cy.get('[data-id="title"]').click();
5348

54-
cy.get('[data-test="totalItemCount"]')
55-
.should('have.text', '150');
49+
cy.get('[data-test="totalItemCount"]').should('have.text', '150');
5650

57-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
58-
.scrollTo('top');
51+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top');
5952

6053
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
6154
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1');
@@ -66,14 +59,11 @@ describe('Example 40 - Infinite Scroll from JSON data', () => {
6659
it('should enable onSort for data reset and expect dataset to be reset to 50 items after sorting by Title', () => {
6760
cy.get('[data-test="onsort-on"]').click();
6861

69-
cy.get('[data-id="title"]')
70-
.click();
62+
cy.get('[data-id="title"]').click();
7163

72-
cy.get('[data-test="totalItemCount"]')
73-
.should('have.text', '50');
64+
cy.get('[data-test="totalItemCount"]').should('have.text', '50');
7465

75-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
76-
.scrollTo('top');
66+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top');
7767

7868
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 9');
7969
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 8');
@@ -84,27 +74,56 @@ describe('Example 40 - Infinite Scroll from JSON data', () => {
8474
it('should "Group by Duration" and expect 50 items grouped', () => {
8575
cy.get('[data-test="group-by-duration"]').click();
8676

87-
cy.get('[data-test="totalItemCount"]')
88-
.should('have.text', '50');
77+
cy.get('[data-test="totalItemCount"]').should('have.text', '50');
8978

90-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
91-
.scrollTo('top');
79+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top');
9280

9381
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
9482
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/);
9583
});
9684

9785
it('should scroll to the bottom "Group by Duration" and expect 50 more items for a total of 100 items grouped', () => {
98-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
99-
.scrollTo('bottom');
86+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom');
10087

101-
cy.get('[data-test="totalItemCount"]')
102-
.should('have.text', '100');
88+
cy.get('[data-test="totalItemCount"]').should('have.text', '100');
10389

104-
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
105-
.scrollTo('top');
90+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top');
10691

10792
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
10893
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/);
10994
});
95+
96+
it('should clear all grouping', () => {
97+
cy.get('#grid40').find('.slick-row .slick-cell:nth(1)').rightclick({ force: true });
98+
99+
cy.get('.slick-context-menu.dropright .slick-menu-command-list')
100+
.find('.slick-menu-item')
101+
.find('.slick-menu-content')
102+
.contains('Clear all Grouping')
103+
.click();
104+
});
105+
106+
it('should hover over the "Start" column header menu of 1st grid and click on "Sort Descending" command', () => {
107+
cy.get('[data-test="clear-filters-sorting"]').click();
108+
cy.get('#grid40').find('.slick-header-column:nth(3)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
109+
110+
cy.get('.slick-header-menu .slick-menu-command-list').should('be.visible').should('contain', 'Sort Descending').click();
111+
112+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains('2020');
113+
});
114+
115+
it('should load 200 items and filter "Start" column with <=2020-08-25', () => {
116+
cy.get('[data-test="set-dynamic-filter"]').click();
117+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom');
118+
cy.wait(10);
119+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom');
120+
cy.get('[data-test="totalItemCount"]').should('have.text', '200');
121+
122+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top');
123+
124+
cy.get(`[data-row=0] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/);
125+
cy.get(`[data-row=1] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/);
126+
cy.get(`[data-row=2] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/);
127+
cy.get(`[data-row=3] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/);
128+
});
110129
});

0 commit comments

Comments
 (0)