From e9fedb9bad4357bd3dfb509eabcba875ddafc5de Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 7 Jan 2017 09:26:16 +0100 Subject: [PATCH 1/5] feat: add a common class to be used when dealing with selection logic Adds the `MdSelectionModel` class that can be used when dealing with single and multiple selection within a component. Relates to #2412. --- src/lib/core/core.ts | 3 + src/lib/core/selection/selection.spec.ts | 256 +++++++++++++++++++++++ src/lib/core/selection/selection.ts | 168 +++++++++++++++ 3 files changed, 427 insertions(+) create mode 100644 src/lib/core/selection/selection.spec.ts create mode 100644 src/lib/core/selection/selection.ts diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 8943114da921..4e13aab3957b 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -69,6 +69,9 @@ export { LIVE_ANNOUNCER_ELEMENT_TOKEN, } from './a11y/live-announcer'; +// Selection +export * from './selection/selection'; + /** @deprecated */ export {LiveAnnouncer as MdLiveAnnouncer} from './a11y/live-announcer'; diff --git a/src/lib/core/selection/selection.spec.ts b/src/lib/core/selection/selection.spec.ts new file mode 100644 index 000000000000..ea21f28683d1 --- /dev/null +++ b/src/lib/core/selection/selection.spec.ts @@ -0,0 +1,256 @@ +import {MdSelectionModel} from './selection'; + + +describe('MdSelectionModel', () => { + describe('single selection', () => { + let model: MdSelectionModel; + + beforeEach(() => model = new MdSelectionModel([1, 2, 3])); + + it('should be able to select a single value', () => { + model.select(1); + + expect(model.selected.length).toBe(1); + expect(model.isSelected(1)).toBe(true); + }); + + it('should deselect the previously selected value', () => { + model.select(1); + model.select(2); + + expect(model.isSelected(1)).toBe(false); + expect(model.isSelected(2)).toBe(true); + }); + + it('should throw an error when trying to select all of the values', () => { + expect(() => model.selectAll()).toThrow(); + }); + + it('should only preselect one value', () => { + model = new MdSelectionModel([1, 2, 3], false, [1, 2]); + + expect(model.selected.length).toBe(1); + expect(model.isSelected(1)).toBe(true); + expect(model.isSelected(2)).toBe(false); + }); + }); + + describe('multiple selection', () => { + let model: MdSelectionModel; + + beforeEach(() => model = new MdSelectionModel([1, 2, 3], true)); + + it('should be able to select multiple options at the same time', () => { + model.select(1); + model.select(2); + + expect(model.selected.length).toBe(2); + expect(model.isSelected(1)).toBe(true); + expect(model.isSelected(2)).toBe(true); + }); + + it('should be able to preselect multiple options', () => { + model = new MdSelectionModel([1, 2, 3], true, [1, 2]); + + expect(model.selected.length).toBe(2); + expect(model.isSelected(1)).toBe(true); + expect(model.isSelected(2)).toBe(true); + }); + + it('should be able to select all of the options', () => { + model.selectAll(); + expect(model.options.every(value => model.isSelected(value))).toBe(true); + }); + }); + + describe('updating the options', () => { + let model: MdSelectionModel; + + beforeEach(() => model = new MdSelectionModel([1, 2, 3], true)); + + it('should be able to update the list of options', () => { + let newOptions = [1, 2, 3, 4, 5]; + + model.options = newOptions; + + expect(model.options).not.toBe(newOptions, 'Expected the array to have been cloned.'); + expect(model.options).toEqual(newOptions); + }); + + it('should keep the selected value', () => { + model.select(2); + + model.options = [1, 2, 3, 4, 5]; + + expect(model.isSelected(2)).toBe(true); + }); + + it('should deselect values that are not longer in the list', () => { + model.select(1); + + model.options = [2, 3, 4]; + + expect(model.isSelected(1)).toBe(false); + }); + }); + + describe('onChange event', () => { + it('should return both the added and removed values', () => { + let model = new MdSelectionModel([1, 2, 3]); + let spy = jasmine.createSpy('MdSelectionModel change event'); + + model.select(1); + + model.onChange.subscribe(spy); + + model.select(2); + + let event = spy.calls.mostRecent().args[0]; + + expect(spy).toHaveBeenCalled(); + expect(event.removed).toEqual([1]); + expect(event.added).toEqual([2]); + }); + + describe('selection', () => { + let model: MdSelectionModel; + let spy: jasmine.Spy; + + beforeEach(() => { + model = new MdSelectionModel([1, 2, 3], true); + spy = jasmine.createSpy('MdSelectionModel change event'); + + model.onChange.subscribe(spy); + }); + + it('should emit an event when a value is selected', () => { + model.select(1); + + let event = spy.calls.mostRecent().args[0]; + + expect(spy).toHaveBeenCalled(); + expect(event.added).toEqual([1]); + expect(event.removed).toEqual([]); + }); + + it('should not emit multiple events for the same value', () => { + model.select(1); + model.select(1); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should emit a single event when selecting all of the values', () => { + model.selectAll(); + + let event = spy.calls.mostRecent().args[0]; + + expect(spy).toHaveBeenCalledTimes(1); + expect(event.added).toEqual([1, 2, 3]); + }); + + it('should not emit an event when preselecting values', () => { + model = new MdSelectionModel([1, 2, 3], false, [1]); + spy = jasmine.createSpy('MdSelectionModel initial change event'); + model.onChange.subscribe(spy); + + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('deselection', () => { + let model: MdSelectionModel; + let spy: jasmine.Spy; + + beforeEach(() => { + model = new MdSelectionModel([1, 2, 3], true, [1, 2]); + spy = jasmine.createSpy('MdSelectionModel change event'); + + model.onChange.subscribe(spy); + }); + + it('should emit an event when a value is deselected', () => { + model.deselect(1); + + let event = spy.calls.mostRecent().args[0]; + + expect(spy).toHaveBeenCalled(); + expect(event.removed).toEqual([1]); + }); + + it('should not emit an event when a non-selected value is deselected', () => { + model.deselect(3); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should emit a single event when clearing all of the selected options', () => { + model.clear(); + + let event = spy.calls.mostRecent().args[0]; + + expect(spy).toHaveBeenCalledTimes(1); + expect(event.removed).toEqual([2, 1]); + }); + + it('should emit an event when a value is deselected due to it being removed from the options', + () => { + model.options = [4, 5, 6]; + + let event = spy.calls.mostRecent().args[0]; + + expect(spy).toHaveBeenCalledTimes(1); + expect(event.removed).toEqual([2, 1]); + }); + }); + }); + + it('should be able to determine whether it is empty', () => { + let model = new MdSelectionModel([1, 2, 3]); + + expect(model.isEmpty()).toBe(true); + + model.select(1); + + expect(model.isEmpty()).toBe(false); + }); + + it('should throw when trying to select a value that is not in the list of options', () => { + let model = new MdSelectionModel([]); + expect(() => model.select(1)).toThrow(); + }); + + it('should throw when trying to deselect a value that is not in the list of options', () => { + let model = new MdSelectionModel([]); + expect(() => model.deselect(1)).toThrow(); + }); + + it('should be able to clear the selected options', () => { + let model = new MdSelectionModel([1, 2, 3], true); + + model.select(1); + model.select(2); + + expect(model.selected.length).toBe(2); + + model.clear(); + + expect(model.selected.length).toBe(0); + expect(model.isEmpty()).toBe(true); + }); + + it('should not expose the internal array of options directly', () => { + let options = [1, 2, 3]; + let model = new MdSelectionModel(options); + + expect(model.options).not.toBe(options, 'Expect the array to be different'); + expect(model.options).toEqual(options); + }); + + it('should not expose the internal array of selected values directly', () => { + let model = new MdSelectionModel([1, 2, 3], true, [1, 2]); + let selected = model.selected; + + selected.length = 0; + expect(model.selected).toEqual([1, 2]); + }); +}); diff --git a/src/lib/core/selection/selection.ts b/src/lib/core/selection/selection.ts new file mode 100644 index 000000000000..4dba615307a6 --- /dev/null +++ b/src/lib/core/selection/selection.ts @@ -0,0 +1,168 @@ +import {Subject} from 'rxjs/Subject'; + + +/** + * Class to be used to power selecting one or more options from a list. + * @docs-private + */ +export class MdSelectionModel { + constructor( + private _options: any[], + private _isMulti = false, + initiallySelectedValues?: any[]) { + + if (initiallySelectedValues && initiallySelectedValues.length) { + if (_isMulti) { + initiallySelectedValues.forEach(value => this._select(value)); + } else { + this._select(initiallySelectedValues[0]); + } + + // Clear the array in order to avoid firing the change event for preselected values. + this._unflushedSelectedValues.length = 0; + } + } + + /** Event emitted when the value has changed. */ + onChange: Subject = new Subject(); + + /** Currently-selected values. */ + private _selectedValues: any[] = []; + + /** Keeps track of the deselected options that haven't been emitted by the change event. */ + private _unflushedDeselectedValues: any[] = []; + + /** Keeps track of the selected option that haven't been emitted by the change event. */ + private _unflushedSelectedValues: any[] = []; + + /** List of available available options. */ + get options(): any[] { return this._options.slice(); } + set options(newOptions: any[]) { + this._options = newOptions.slice(); + + // Remove any options that are no longer a part of the options and skip throwing an error. + // Uses a reverse while, because it's modifying the array that it is iterating. + let i = this._selectedValues.length; + + while (i--) { + if (this._options.indexOf(this._selectedValues[i]) === -1) { + this._deselect(this._selectedValues[i]); + } + } + + this._flushChangeEvent(); + } + + /** Selected value(s). */ + get selected(): any[] { + return this._selectedValues.slice(); + } + + /** + * Selects a value. + */ + select(value: any): void { + this._verifyExistence(value); + + if (!this._isMulti && !this.isEmpty()) { + this._deselect(this._selectedValues[0]); + } + + this._select(value); + this._flushChangeEvent(); + } + + /** + * Deselects a value. + */ + deselect(value: any): void { + this._verifyExistence(value); + this._deselect(value); + this._flushChangeEvent(); + } + + /** + * Determines whether a value is selected. + */ + isSelected(value: any): boolean { + return this._selectedValues.indexOf(value) > -1; + } + + /** + * Determines whether the model has a value. + */ + isEmpty(): boolean { + return this._selectedValues.length === 0; + } + + /** + * Selects all of the options. Only applicable when the model is in multi-selection mode. + */ + selectAll(): void { + if (!this._isMulti) { + throw new Error('selectAll is only allowed in multi-selection mode'); + } + + this._options.forEach(option => this._select(option)); + this._flushChangeEvent(); + } + + /** + * Clears all of the selected values. + */ + clear(): void { + if (!this.isEmpty()) { + let i = this._selectedValues.length; + + // Use a reverse while, because we're modifying the array that we're iterating. + while (i--) { + this._deselect(this._selectedValues[i]); + } + + this._flushChangeEvent(); + } + } + + /** Emits a change event and clears the records of selected and deselected values. */ + private _flushChangeEvent() { + if (this._unflushedSelectedValues.length || this._unflushedDeselectedValues.length) { + let event = new MdSelectionChange(this._unflushedSelectedValues, + this._unflushedDeselectedValues); + + this.onChange.next(event); + this._unflushedDeselectedValues = []; + this._unflushedSelectedValues = []; + } + } + + /** Selects a value. */ + private _select(value: any) { + if (!this.isSelected(value)) { + this._selectedValues.push(value); + this._unflushedSelectedValues.push(value); + } + } + + /** Deselects a value. */ + private _deselect(value: any) { + if (this.isSelected(value)) { + this._selectedValues.splice(this._selectedValues.indexOf(value), 1); + this._unflushedDeselectedValues.push(value); + } + } + + /** Throws an error if a value isn't a part of the list of options. */ + private _verifyExistence(value: any): void { + if (this._options.indexOf(value) === -1) { + throw new Error('Attempting to manipulate an option that is not part of the option list.'); + } + } +} + +/** + * Describes an event emitted when the value of a MdSelectionModel has changed. + * @docs-private + */ +export class MdSelectionChange { + constructor(public added?: any, public removed?: any) { } +} From 6bcc64458c8aac7c17ad3745cdc4d9137fe44788 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 11 Jan 2017 21:53:14 +0100 Subject: [PATCH 2/5] Refactor and simplify based on the feedback. --- src/lib/core/selection/selection.spec.ts | 128 ++++--------------- src/lib/core/selection/selection.ts | 154 +++++++++-------------- 2 files changed, 80 insertions(+), 202 deletions(-) diff --git a/src/lib/core/selection/selection.spec.ts b/src/lib/core/selection/selection.spec.ts index ea21f28683d1..b05e5236d9fd 100644 --- a/src/lib/core/selection/selection.spec.ts +++ b/src/lib/core/selection/selection.spec.ts @@ -1,11 +1,11 @@ -import {MdSelectionModel} from './selection'; +import {SelectionModel} from './selection'; -describe('MdSelectionModel', () => { +describe('SelectionModel', () => { describe('single selection', () => { - let model: MdSelectionModel; + let model: SelectionModel; - beforeEach(() => model = new MdSelectionModel([1, 2, 3])); + beforeEach(() => model = new SelectionModel()); it('should be able to select a single value', () => { model.select(1); @@ -22,12 +22,8 @@ describe('MdSelectionModel', () => { expect(model.isSelected(2)).toBe(true); }); - it('should throw an error when trying to select all of the values', () => { - expect(() => model.selectAll()).toThrow(); - }); - it('should only preselect one value', () => { - model = new MdSelectionModel([1, 2, 3], false, [1, 2]); + model = new SelectionModel(false, [1, 2]); expect(model.selected.length).toBe(1); expect(model.isSelected(1)).toBe(true); @@ -36,9 +32,9 @@ describe('MdSelectionModel', () => { }); describe('multiple selection', () => { - let model: MdSelectionModel; + let model: SelectionModel; - beforeEach(() => model = new MdSelectionModel([1, 2, 3], true)); + beforeEach(() => model = new SelectionModel(true)); it('should be able to select multiple options at the same time', () => { model.select(1); @@ -50,54 +46,18 @@ describe('MdSelectionModel', () => { }); it('should be able to preselect multiple options', () => { - model = new MdSelectionModel([1, 2, 3], true, [1, 2]); + model = new SelectionModel(true, [1, 2]); expect(model.selected.length).toBe(2); expect(model.isSelected(1)).toBe(true); expect(model.isSelected(2)).toBe(true); }); - - it('should be able to select all of the options', () => { - model.selectAll(); - expect(model.options.every(value => model.isSelected(value))).toBe(true); - }); - }); - - describe('updating the options', () => { - let model: MdSelectionModel; - - beforeEach(() => model = new MdSelectionModel([1, 2, 3], true)); - - it('should be able to update the list of options', () => { - let newOptions = [1, 2, 3, 4, 5]; - - model.options = newOptions; - - expect(model.options).not.toBe(newOptions, 'Expected the array to have been cloned.'); - expect(model.options).toEqual(newOptions); - }); - - it('should keep the selected value', () => { - model.select(2); - - model.options = [1, 2, 3, 4, 5]; - - expect(model.isSelected(2)).toBe(true); - }); - - it('should deselect values that are not longer in the list', () => { - model.select(1); - - model.options = [2, 3, 4]; - - expect(model.isSelected(1)).toBe(false); - }); }); describe('onChange event', () => { it('should return both the added and removed values', () => { - let model = new MdSelectionModel([1, 2, 3]); - let spy = jasmine.createSpy('MdSelectionModel change event'); + let model = new SelectionModel(); + let spy = jasmine.createSpy('SelectionModel change event'); model.select(1); @@ -113,12 +73,12 @@ describe('MdSelectionModel', () => { }); describe('selection', () => { - let model: MdSelectionModel; + let model: SelectionModel; let spy: jasmine.Spy; beforeEach(() => { - model = new MdSelectionModel([1, 2, 3], true); - spy = jasmine.createSpy('MdSelectionModel change event'); + model = new SelectionModel(true); + spy = jasmine.createSpy('SelectionModel change event'); model.onChange.subscribe(spy); }); @@ -140,18 +100,9 @@ describe('MdSelectionModel', () => { expect(spy).toHaveBeenCalledTimes(1); }); - it('should emit a single event when selecting all of the values', () => { - model.selectAll(); - - let event = spy.calls.mostRecent().args[0]; - - expect(spy).toHaveBeenCalledTimes(1); - expect(event.added).toEqual([1, 2, 3]); - }); - it('should not emit an event when preselecting values', () => { - model = new MdSelectionModel([1, 2, 3], false, [1]); - spy = jasmine.createSpy('MdSelectionModel initial change event'); + model = new SelectionModel(false, [1]); + spy = jasmine.createSpy('SelectionModel initial change event'); model.onChange.subscribe(spy); expect(spy).not.toHaveBeenCalled(); @@ -159,12 +110,12 @@ describe('MdSelectionModel', () => { }); describe('deselection', () => { - let model: MdSelectionModel; + let model: SelectionModel; let spy: jasmine.Spy; beforeEach(() => { - model = new MdSelectionModel([1, 2, 3], true, [1, 2]); - spy = jasmine.createSpy('MdSelectionModel change event'); + model = new SelectionModel(true, [1, 2, 3]); + spy = jasmine.createSpy('SelectionModel change event'); model.onChange.subscribe(spy); }); @@ -179,7 +130,7 @@ describe('MdSelectionModel', () => { }); it('should not emit an event when a non-selected value is deselected', () => { - model.deselect(3); + model.deselect(4); expect(spy).not.toHaveBeenCalled(); }); @@ -189,23 +140,14 @@ describe('MdSelectionModel', () => { let event = spy.calls.mostRecent().args[0]; expect(spy).toHaveBeenCalledTimes(1); - expect(event.removed).toEqual([2, 1]); + expect(event.removed).toEqual([1, 2, 3]); }); - it('should emit an event when a value is deselected due to it being removed from the options', - () => { - model.options = [4, 5, 6]; - - let event = spy.calls.mostRecent().args[0]; - - expect(spy).toHaveBeenCalledTimes(1); - expect(event.removed).toEqual([2, 1]); - }); }); }); it('should be able to determine whether it is empty', () => { - let model = new MdSelectionModel([1, 2, 3]); + let model = new SelectionModel(); expect(model.isEmpty()).toBe(true); @@ -214,18 +156,8 @@ describe('MdSelectionModel', () => { expect(model.isEmpty()).toBe(false); }); - it('should throw when trying to select a value that is not in the list of options', () => { - let model = new MdSelectionModel([]); - expect(() => model.select(1)).toThrow(); - }); - - it('should throw when trying to deselect a value that is not in the list of options', () => { - let model = new MdSelectionModel([]); - expect(() => model.deselect(1)).toThrow(); - }); - it('should be able to clear the selected options', () => { - let model = new MdSelectionModel([1, 2, 3], true); + let model = new SelectionModel(true); model.select(1); model.select(2); @@ -237,20 +169,4 @@ describe('MdSelectionModel', () => { expect(model.selected.length).toBe(0); expect(model.isEmpty()).toBe(true); }); - - it('should not expose the internal array of options directly', () => { - let options = [1, 2, 3]; - let model = new MdSelectionModel(options); - - expect(model.options).not.toBe(options, 'Expect the array to be different'); - expect(model.options).toEqual(options); - }); - - it('should not expose the internal array of selected values directly', () => { - let model = new MdSelectionModel([1, 2, 3], true, [1, 2]); - let selected = model.selected; - - selected.length = 0; - expect(model.selected).toEqual([1, 2]); - }); }); diff --git a/src/lib/core/selection/selection.ts b/src/lib/core/selection/selection.ts index 4dba615307a6..2a4d547d7d8b 100644 --- a/src/lib/core/selection/selection.ts +++ b/src/lib/core/selection/selection.ts @@ -5,67 +5,50 @@ import {Subject} from 'rxjs/Subject'; * Class to be used to power selecting one or more options from a list. * @docs-private */ -export class MdSelectionModel { - constructor( - private _options: any[], - private _isMulti = false, - initiallySelectedValues?: any[]) { - - if (initiallySelectedValues && initiallySelectedValues.length) { - if (_isMulti) { - initiallySelectedValues.forEach(value => this._select(value)); - } else { - this._select(initiallySelectedValues[0]); - } - - // Clear the array in order to avoid firing the change event for preselected values. - this._unflushedSelectedValues.length = 0; - } - } - - /** Event emitted when the value has changed. */ - onChange: Subject = new Subject(); - +export class SelectionModel { /** Currently-selected values. */ - private _selectedValues: any[] = []; + private _selection: Set = new Set(); /** Keeps track of the deselected options that haven't been emitted by the change event. */ - private _unflushedDeselectedValues: any[] = []; + private _deselectedToEmit: T[] = []; /** Keeps track of the selected option that haven't been emitted by the change event. */ - private _unflushedSelectedValues: any[] = []; - - /** List of available available options. */ - get options(): any[] { return this._options.slice(); } - set options(newOptions: any[]) { - this._options = newOptions.slice(); + private _selectedToEmit: T[] = []; - // Remove any options that are no longer a part of the options and skip throwing an error. - // Uses a reverse while, because it's modifying the array that it is iterating. - let i = this._selectedValues.length; + /** Cache for the array value of the selected items. */ + private _selected: T[]; - while (i--) { - if (this._options.indexOf(this._selectedValues[i]) === -1) { - this._deselect(this._selectedValues[i]); - } + /** Selected value(s). */ + get selected(): T[] { + if (!this._selected) { + this._selected = Array.from(this._selection.values()); } - this._flushChangeEvent(); + return this._selected; } - /** Selected value(s). */ - get selected(): any[] { - return this._selectedValues.slice(); + /** Event emitted when the value has changed. */ + onChange: Subject> = new Subject(); + + constructor(private _isMulti = false, initiallySelectedValues?: T[]) { + if (initiallySelectedValues) { + if (_isMulti) { + initiallySelectedValues.forEach(value => this._select(value)); + } else { + this._select(initiallySelectedValues[0]); + } + + // Clear the array in order to avoid firing the change event for preselected values. + this._selectedToEmit.length = 0; + } } /** - * Selects a value. + * Selects a value or an array of values. */ - select(value: any): void { - this._verifyExistence(value); - - if (!this._isMulti && !this.isEmpty()) { - this._deselect(this._selectedValues[0]); + select(value: T): void { + if (!this._isMulti) { + this._clear(); } this._select(value); @@ -73,10 +56,9 @@ export class MdSelectionModel { } /** - * Deselects a value. + * Deselects a value or an array of values. */ - deselect(value: any): void { - this._verifyExistence(value); + deselect(value: T): void { this._deselect(value); this._flushChangeEvent(); } @@ -84,77 +66,57 @@ export class MdSelectionModel { /** * Determines whether a value is selected. */ - isSelected(value: any): boolean { - return this._selectedValues.indexOf(value) > -1; + isSelected(value: T): boolean { + return this._selection.has(value); } /** * Determines whether the model has a value. */ isEmpty(): boolean { - return this._selectedValues.length === 0; - } - - /** - * Selects all of the options. Only applicable when the model is in multi-selection mode. - */ - selectAll(): void { - if (!this._isMulti) { - throw new Error('selectAll is only allowed in multi-selection mode'); - } - - this._options.forEach(option => this._select(option)); - this._flushChangeEvent(); + return this._selection.size === 0; } /** * Clears all of the selected values. */ clear(): void { - if (!this.isEmpty()) { - let i = this._selectedValues.length; - - // Use a reverse while, because we're modifying the array that we're iterating. - while (i--) { - this._deselect(this._selectedValues[i]); - } - - this._flushChangeEvent(); - } + this._clear(); + this._flushChangeEvent(); } /** Emits a change event and clears the records of selected and deselected values. */ private _flushChangeEvent() { - if (this._unflushedSelectedValues.length || this._unflushedDeselectedValues.length) { - let event = new MdSelectionChange(this._unflushedSelectedValues, - this._unflushedDeselectedValues); + if (this._selectedToEmit.length || this._deselectedToEmit.length) { + let eventData = new SelectionChange(this._selectedToEmit, this._deselectedToEmit); - this.onChange.next(event); - this._unflushedDeselectedValues = []; - this._unflushedSelectedValues = []; + this.onChange.next(eventData); + this._deselectedToEmit = []; + this._selectedToEmit = []; + this._selected = null; } } /** Selects a value. */ - private _select(value: any) { + private _select(value: T) { if (!this.isSelected(value)) { - this._selectedValues.push(value); - this._unflushedSelectedValues.push(value); + this._selection.add(value); + this._selectedToEmit.push(value); } } - /** Deselects a value. */ - private _deselect(value: any) { - if (this.isSelected(value)) { - this._selectedValues.splice(this._selectedValues.indexOf(value), 1); - this._unflushedDeselectedValues.push(value); - } - } + /** Deselects a value. */ + private _deselect(value: T) { + if (this.isSelected(value)) { + this._selection.delete(value); + this._deselectedToEmit.push(value); + } + } - /** Throws an error if a value isn't a part of the list of options. */ - private _verifyExistence(value: any): void { - if (this._options.indexOf(value) === -1) { - throw new Error('Attempting to manipulate an option that is not part of the option list.'); + /** Clears out the selected values. */ + private _clear() { + if (!this.isEmpty()) { + this._selection.forEach(value => this._deselect(value)); } } } @@ -163,6 +125,6 @@ export class MdSelectionModel { * Describes an event emitted when the value of a MdSelectionModel has changed. * @docs-private */ -export class MdSelectionChange { - constructor(public added?: any, public removed?: any) { } +export class SelectionChange { + constructor(public added?: T[], public removed?: T[]) { } } From 382dac84868257abea8ddb1750e1d028dcf491c1 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 11 Jan 2017 21:59:49 +0100 Subject: [PATCH 3/5] Rename private method. --- src/lib/core/selection/selection.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/core/selection/selection.ts b/src/lib/core/selection/selection.ts index 2a4d547d7d8b..97f723226a99 100644 --- a/src/lib/core/selection/selection.ts +++ b/src/lib/core/selection/selection.ts @@ -52,7 +52,7 @@ export class SelectionModel { } this._select(value); - this._flushChangeEvent(); + this._emitChangeEvent(); } /** @@ -60,7 +60,7 @@ export class SelectionModel { */ deselect(value: T): void { this._deselect(value); - this._flushChangeEvent(); + this._emitChangeEvent(); } /** @@ -82,11 +82,11 @@ export class SelectionModel { */ clear(): void { this._clear(); - this._flushChangeEvent(); + this._emitChangeEvent(); } /** Emits a change event and clears the records of selected and deselected values. */ - private _flushChangeEvent() { + private _emitChangeEvent() { if (this._selectedToEmit.length || this._deselectedToEmit.length) { let eventData = new SelectionChange(this._selectedToEmit, this._deselectedToEmit); From e82403da3ced3ab17a82f8138d63e94d74c72978 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 11 Jan 2017 22:35:47 +0100 Subject: [PATCH 4/5] Move the clearing logic to _select and shuffle the method order. --- src/lib/core/selection/selection.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib/core/selection/selection.ts b/src/lib/core/selection/selection.ts index 97f723226a99..6773afa2e40c 100644 --- a/src/lib/core/selection/selection.ts +++ b/src/lib/core/selection/selection.ts @@ -47,10 +47,6 @@ export class SelectionModel { * Selects a value or an array of values. */ select(value: T): void { - if (!this._isMulti) { - this._clear(); - } - this._select(value); this._emitChangeEvent(); } @@ -63,6 +59,14 @@ export class SelectionModel { this._emitChangeEvent(); } + /** + * Clears all of the selected values. + */ + clear(): void { + this._clear(); + this._emitChangeEvent(); + } + /** * Determines whether a value is selected. */ @@ -77,14 +81,6 @@ export class SelectionModel { return this._selection.size === 0; } - /** - * Clears all of the selected values. - */ - clear(): void { - this._clear(); - this._emitChangeEvent(); - } - /** Emits a change event and clears the records of selected and deselected values. */ private _emitChangeEvent() { if (this._selectedToEmit.length || this._deselectedToEmit.length) { @@ -100,6 +96,10 @@ export class SelectionModel { /** Selects a value. */ private _select(value: T) { if (!this.isSelected(value)) { + if (!this._isMulti) { + this._clear(); + } + this._selection.add(value); this._selectedToEmit.push(value); } From 70e47687efb013870729c859414f3ca845b289f6 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 11 Jan 2017 22:48:39 +0100 Subject: [PATCH 5/5] Rename private methods. --- src/lib/core/selection/selection.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/core/selection/selection.ts b/src/lib/core/selection/selection.ts index 6773afa2e40c..91a594b51e7a 100644 --- a/src/lib/core/selection/selection.ts +++ b/src/lib/core/selection/selection.ts @@ -33,9 +33,9 @@ export class SelectionModel { constructor(private _isMulti = false, initiallySelectedValues?: T[]) { if (initiallySelectedValues) { if (_isMulti) { - initiallySelectedValues.forEach(value => this._select(value)); + initiallySelectedValues.forEach(value => this._markSelected(value)); } else { - this._select(initiallySelectedValues[0]); + this._markSelected(initiallySelectedValues[0]); } // Clear the array in order to avoid firing the change event for preselected values. @@ -47,7 +47,7 @@ export class SelectionModel { * Selects a value or an array of values. */ select(value: T): void { - this._select(value); + this._markSelected(value); this._emitChangeEvent(); } @@ -55,7 +55,7 @@ export class SelectionModel { * Deselects a value or an array of values. */ deselect(value: T): void { - this._deselect(value); + this._unmarkSelected(value); this._emitChangeEvent(); } @@ -63,7 +63,7 @@ export class SelectionModel { * Clears all of the selected values. */ clear(): void { - this._clear(); + this._unmarkAll(); this._emitChangeEvent(); } @@ -94,10 +94,10 @@ export class SelectionModel { } /** Selects a value. */ - private _select(value: T) { + private _markSelected(value: T) { if (!this.isSelected(value)) { if (!this._isMulti) { - this._clear(); + this._unmarkAll(); } this._selection.add(value); @@ -106,7 +106,7 @@ export class SelectionModel { } /** Deselects a value. */ - private _deselect(value: T) { + private _unmarkSelected(value: T) { if (this.isSelected(value)) { this._selection.delete(value); this._deselectedToEmit.push(value); @@ -114,9 +114,9 @@ export class SelectionModel { } /** Clears out the selected values. */ - private _clear() { + private _unmarkAll() { if (!this.isEmpty()) { - this._selection.forEach(value => this._deselect(value)); + this._selection.forEach(value => this._unmarkSelected(value)); } } }