Skip to content

Commit 1acd488

Browse files
committed
Add tracking example
1 parent d88f3dc commit 1acd488

File tree

7 files changed

+523
-9
lines changed

7 files changed

+523
-9
lines changed

website/docs/api-reference/props.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ When `type` is `DELETE`, the row indices refer to the old value, not the one pas
6868
- You can compute the number of impacted rows: `toRowIndex - fromRowIndex`
6969
- You can get the impacted rows: `value.slice(fromRowIndex, toRowIndex)`
7070
71+
You can read [this recipe](../recipes/tracking-rows-changes) to have an example.
72+
7173
## Columns
7274
### columns
7375
> Type: `Column[]`
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
---
2+
sidebar_position: 5
3+
---
4+
import {FinalResult} from '../../src/demos/trackRows.tsx'
5+
6+
# Tracking rows changes
7+
8+
For an app that offers a way to interact with a database, you might want to let the user do changes locally,
9+
then commit to the server or cancel their changes. This is easily achievable using the second argument of
10+
[`onChange`](../api-reference/props#onchange).
11+
12+
## Setup
13+
14+
First we want to setup our DataSheetGrid:
15+
```tsx
16+
const [data, setData] = useState([])
17+
18+
return (
19+
<DataSheetGrid
20+
columns={[/*...*/]}
21+
value={data}
22+
onChange={(newValue) => setData(newValue)}
23+
/>
24+
)
25+
```
26+
Then we need a way to track rows to know which ones were added, deleted, or updated.
27+
We can simply add [unique ids](unique-ids):
28+
```tsx
29+
<DataSheetGrid
30+
...
31+
createRow={() => ({ id: genId() })}
32+
duplicateRow={({ rowData }) => ({ ...rowData, id: genId() })}
33+
/>
34+
```
35+
36+
Then we want to use the second argument of [`onChange`](../api-reference/props#onchange) to track changes:
37+
```tsx
38+
<DataSheetGrid
39+
...
40+
onChange={(newValue, operations) => {
41+
for (const operation of operations) {
42+
// Handle operation
43+
}
44+
45+
setData(newValue)
46+
}}
47+
/>
48+
```
49+
50+
## Tracking rows
51+
We can track ids of created, deleted, and updated rows using a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)
52+
and we use a `useMemo` to save its value between renders.
53+
54+
```tsx
55+
const createdRowIds = useMemo(() => new Set(), [])
56+
const deletedRowIds = useMemo(() => new Set(), [])
57+
const updatedRowIds = useMemo(() => new Set(), [])
58+
59+
return (
60+
<DataSheetGrid
61+
...
62+
/>
63+
)
64+
```
65+
66+
### Handle new rows
67+
```tsx
68+
<DataSheetGrid
69+
onChange={(newValue, operations) => {
70+
for (const operation of operations) {
71+
if (operation.type === 'CREATE') {
72+
newValue
73+
.slice(operation.fromRowIndex, operation.toRowIndex)
74+
.forEach(({ id }) => createdRowIds.add(id))
75+
}
76+
77+
// Handle update
78+
// Handle delete
79+
}
80+
81+
setData(newValue)
82+
}}
83+
/>
84+
```
85+
86+
`.slice()` gives us the list of created rows that we then `.add()` to the set of created row ids one by one.
87+
88+
### Handle updated rows
89+
90+
This time we only want to track an updated row if it was not freshly created (to prevent an insert immediately
91+
followed by an update) or deleted (to prevent updating a row and immediately deleting it).
92+
93+
```tsx
94+
<DataSheetGrid
95+
onChange={(newValue, operations) => {
96+
for (const operation of operations) {
97+
// Handle new rows
98+
99+
if (operation.type === 'UPDATE') {
100+
newValue
101+
.slice(operation.fromRowIndex, operation.toRowIndex)
102+
.forEach(({ id }) => {
103+
if (!createdRowIds.has(id) && !deletedRowIds.has(id)) {
104+
updatedRowIds.add(id)
105+
}
106+
})
107+
}
108+
109+
// Handle delete
110+
}
111+
112+
setData(newValue)
113+
}}
114+
/>
115+
```
116+
117+
### Handle deleted rows
118+
119+
This time we the indices of the operation refer to the current `data`, not the `newValue`, because deleted rows
120+
are not present in `newValue`.
121+
122+
```tsx
123+
<DataSheetGrid
124+
onChange={(newValue, operations) => {
125+
for (const operation of operations) {
126+
// Handle new rows
127+
// Handle update
128+
129+
if (operation.type === 'DELETE') {
130+
let keptRows = 0
131+
132+
// Make sure to use data and not newValue
133+
data
134+
.slice(operation.fromRowIndex, operation.toRowIndex)
135+
.forEach(({ id }, i) => {
136+
// If the row was updated, ignore the update
137+
// No need to update a row and immediately delete it
138+
updatedRowIds.delete(id)
139+
140+
if (createdRowIds.has(id)) {
141+
// Row was freshly created, simply ignore it
142+
// No need to insert a row and immediately delete it
143+
createdRowIds.delete(id)
144+
} else {
145+
// Track the row as deleted
146+
deletedRowIds.add(id)
147+
// Insert it back into newValue to display it in red to the user
148+
newValue.splice(
149+
operation.fromRowIndex + keptRows++,
150+
0,
151+
data[operation.fromRowIndex + i]
152+
)
153+
}
154+
})
155+
}
156+
}
157+
158+
setData(newValue)
159+
}}
160+
/>
161+
```
162+
163+
## Styling
164+
165+
We can now add a class to the rows based on the tracking:
166+
167+
```tsx
168+
<DataSheetGrid
169+
rowClassName={({ rowData }) => {
170+
if (deletedRowIds.has(rowData.id)) {
171+
return 'row-deleted'
172+
}
173+
if (createdRowIds.has(rowData.id)) {
174+
return 'row-created'
175+
}
176+
if (updatedRowIds.has(rowData.id)) {
177+
return 'row-updated'
178+
}
179+
}}
180+
/>
181+
```
182+
183+
The add simple CSS to update the cell's color based on the row's calss:
184+
```css
185+
.row-deleted .dsg-cell {
186+
/* Red */
187+
background: #fff1f0;
188+
}
189+
190+
.row-created .dsg-cell {
191+
/* Green */
192+
background: #f6ffed;
193+
}
194+
195+
.row-updated .dsg-cell {
196+
/* Orange */
197+
background: #fff7e6;
198+
}
199+
```
200+
201+
## Cancel button
202+
203+
To cancel changes, simply rollback `data` and clear all tracking.
204+
```tsx
205+
const [data, setData] = useState([])
206+
const [prevData, setPrevData] = useState(data)
207+
208+
const cancel = () => {
209+
setData(prevData)
210+
createdRowIds.clear()
211+
deletedRowIds.clear()
212+
updatedRowIds.clear()
213+
}
214+
215+
return (
216+
<>
217+
<button onClick={cancel}>
218+
Cancel
219+
</button>
220+
221+
<DataSheetGrid
222+
...
223+
/>
224+
</>
225+
)
226+
```
227+
228+
## Commit button
229+
230+
Tracking can be used to perform all database operations when the user commits its changes.
231+
Then the `data` can be updated to finally visually delete rows that were still displayed in red.
232+
All tracking can then be cleared.
233+
```tsx
234+
const commit = () => {
235+
/* Use tracking to perform insert, update, and delete to the database */
236+
237+
const newData = data.filter(({ id }) => !deletedRowIds.has(id))
238+
setData(newData)
239+
setPrevData(newData)
240+
241+
createdRowIds.clear()
242+
deletedRowIds.clear()
243+
updatedRowIds.clear()
244+
}
245+
246+
return (
247+
<>
248+
<button onClick={commit}>
249+
Commit
250+
</button>
251+
252+
<DataSheetGrid
253+
...
254+
/>
255+
</>
256+
)
257+
```
258+
259+
## Final result
260+
261+
```tsx
262+
const [data, setData] = useState([])
263+
const [prevData, setPrevData] = useState(data)
264+
265+
const createdRowIds = useMemo(() => new Set(), [])
266+
const deletedRowIds = useMemo(() => new Set(), [])
267+
const updatedRowIds = useMemo(() => new Set(), [])
268+
269+
const cancel = () => {
270+
setData(prevData)
271+
createdRowIds.clear()
272+
deletedRowIds.clear()
273+
updatedRowIds.clear()
274+
}
275+
276+
const commit = () => {
277+
/* Use tracking to perform insert, update, and delete to the database */
278+
279+
const newData = data.filter(({ id }) => !deletedRowIds.has(id))
280+
setData(newData)
281+
setPrevData(newData)
282+
283+
createdRowIds.clear()
284+
deletedRowIds.clear()
285+
updatedRowIds.clear()
286+
}
287+
288+
return (
289+
<>
290+
<button onClick={commit}>
291+
Commit
292+
</button>
293+
294+
<button onClick={cancel}>
295+
Cancel
296+
</button>
297+
298+
<DataSheetGrid
299+
columns={[/*...*/]}
300+
value={data}
301+
createRow={() => ({ id: genId() })}
302+
duplicateRow={({ rowData }) => ({ ...rowData, id: genId() })}
303+
rowClassName={({ rowData }) => {
304+
if (deletedRowIds.has(rowData.id)) {
305+
return 'row-deleted'
306+
}
307+
if (createdRowIds.has(rowData.id)) {
308+
return 'row-created'
309+
}
310+
if (updatedRowIds.has(rowData.id)) {
311+
return 'row-updated'
312+
}
313+
}}
314+
onChange={(newValue, operations) => {
315+
for (const operation of operations) {
316+
if (operation.type === 'CREATE') {
317+
newValue
318+
.slice(operation.fromRowIndex, operation.toRowIndex)
319+
.forEach(({ id }) => createdRowIds.add(id))
320+
}
321+
322+
if (operation.type === 'UPDATE') {
323+
newValue
324+
.slice(operation.fromRowIndex, operation.toRowIndex)
325+
.forEach(({ id }) => {
326+
if (!createdRowIds.has(id) && !deletedRowIds.has(id)) {
327+
updatedRowIds.add(id)
328+
}
329+
})
330+
}
331+
332+
if (operation.type === 'DELETE') {
333+
let keptRows = 0
334+
335+
data
336+
.slice(operation.fromRowIndex, operation.toRowIndex)
337+
.forEach(({ id }, i) => {
338+
updatedRowIds.delete(id)
339+
340+
if (createdRowIds.has(id)) {
341+
createdRowIds.delete(id)
342+
} else {
343+
deletedRowIds.add(id)
344+
newValue.splice(
345+
operation.fromRowIndex + keptRows++,
346+
0,
347+
data[operation.fromRowIndex + i]
348+
)
349+
}
350+
})
351+
}
352+
}
353+
354+
setData(newValue)
355+
}}
356+
/>
357+
</>
358+
)
359+
```
360+
361+
<FinalResult />

0 commit comments

Comments
 (0)