Skip to content

fix(selection-list): fix option value coercion and selection events #6901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 12, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 106 additions & 2 deletions src/lib/list/selection-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
import {Component, DebugElement} from '@angular/core';
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {MatListModule, MatListOption, MatSelectionList} from './index';
import {MatListModule, MatListOption, MatSelectionList, MatSelectionListOptionEvent} from './index';


describe('MatSelectionList', () => {
Expand Down Expand Up @@ -483,9 +483,88 @@ describe('MatSelectionList', () => {
expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
});
});
});


describe('with multiple values', () => {
let fixture: ComponentFixture<SelectionListWithMultipleValues>;
let listOption: DebugElement[];
let listItemEl: DebugElement;
let selectionList: DebugElement;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [
SelectionListWithMultipleValues
],
});

TestBed.compileComponents();
}));

beforeEach(async(() => {
fixture = TestBed.createComponent(SelectionListWithMultipleValues);
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
fixture.detectChanges();
}));

it('should have a value for each item', () => {
expect(listOption[0].componentInstance.value).toBe(1);
expect(listOption[1].componentInstance.value).toBe('a');
expect(listOption[2].componentInstance.value).toBe(true);
});

});

describe('with option selected events', () => {
let fixture: ComponentFixture<SelectionListWithOptionEvents>;
let testComponent: SelectionListWithOptionEvents;
let listOption: DebugElement[];
let selectionList: DebugElement;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [
SelectionListWithOptionEvents
],
});

TestBed.compileComponents();
}));

beforeEach(async(() => {
fixture = TestBed.createComponent(SelectionListWithOptionEvents);
testComponent = fixture.debugElement.componentInstance;
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
fixture.detectChanges();
}));

it('should trigger the selected and deselected events when clicked in succession.', () => {

let selected: boolean = false;

spyOn(testComponent, 'onOptionSelectionChange')
.and.callFake((event: MatSelectionListOptionEvent) => {
selected = event.selected;
});

listOption[0].nativeElement.click();
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(1);
expect(selected).toBe(true);

listOption[0].nativeElement.click();
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(2);
expect(selected).toBe(false);
});

});

});

