mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat: add a common class to be used when dealing with selection logic (#2562)
* 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. * Refactor and simplify based on the feedback. * Rename private method. * Move the clearing logic to _select and shuffle the method order. * Rename private methods.
This commit is contained in:
committed by
tinayuangao
parent
e18ab5df0a
commit
c295fa92c7
@@ -72,6 +72,10 @@ export {
|
||||
LIVE_ANNOUNCER_ELEMENT_TOKEN,
|
||||
LIVE_ANNOUNCER_PROVIDER,
|
||||
} from './a11y/live-announcer';
|
||||
|
||||
// Selection
|
||||
export * from './selection/selection';
|
||||
|
||||
/** @deprecated */
|
||||
export {LiveAnnouncer as MdLiveAnnouncer} from './a11y/live-announcer';
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import {SelectionModel} from './selection';
|
||||
|
||||
|
||||
describe('SelectionModel', () => {
|
||||
describe('single selection', () => {
|
||||
let model: SelectionModel<any>;
|
||||
|
||||
beforeEach(() => model = new SelectionModel());
|
||||
|
||||
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 only preselect one value', () => {
|
||||
model = new SelectionModel(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: SelectionModel<any>;
|
||||
|
||||
beforeEach(() => model = new SelectionModel(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 SelectionModel(true, [1, 2]);
|
||||
|
||||
expect(model.selected.length).toBe(2);
|
||||
expect(model.isSelected(1)).toBe(true);
|
||||
expect(model.isSelected(2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange event', () => {
|
||||
it('should return both the added and removed values', () => {
|
||||
let model = new SelectionModel();
|
||||
let spy = jasmine.createSpy('SelectionModel 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: SelectionModel<any>;
|
||||
let spy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
model = new SelectionModel(true);
|
||||
spy = jasmine.createSpy('SelectionModel 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 not emit an event when preselecting values', () => {
|
||||
model = new SelectionModel(false, [1]);
|
||||
spy = jasmine.createSpy('SelectionModel initial change event');
|
||||
model.onChange.subscribe(spy);
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deselection', () => {
|
||||
let model: SelectionModel<any>;
|
||||
let spy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
model = new SelectionModel(true, [1, 2, 3]);
|
||||
spy = jasmine.createSpy('SelectionModel 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(4);
|
||||
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([1, 2, 3]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to determine whether it is empty', () => {
|
||||
let model = new SelectionModel();
|
||||
|
||||
expect(model.isEmpty()).toBe(true);
|
||||
|
||||
model.select(1);
|
||||
|
||||
expect(model.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to clear the selected options', () => {
|
||||
let model = new SelectionModel(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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
|
||||
/**
|
||||
* Class to be used to power selecting one or more options from a list.
|
||||
* @docs-private
|
||||
*/
|
||||
export class SelectionModel<T> {
|
||||
/** Currently-selected values. */
|
||||
private _selection: Set<T> = new Set();
|
||||
|
||||
/** Keeps track of the deselected options that haven't been emitted by the change event. */
|
||||
private _deselectedToEmit: T[] = [];
|
||||
|
||||
/** Keeps track of the selected option that haven't been emitted by the change event. */
|
||||
private _selectedToEmit: T[] = [];
|
||||
|
||||
/** Cache for the array value of the selected items. */
|
||||
private _selected: T[];
|
||||
|
||||
/** Selected value(s). */
|
||||
get selected(): T[] {
|
||||
if (!this._selected) {
|
||||
this._selected = Array.from(this._selection.values());
|
||||
}
|
||||
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
/** Event emitted when the value has changed. */
|
||||
onChange: Subject<SelectionChange<T>> = new Subject();
|
||||
|
||||
constructor(private _isMulti = false, initiallySelectedValues?: T[]) {
|
||||
if (initiallySelectedValues) {
|
||||
if (_isMulti) {
|
||||
initiallySelectedValues.forEach(value => this._markSelected(value));
|
||||
} else {
|
||||
this._markSelected(initiallySelectedValues[0]);
|
||||
}
|
||||
|
||||
// Clear the array in order to avoid firing the change event for preselected values.
|
||||
this._selectedToEmit.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a value or an array of values.
|
||||
*/
|
||||
select(value: T): void {
|
||||
this._markSelected(value);
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselects a value or an array of values.
|
||||
*/
|
||||
deselect(value: T): void {
|
||||
this._unmarkSelected(value);
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all of the selected values.
|
||||
*/
|
||||
clear(): void {
|
||||
this._unmarkAll();
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is selected.
|
||||
*/
|
||||
isSelected(value: T): boolean {
|
||||
return this._selection.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the model has a value.
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this._selection.size === 0;
|
||||
}
|
||||
|
||||
/** Emits a change event and clears the records of selected and deselected values. */
|
||||
private _emitChangeEvent() {
|
||||
if (this._selectedToEmit.length || this._deselectedToEmit.length) {
|
||||
let eventData = new SelectionChange(this._selectedToEmit, this._deselectedToEmit);
|
||||
|
||||
this.onChange.next(eventData);
|
||||
this._deselectedToEmit = [];
|
||||
this._selectedToEmit = [];
|
||||
this._selected = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Selects a value. */
|
||||
private _markSelected(value: T) {
|
||||
if (!this.isSelected(value)) {
|
||||
if (!this._isMulti) {
|
||||
this._unmarkAll();
|
||||
}
|
||||
|
||||
this._selection.add(value);
|
||||
this._selectedToEmit.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Deselects a value. */
|
||||
private _unmarkSelected(value: T) {
|
||||
if (this.isSelected(value)) {
|
||||
this._selection.delete(value);
|
||||
this._deselectedToEmit.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears out the selected values. */
|
||||
private _unmarkAll() {
|
||||
if (!this.isEmpty()) {
|
||||
this._selection.forEach(value => this._unmarkSelected(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an event emitted when the value of a MdSelectionModel has changed.
|
||||
* @docs-private
|
||||
*/
|
||||
export class SelectionChange<T> {
|
||||
constructor(public added?: T[], public removed?: T[]) { }
|
||||
}
|
||||
Reference in New Issue
Block a user