Skip to content

Commit f9e200e

Browse files
committed
fix(cdk/collections): add ability to bulk select/deselect values
Currently the selection model expects values to be spread into the `select`, `deselect` and `setSelection` methods. This works fine for single values, but can cause an error if the user tries to spread a huge array, because it can hit browser limitations. These changes expose a `.bulk` member on the list that accepts arrays instead of spreads. We need to do it this way, rather than add an array signature to the existing methods, because the selection model might be working with arrays already and we'd have no way of distinguishing them. Fixes #33466.
1 parent 85a81a6 commit f9e200e

3 files changed

Lines changed: 108 additions & 20 deletions

File tree

goldens/cdk/collections/index.api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export interface SelectionChange<T> {
7171
// @public
7272
export class SelectionModel<T> {
7373
constructor(_multiple?: boolean, initiallySelectedValues?: T[], _emitChanges?: boolean, compareWith?: ((o1: T, o2: T) => boolean) | undefined);
74+
readonly bulk: Readonly<{
75+
select: (values: T[]) => boolean;
76+
deselect: (values: T[]) => boolean;
77+
setSelection: (values: T[]) => boolean;
78+
}>;
7479
readonly changed: Subject<SelectionChange<T>>;
7580
clear(flushEvent?: boolean): boolean;
7681
// (undocumented)

src/cdk/collections/selection-model.ts

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ export class SelectionModel<T> {
3636
/** Event emitted when the value has changed. */
3737
readonly changed = new Subject<SelectionChange<T>>();
3838

39+
/**
40+
* Exposes selection/deselection methods that work on array of values and don't expect a spread.
41+
* This is useful in the cases where you may have a large collection of items that can't be
42+
* easily spread into the existing methods without hitting browser limits.
43+
*/
44+
readonly bulk: Readonly<{
45+
select: (values: T[]) => boolean;
46+
deselect: (values: T[]) => boolean;
47+
setSelection: (values: T[]) => boolean;
48+
}> = {
49+
select: values => this._select(values),
50+
deselect: values => this._deselect(values),
51+
setSelection: values => this._setSelection(values),
52+
};
53+
3954
constructor(
4055
private _multiple = false,
4156
initiallySelectedValues?: T[],
@@ -60,11 +75,7 @@ export class SelectionModel<T> {
6075
* @return Whether the selection changed as a result of this call
6176
*/
6277
select(...values: T[]): boolean {
63-
this._verifyValueAssignment(values);
64-
values.forEach(value => this._markSelected(value));
65-
const changed = this._hasQueuedChanges();
66-
this._emitChangeEvent();
67-
return changed;
78+
return this._select(values);
6879
}
6980

7081
/**
@@ -73,11 +84,7 @@ export class SelectionModel<T> {
7384
* @return Whether the selection changed as a result of this call
7485
*/
7586
deselect(...values: T[]): boolean {
76-
this._verifyValueAssignment(values);
77-
values.forEach(value => this._unmarkSelected(value));
78-
const changed = this._hasQueuedChanges();
79-
this._emitChangeEvent();
80-
return changed;
87+
return this._deselect(values);
8188
}
8289

8390
/**
@@ -86,16 +93,7 @@ export class SelectionModel<T> {
8693
* @return Whether the selection changed as a result of this call
8794
*/
8895
setSelection(...values: T[]): boolean {
89-
this._verifyValueAssignment(values);
90-
const oldValues = this.selected;
91-
const newSelectedSet = new Set(values.map(value => this._getConcreteValue(value)));
92-
values.forEach(value => this._markSelected(value));
93-
oldValues
94-
.filter(value => !newSelectedSet.has(this._getConcreteValue(value, newSelectedSet)))
95-
.forEach(value => this._unmarkSelected(value));
96-
const changed = this._hasQueuedChanges();
97-
this._emitChangeEvent();
98-
return changed;
96+
return this._setSelection(values);
9997
}
10098

10199
/**
@@ -159,6 +157,38 @@ export class SelectionModel<T> {
159157
return this._multiple;
160158
}
161159

160+
/** Selects an array of values. */
161+
private _select(values: T[]) {
162+
this._verifyValueAssignment(values);
163+
values.forEach(value => this._markSelected(value));
164+
const changed = this._hasQueuedChanges();
165+
this._emitChangeEvent();
166+
return changed;
167+
}
168+
169+
/** Deselects an array of values. */
170+
private _deselect(values: T[]) {
171+
this._verifyValueAssignment(values);
172+
values.forEach(value => this._unmarkSelected(value));
173+
const changed = this._hasQueuedChanges();
174+
this._emitChangeEvent();
175+
return changed;
176+
}
177+
178+
/** Sets the current selection from an array of items. */
179+
private _setSelection(values: T[]) {
180+
this._verifyValueAssignment(values);
181+
const oldValues = this.selected;
182+
const newSelectedSet = new Set(values.map(value => this._getConcreteValue(value)));
183+
values.forEach(value => this._markSelected(value));
184+
oldValues
185+
.filter(value => !newSelectedSet.has(this._getConcreteValue(value, newSelectedSet)))
186+
.forEach(value => this._unmarkSelected(value));
187+
const changed = this._hasQueuedChanges();
188+
this._emitChangeEvent();
189+
return changed;
190+
}
191+
162192
/** Emits a change event and clears the records of selected and deselected values. */
163193
private _emitChangeEvent() {
164194
// Clear the selected values so they can be re-cached.

src/cdk/collections/selection.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,59 @@ describe('SelectionModel', () => {
224224
});
225225
});
226226

227+
describe('bulk actions', () => {
228+
let model: SelectionModel<number>;
229+
230+
beforeEach(() => (model = new SelectionModel(true)));
231+
232+
it('should be able to bulk select values', () => {
233+
const changedSpy = jasmine.createSpy('changed spy');
234+
235+
model.changed.subscribe(changedSpy);
236+
model.bulk.select([1, 2]);
237+
238+
expect(model.selected.length).toBe(2);
239+
expect(model.isSelected(1)).toBe(true);
240+
expect(model.isSelected(2)).toBe(true);
241+
expect(changedSpy).toHaveBeenCalledTimes(1);
242+
});
243+
244+
it('should be able to bulk deselect values', () => {
245+
model.bulk.select([1, 2]);
246+
expect(model.selected.length).toBe(2);
247+
expect(model.isSelected(1)).toBe(true);
248+
expect(model.isSelected(2)).toBe(true);
249+
250+
const changedSpy = jasmine.createSpy('changed spy');
251+
model.changed.subscribe(changedSpy);
252+
model.bulk.deselect([1, 2]);
253+
254+
expect(model.selected.length).toBe(0);
255+
expect(model.isSelected(1)).toBe(false);
256+
expect(model.isSelected(2)).toBe(false);
257+
expect(changedSpy).toHaveBeenCalledTimes(1);
258+
});
259+
260+
it('should be able to bulk set the selection', () => {
261+
model.bulk.setSelection([1, 2]);
262+
expect(model.selected.length).toBe(2);
263+
expect(model.isSelected(1)).toBe(true);
264+
expect(model.isSelected(2)).toBe(true);
265+
266+
model.bulk.setSelection([2]);
267+
expect(model.selected.length).toBe(1);
268+
expect(model.isSelected(1)).toBe(false);
269+
expect(model.isSelected(2)).toBe(true);
270+
271+
model.bulk.setSelection([3, 4]);
272+
expect(model.selected.length).toBe(2);
273+
expect(model.isSelected(1)).toBe(false);
274+
expect(model.isSelected(2)).toBe(false);
275+
expect(model.isSelected(3)).toBe(true);
276+
expect(model.isSelected(4)).toBe(true);
277+
});
278+
});
279+
227280
it('should be able to determine whether it is empty', () => {
228281
let model = new SelectionModel();
229282

0 commit comments

Comments
 (0)