@Component({template: `
<mat-selection-list id="selection-list-1">
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
Expand Down Expand Up @@ -579,3 +658,28 @@ class SelectionListWithTabindexBinding {
tabIndex: number;
disabled: boolean;
}

@Component({template: `
<mat-selection-list id = "selection-list-5">
Copy link
Contributor

@rafaelss95 rafaelss95 Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the spaces around: id="selection-list-5"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I hit the format button on my side, but there are spaces around most of the unit test component's id's in this file.

I'll fix them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. There are a lot of strange spaces in this spec file.

<mat-list-option [value]="1" checkboxPosition="after">
1
</mat-list-option>
<mat-list-option value="a" checkboxPosition="after">
a
</mat-list-option>
<mat-list-option [value]="true" checkboxPosition="after">
true
</mat-list-option>
</mat-selection-list>`})
class SelectionListWithMultipleValues {
}

@Component({template: `
<mat-selection-list id = "selection-list-6">
<mat-list-option (selectionChange)="onOptionSelectionChange($event)">
Inbox
</mat-list-option>
</mat-selection-list>`})
class SelectionListWithOptionEvents {
onOptionSelectionChange: (event?: MatSelectionListOptionEvent) => void = () => {};
}
37 changes: 21 additions & 16 deletions src/lib/list/selection-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export class MatListOptionBase {}
export const _MatListOptionMixinBase = mixinDisableRipple(MatListOptionBase);

/** Event emitted by a selection-list whenever the state of an option is changed. */
export interface MatSelectionListOptionEvent {
export class MatSelectionListOptionEvent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MatListOptionChange?

option: MatListOption;
selected: boolean;
}
Copy link
Contributor

@rafaelss95 rafaelss95 Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep the consistency between this and other components:

/** Change event object emitted by MatListOption */
export class MatListOptionChange {
  /** The source MatListOption of the event. */
  source: MatListOption;
  /** The new `selected` value of the option. */
  selected: boolean;
}

See checkbox for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So checked instead of selected? Gotcha.

Copy link
Contributor

@rafaelss95 rafaelss95 Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, all changes above (to keep consistency). Also with comments so it can be shown in docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By others you mean other checkboxes? Add comments?

Changing it from selected to checked seems like a step in the wrong direction, since it is MatListOption.selected, not MatListOption.checked.

Copy link
Contributor

@rafaelss95 rafaelss95 Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right (bad copy). Comments === docs description.

What about source instead of option? All change events use source?
Also, MatListOptionChange instead of MatSelectionListOptionEvent?

I updated the code above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll change that.


/**
Expand Down Expand Up @@ -86,7 +87,6 @@ export interface MatSelectionListOptionEvent {
export class MatListOption extends _MatListOptionMixinBase
implements AfterContentInit, OnInit, OnDestroy, FocusableOption, CanDisableRipple {
private _lineSetter: MatLineSetter;
private _selected: boolean = false;
private _disabled: boolean = false;

/** Whether the option has focus. */
Expand All @@ -97,34 +97,29 @@ export class MatListOption extends _MatListOptionMixinBase
/** Whether the label should appear before or after the checkbox. Defaults to 'after' */
@Input() checkboxPosition: 'before' | 'after' = 'after';

/** Value of the option */
@Input() value: any;

/** Whether the option is disabled. */
@Input()
get disabled() { return (this.selectionList && this.selectionList.disabled) || this._disabled; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

: boolean?

set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

: boolean?

Copy link
Contributor Author

@dereekb dereekb Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value is the actual selection option value.

We don't want to coerce it to a boolean, again! 😛

Edit: Misread

Copy link
Contributor

@rafaelss95 rafaelss95 Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well... for selected we have: set selected(value: boolean) { const isSelected = coerceBooleanProperty(value); ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. The highlighted code I see is both on this section:

/** Value of the option */
-  @Input() value: any;

So I was kind of guessing what you meant. Making those changes now.


/** Value of the option */
@Input() value: any;

/** Whether the option is selected. */
@Input()
get selected() { return this._selected; }
get selected() { return this.selectionList.selectedOptions.isSelected(this); }
Copy link
Contributor

@rafaelss95 rafaelss95 Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add :boolean here? get selected(): boolean.

set selected(value: boolean) {
const isSelected = coerceBooleanProperty(value);

if (isSelected !== this._selected) {
const selectionModel = this.selectionList.selectedOptions;

this._selected = isSelected;
isSelected ? selectionModel.select(this) : selectionModel.deselect(this);
if (isSelected !== this.selected) {
this.selectionList.selectedOptions.toggle(this);
this._changeDetector.markForCheck();
this.selectionChange.emit(this._createChangeEvent());
}
}

/** Emitted when the option is selected. */
@Output() selectChange = new EventEmitter<MatSelectionListOptionEvent>();

/** Emitted when the option is deselected. */
@Output() deselected = new EventEmitter<MatSelectionListOptionEvent>();
/** Emitted when the option is selected or deselected. */
@Output() selectionChange = new EventEmitter<MatSelectionListOptionEvent>();

constructor(private _renderer: Renderer2,
private _element: ElementRef,
Expand Down Expand Up @@ -174,6 +169,16 @@ export class MatListOption extends _MatListOptionMixinBase
this.selectionList._setFocusedOption(this);
}

/** Creates a selection event object from the specified option. */
private _createChangeEvent(option: MatListOption = this): MatSelectionListOptionEvent {
let event = new MatSelectionListOptionEvent();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const?


event.option = option;
event.selected = option.selected;

return event;
}

/** Retrieves the DOM element of the component host. */
_getHostElement(): HTMLElement {
return this._element.nativeElement;
Expand Down