Skip to content

Commit 4a92201

Browse files
kturneypzuraq
authored andcommitted
failing test for selection after rows update (#596)
This is a failing test for an issue we ran into around selecting items and then the table row content changing. For our specific case, changing the sort triggers a network request, which replaces the rows in the table. If a user had selected some items and then changed the sort, they could no longer select more items. However, the sort changing does not appear to be core to the issue. Just changing the rows is enough to reveal the bug. I plan to dig in and see if I can find a fix, but any guidance would be appreciated 😄.
1 parent 7668d46 commit 4a92201

File tree

6 files changed

+71
-38
lines changed

6 files changed

+71
-38
lines changed

addon/-private/collapse-tree.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@ export const SELECT_MODE = {
1616
MULTIPLE: 'multiple',
1717
};
1818

19-
class TableRowMeta extends EmberObject {
19+
export class TableRowMeta extends EmberObject {
2020
_rowValue = null;
21+
22+
/**
23+
The map that contains cell meta information for this row. Is meant to be
24+
unique to this row, which is why it is created here. In order to prevent
25+
memory leaks, we need to be able to clean the cache manually when the row
26+
is destroyed or updated, which is why we use a Map instead of WeakMap
27+
*/
28+
_cellMetaCache = new Map();
2129
_isCollapsed = false;
2230

2331
@computed('_rowValue.isCollapsed')
@@ -222,6 +230,12 @@ class TableRowMeta extends EmberObject {
222230

223231
tree._lastSelectedIndex = rowIndex;
224232
}
233+
234+
destroy() {
235+
super.destroy();
236+
237+
this._cellMetaCache.clear();
238+
}
225239
}
226240

227241
function reduceSelectedRows(selection, groupingCounts, rowMetaCache) {

addon/components/-private/row-wrapper.js

+8-22
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,20 @@ import { computed } from '@ember-decorators/object';
1212
import { objectAt } from '../../-private/utils/array';
1313
import { dynamicAlias } from '../../-private/utils/computed';
1414

15-
import { guidFor } from '@ember/object/internals';
16-
1715
class CellWrapper extends EmberObject {
1816
@dynamicAlias('rowValue', 'columnValue.valuePath')
1917
cellValue;
2018

21-
@computed('rowValue', 'columnValue')
19+
@computed('rowMeta', 'columnValue')
2220
get cellMeta() {
23-
let row = get(this, 'rowValue');
24-
let rowId = guidFor(row);
25-
let columnId = guidFor(get(this, 'columnValue'));
26-
let cellMetaCache = get(this, 'cellMetaCache');
27-
28-
let cellId = `${rowId}:${columnId}`;
21+
let rowMeta = get(this, 'rowMeta');
22+
let columnValue = get(this, 'columnValue');
2923

30-
if (!cellMetaCache.has(cellId)) {
31-
cellMetaCache.set(cellId, EmberObject.create());
24+
if (!rowMeta._cellMetaCache.has(columnValue)) {
25+
rowMeta._cellMetaCache.set(columnValue, EmberObject.create());
3226
}
3327

34-
return cellMetaCache.get(cellId);
28+
return rowMeta._cellMetaCache.get(columnValue);
3529
}
3630
}
3731

@@ -44,7 +38,6 @@ export default class RowWrapper extends Component {
4438
@argument rowValue;
4539
@argument columns;
4640

47-
@argument cellMetaCache;
4841
@argument columnMetaCache;
4942
@argument rowMetaCache;
5043

@@ -88,8 +81,6 @@ export default class RowWrapper extends Component {
8881
'rowSelectionMode'
8982
)
9083
get cells() {
91-
let cellMetaCache = this.get('cellMetaCache');
92-
9384
let columns = this.get('columns');
9485
let numColumns = get(columns, 'length');
9586

@@ -103,16 +94,11 @@ export default class RowWrapper extends Component {
10394

10495
if (numColumns !== _cells.length) {
10596
while (_cells.length < numColumns) {
106-
let cell = CellWrapper.create({
107-
cellMetaCache,
108-
});
109-
110-
_cells.pushObject(cell);
97+
_cells.pushObject(CellWrapper.create());
11198
}
11299

113100
while (_cells.length > numColumns) {
114-
let cell = _cells.popObject();
115-
cell.destroy();
101+
_cells.popObject().destroy();
116102
}
117103
}
118104

addon/components/ember-tbody/component.js

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Component from '@ember/component';
2+
import { isArray } from '@ember/array';
23

34
import { tagName } from '@ember-decorators/component';
45
import { computed } from '@ember-decorators/object';
@@ -192,14 +193,6 @@ export default class EmberTBody extends Component {
192193
@type('string')
193194
key = '@identity';
194195

195-
/**
196-
The map that contains cell meta information for this table. Is meant to be
197-
unique to this table, which is why it is created here. In order to prevent
198-
memory leaks, we need to be able to clean the cache manually when the table
199-
is destroyed or updated, which is why we use a Map instead of WeakMap
200-
*/
201-
cellMetaCache = new Map();
202-
203196
/**
204197
The map that contains row meta information for this table.
205198
*/
@@ -212,7 +205,6 @@ export default class EmberTBody extends Component {
212205
*/
213206
collapseTree = CollapseTree.create({
214207
sendAction: this.sendAction.bind(this),
215-
rowMetaCache: this.rowMetaCache,
216208
});
217209

218210
/**
@@ -269,10 +261,11 @@ export default class EmberTBody extends Component {
269261
*/
270262
@computed('rows')
271263
get wrappedRows() {
272-
this._cleanCaches();
273-
274264
let rows = this.get('rows');
275265

266+
this._cleanCaches(rows);
267+
268+
this.collapseTree.set('rowMetaCache', this.rowMetaCache);
276269
this.collapseTree.set('rows', rows);
277270

278271
return this.collapseTree;
@@ -282,12 +275,24 @@ export default class EmberTBody extends Component {
282275
Helper method that is used to clear the row and cell caches whenever the
283276
underlying data has changed.
284277
*/
285-
_cleanCaches() {
286-
this.cellMetaCache.clear();
278+
_cleanCaches(newRows) {
279+
let newRowMetaCache = new Map();
280+
let meta;
281+
282+
if (isArray(newRows)) {
283+
newRows.forEach(row => {
284+
if ((meta = this.rowMetaCache.get(row))) {
285+
newRowMetaCache.set(row, meta);
286+
this.rowMetaCache.delete(row);
287+
}
288+
});
289+
}
287290

288291
for (let [row, meta] of this.rowMetaCache.entries()) {
289292
meta.destroy();
290293
this.rowMetaCache.delete(row);
291294
}
295+
296+
this.set('rowMetaCache', newRowMetaCache);
292297
}
293298
}

addon/components/ember-tbody/template.hbs

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
rowValue=rowValue
1919
columns=columns
2020

21-
cellMetaCache=cellMetaCache
2221
columnMetaCache=columnMetaCache
2322
rowMetaCache=rowMetaCache
2423

addon/components/ember-tfoot/template.hbs

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
rowValue=rowValue
44
columns=columns
55

6-
cellMetaCache=cellMetaCache
76
columnMetaCache=columnMetaCache
87
rowMetaCache=rowMetaCache
98

tests/integration/components/selection-test.js

+30
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,36 @@ module('Integration | selection', () => {
169169

170170
assert.ok(table.validateSelected(0, 1, 2, 3), 'row and its children are selected');
171171
});
172+
173+
test('selection continues to work after rows are updated', async function(assert) {
174+
let columns = [
175+
{
176+
name: 'Name',
177+
valuePath: 'name',
178+
},
179+
{
180+
name: 'Age',
181+
valuePath: 'age',
182+
},
183+
];
184+
185+
let rows = [{ name: 'Zoe', age: 34 }, { name: 'Alex', age: 43 }, { name: 'Liz', age: 25 }];
186+
187+
await generateTable(this, { columns, rows });
188+
189+
assert.ok(table.validateSelected(), 'the row is not marked as selected on initialization');
190+
191+
await table.selectRow(0);
192+
193+
assert.ok(table.validateSelected(0), 'Zoe is selected after being clicked');
194+
195+
this.set('rows', rows.slice());
196+
197+
assert.ok(table.validateSelected(0), 'Zoe is still selected rows update');
198+
199+
await table.selectRow(1);
200+
assert.ok(table.validateSelected(1), 'Liz is selected after being clicked');
201+
});
172202
});
173203

174204
componentModule('single', function() {

0 commit comments

Comments
 (0)