mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(chip-list): implement FormFieldControl and ControlValueAccessor (#6686)
BREAKING CHANGES: - Deprecated: @Output() select, and @Output() deselect - Added @Output() onSelectionChange = EventEmitter<MdChipSelectionChange>
This commit is contained in:
committed by
Jeremy Elbourn
parent
6fa9e6f88b
commit
7a42706614
@@ -45,20 +45,22 @@
|
||||
<code><md-form-field></code>.
|
||||
</p>
|
||||
|
||||
|
||||
<md-form-field>
|
||||
<md-chip-list mdPrefix #chipList1>
|
||||
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
|
||||
<button md-button (click)="tabIndex = (tabIndex === 0 ? -1 : 0)">
|
||||
Set tabIndex to {{tabIndex === 0 ? -1 : 0}}
|
||||
</button>
|
||||
<md-form-field class="has-chip-list">
|
||||
<md-chip-list [tabIndex]="tabIndex" #chipList1 [(ngModel)]="selectedPeople" required>
|
||||
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
|
||||
[removable]="removable" (remove)="remove(person)">
|
||||
{{person.name}}
|
||||
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
|
||||
</md-chip>
|
||||
<input placeholder="New Contributor..."
|
||||
[mdChipInputFor]="chipList1"
|
||||
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[mdChipInputAddOnBlur]="false"
|
||||
(mdChipInputTokenEnd)="add($event)" />
|
||||
</md-chip-list>
|
||||
<input mdInput placeholder="New Contributor..."
|
||||
[mdChipInputFor]="chipList1"
|
||||
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[mdChipInputAddOnBlur]="addOnBlur"
|
||||
(mdChipInputTokenEnd)="add($event)" />
|
||||
</md-form-field>
|
||||
|
||||
|
||||
@@ -68,21 +70,37 @@
|
||||
With <code>mdChipInput</code> the input work with chip-list
|
||||
</p>
|
||||
|
||||
<md-chip-list #chipList2>
|
||||
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
|
||||
[removable]="removable" (remove)="remove(person)">
|
||||
{{person.name}}
|
||||
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="New Contributor..."
|
||||
<md-chip-list [tabIndex]="tabIndex" #chipList2 [(ngModel)]="selectedPeople" required>
|
||||
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
|
||||
[removable]="removable" (remove)="remove(person)">
|
||||
{{person.name}}
|
||||
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
<input placeholder="New Contributor..."
|
||||
[mdChipInputFor]="chipList2"
|
||||
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[mdChipInputAddOnBlur]="addOnBlur"
|
||||
(mdChipInputTokenEnd)="add($event)" />
|
||||
</md-form-field>
|
||||
|
||||
<p>
|
||||
Chips list without input
|
||||
</p>
|
||||
|
||||
|
||||
<md-form-field class="has-chip-list">
|
||||
<md-chip-list #chipList3>
|
||||
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
|
||||
[removable]="removable" (remove)="remove(person)">
|
||||
{{person.name}}
|
||||
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
</md-form-field>
|
||||
|
||||
|
||||
<p>
|
||||
The example above has overridden the <code>[separatorKeys]</code> input to allow for
|
||||
<code>ENTER</code>, <code>COMMA</code> and <code>SEMI COLON</code> keys.
|
||||
@@ -108,6 +126,28 @@
|
||||
{{aColor.name}}
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
|
||||
<h4>NgModel with chip list</h4>
|
||||
<md-chip-list [multiple]="true" [(ngModel)]="selectedColors">
|
||||
<md-chip *ngFor="let aColor of availableColors" color="{{aColor.color}}"
|
||||
[value]="aColor.name" (remove)="removeColor(aColor)">
|
||||
{{aColor.name}}
|
||||
<md-icon mdChipRemove>cancel</md-icon>
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
|
||||
The selected colors are <span *ngFor="let color of selectedColors"> {{color}}</span>.
|
||||
|
||||
<h4>NgModel with single selection chip list</h4>
|
||||
<md-chip-list [(ngModel)]="selectedColor">
|
||||
<md-chip *ngFor="let aColor of availableColors" color="{{aColor.color}}"
|
||||
[value]="aColor.name" (remove)="removeColor(aColor)">
|
||||
{{aColor.name}}
|
||||
<md-icon mdChipRemove>cancel</md-icon>
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
|
||||
The selected color is {{selectedColor}}.
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
</div>
|
||||
|
||||
@@ -25,3 +25,7 @@
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.has-chip-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface DemoColor {
|
||||
styleUrls: ['chips-demo.css']
|
||||
})
|
||||
export class ChipsDemo {
|
||||
tabIndex: number = 0;
|
||||
visible: boolean = true;
|
||||
color: string = '';
|
||||
selectable: boolean = true;
|
||||
@@ -30,6 +31,8 @@ export class ChipsDemo {
|
||||
// Enter, comma, semi-colon
|
||||
separatorKeysCodes = [ENTER, COMMA, 186];
|
||||
|
||||
selectedPeople = null;
|
||||
|
||||
people: Person[] = [
|
||||
{ name: 'Kara' },
|
||||
{ name: 'Jeremy' },
|
||||
@@ -73,7 +76,23 @@ export class ChipsDemo {
|
||||
}
|
||||
}
|
||||
|
||||
removeColor(color: DemoColor) {
|
||||
let index = this.availableColors.indexOf(color);
|
||||
|
||||
if (index >= 0) {
|
||||
this.availableColors.splice(index, 1);
|
||||
}
|
||||
|
||||
index = this.selectedColors.indexOf(color.name);
|
||||
|
||||
if (index >= 0) {
|
||||
this.selectedColors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisible(): void {
|
||||
this.visible = false;
|
||||
}
|
||||
selectedColors: any[] = ['Primary', 'Warn'];
|
||||
selectedColor = 'Accent';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {async, TestBed, ComponentFixture} from '@angular/core/testing';
|
||||
import {MdChipsModule} from './index';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {PlatformModule} from '../core/platform/index';
|
||||
import {MdChipInput, MdChipInputEvent} from './chip-input';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {Directionality} from '@angular/material/core';
|
||||
@@ -21,7 +22,7 @@ describe('MdChipInput', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdChipsModule],
|
||||
imports: [MdChipsModule, PlatformModule],
|
||||
declarations: [TestChipInput],
|
||||
providers: [{
|
||||
provide: Directionality, useFactory: () => {
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, Output, EventEmitter, ElementRef, Input} from '@angular/core';
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
Output,
|
||||
EventEmitter,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {ENTER} from '@angular/material/core';
|
||||
import {MdChipList} from './chip-list';
|
||||
@@ -16,16 +22,21 @@ export interface MdChipInputEvent {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directive that adds chip-specific behaviors to an input element inside <md-form-field>.
|
||||
* May be placed inside or outside of an <md-chip-list>.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'input[mdChipInputFor], input[matChipInputFor]',
|
||||
host: {
|
||||
'class': 'mat-chip-input',
|
||||
'class': 'mat-chip-input mat-input-element',
|
||||
'(keydown)': '_keydown($event)',
|
||||
'(blur)': '_blur()'
|
||||
'(blur)': '_blur()',
|
||||
'(focus)': '_focus()',
|
||||
}
|
||||
})
|
||||
export class MdChipInput {
|
||||
|
||||
focused: boolean = false;
|
||||
_chipList: MdChipList;
|
||||
|
||||
/** Register input for chip list */
|
||||
@@ -33,7 +44,7 @@ export class MdChipInput {
|
||||
set chipList(value: MdChipList) {
|
||||
if (value) {
|
||||
this._chipList = value;
|
||||
this._chipList.registerInput(this._inputElement);
|
||||
this._chipList.registerInput(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +79,13 @@ export class MdChipInput {
|
||||
get matSeparatorKeyCodes() { return this.separatorKeyCodes; }
|
||||
set matSeparatorKeyCodes(v: number[]) { this.separatorKeyCodes = v; }
|
||||
|
||||
@Input() placeholder: string = '';
|
||||
|
||||
get empty(): boolean {
|
||||
let value: string | null = this._inputElement.value;
|
||||
return value == null || value === '';
|
||||
}
|
||||
|
||||
/** The native input element to which this directive is attached. */
|
||||
protected _inputElement: HTMLInputElement;
|
||||
|
||||
@@ -85,6 +103,17 @@ export class MdChipInput {
|
||||
if (this.addOnBlur) {
|
||||
this._emitChipEnd();
|
||||
}
|
||||
this.focused = false;
|
||||
// Blur the chip list if it is not focused
|
||||
if (!this._chipList.focused) {
|
||||
this._chipList._blur();
|
||||
}
|
||||
this._chipList.stateChanges.next();
|
||||
}
|
||||
|
||||
_focus() {
|
||||
this.focused = true;
|
||||
this._chipList.stateChanges.next();
|
||||
}
|
||||
|
||||
/** Checks to see if the (chipEnd) event needs to be emitted. */
|
||||
@@ -100,4 +129,6 @@ export class MdChipInput {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus() { this._inputElement.focus(); }
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import {FocusKeyManager} from '@angular/cdk/a11y';
|
||||
import {createKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {Component, DebugElement, QueryList} from '@angular/core';
|
||||
import {createKeyboardEvent, dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {Component, DebugElement, QueryList, ViewChild, ViewChildren} from '@angular/core';
|
||||
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {
|
||||
BACKSPACE,
|
||||
DELETE,
|
||||
Directionality,
|
||||
ENTER,
|
||||
LEFT_ARROW,
|
||||
RIGHT_ARROW,
|
||||
SPACE,
|
||||
TAB,
|
||||
} from '@angular/material/core';
|
||||
import {MdFormFieldModule} from '@angular/material/form-field';
|
||||
@@ -16,6 +19,7 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
|
||||
import {MdInputModule} from '../input/index';
|
||||
import {MdChip} from './chip';
|
||||
import {MdChipInputEvent} from './chip-input';
|
||||
import {MdChipList, MdChipsModule} from './index';
|
||||
|
||||
|
||||
@@ -31,9 +35,21 @@ describe('MdChipList', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdChipsModule, MdFormFieldModule, MdInputModule, NoopAnimationsModule],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MdChipsModule,
|
||||
MdFormFieldModule,
|
||||
MdInputModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
StandardChipList, FormFieldChipList
|
||||
StandardChipList,
|
||||
FormFieldChipList,
|
||||
BasicChipList,
|
||||
InputChipList,
|
||||
MultiSelectionChipList,
|
||||
FalsyValueChipList,
|
||||
],
|
||||
providers: [{
|
||||
provide: Directionality, useFactory: () => {
|
||||
@@ -311,6 +327,512 @@ describe('MdChipList', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('selection logic', () => {
|
||||
let formField: HTMLElement;
|
||||
let nativeChips: HTMLElement[];
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BasicChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
|
||||
nativeChips = fixture.debugElement.queryAll(By.css('md-chip'))
|
||||
.map((chip) => chip.nativeElement);
|
||||
|
||||
|
||||
chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
|
||||
chipListInstance = chipListDebugElement.componentInstance;
|
||||
chips = chipListInstance.chips;
|
||||
|
||||
});
|
||||
|
||||
it('should not float placeholder if no chip is selected', () => {
|
||||
expect(formField.classList.contains('mat-form-field-should-float'))
|
||||
.toBe(false, 'placeholder should not be floating');
|
||||
});
|
||||
|
||||
it('should remove selection if chip has been removed', async(() => {
|
||||
const instanceChips = fixture.componentInstance.chips;
|
||||
const chipList = fixture.componentInstance.chipList;
|
||||
const firstChip = nativeChips[0];
|
||||
dispatchKeyboardEvent(firstChip, 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(instanceChips.first.selected).toBe(true, 'Expected first option to be selected.');
|
||||
expect(chipList.selected).toBe(chips.first, 'Expected first option to be selected.');
|
||||
|
||||
fixture.componentInstance.foods = [];
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(chipList.selected)
|
||||
.toBe(undefined, 'Expected selection to be removed when option no longer exists.');
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should select an option that was added after initialization', () => {
|
||||
fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'});
|
||||
fixture.detectChanges();
|
||||
|
||||
nativeChips = fixture.debugElement.queryAll(By.css('md-chip'))
|
||||
.map((chip) => chip.nativeElement);
|
||||
const lastChip = nativeChips[8];
|
||||
dispatchKeyboardEvent(lastChip, 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.chipList.value)
|
||||
.toContain('potatoes-8', 'Expect value contain the value of the last option');
|
||||
expect(fixture.componentInstance.chips.last.selected)
|
||||
.toBeTruthy('Expect last option selected');
|
||||
});
|
||||
|
||||
it('should not select disabled chips', () => {
|
||||
const array = chips.toArray();
|
||||
const disabledChip = nativeChips[2];
|
||||
dispatchKeyboardEvent(disabledChip, 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.chipList.value)
|
||||
.toBeUndefined('Expect value to be undefined');
|
||||
expect(array[2].selected).toBeFalsy('Expect disabled chip not selected');
|
||||
expect(fixture.componentInstance.chipList.selected)
|
||||
.toBeUndefined('Expect no selected chips');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('forms integration', () => {
|
||||
let formField: HTMLElement;
|
||||
let nativeChips: HTMLElement[];
|
||||
|
||||
describe('single selection', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BasicChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
|
||||
nativeChips = fixture.debugElement.queryAll(By.css('md-chip'))
|
||||
.map((chip) => chip.nativeElement);
|
||||
chips = fixture.componentInstance.chips;
|
||||
});
|
||||
|
||||
it('should take an initial view value with reactive forms', () => {
|
||||
fixture.componentInstance.control = new FormControl('pizza-1');
|
||||
fixture.detectChanges();
|
||||
|
||||
const array = chips.toArray();
|
||||
|
||||
expect(array[1].selected).toBeTruthy('Expect pizza-1 chip to be selected');
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected).toBeFalsy('Expect chip to be not selected after toggle selected');
|
||||
});
|
||||
|
||||
it('should set the view value from the form', () => {
|
||||
const chipList = fixture.componentInstance.chipList;
|
||||
const array = chips.toArray();
|
||||
|
||||
expect(chipList.value).toBeFalsy('Expect chip list to have no initial value');
|
||||
|
||||
fixture.componentInstance.control.setValue('pizza-1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected).toBeTruthy('Expect chip to be selected');
|
||||
});
|
||||
|
||||
it('should update the form value when the view changes', () => {
|
||||
|
||||
expect(fixture.componentInstance.control.value)
|
||||
.toEqual(null, `Expected the control's value to be empty initially.`);
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.control.value)
|
||||
.toEqual('steak-0', `Expected control's value to be set to the new option.`);
|
||||
});
|
||||
|
||||
it('should clear the selection when a nonexistent option value is selected', () => {
|
||||
const array = chips.toArray();
|
||||
|
||||
fixture.componentInstance.control.setValue('pizza-1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeTruthy(`Expected chip with the value to be selected.`);
|
||||
|
||||
fixture.componentInstance.control.setValue('gibberish');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeFalsy(`Expected chip with the old value not to be selected.`);
|
||||
});
|
||||
|
||||
|
||||
it('should clear the selection when the control is reset', () => {
|
||||
const array = chips.toArray();
|
||||
|
||||
fixture.componentInstance.control.setValue('pizza-1');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.control.reset();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeFalsy(`Expected chip with the old value not to be selected.`);
|
||||
});
|
||||
|
||||
it('should set the control to touched when the chip list is touched', () => {
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(false, 'Expected the control to start off as untouched.');
|
||||
|
||||
const nativeChipList = fixture.debugElement.query(By.css('.mat-chip-list')).nativeElement;
|
||||
dispatchFakeEvent(nativeChipList, 'blur');
|
||||
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(true, 'Expected the control to be touched.');
|
||||
});
|
||||
|
||||
it('should not set touched when a disabled chip list is touched', () => {
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(false, 'Expected the control to start off as untouched.');
|
||||
|
||||
fixture.componentInstance.control.disable();
|
||||
const nativeChipList = fixture.debugElement.query(By.css('.mat-chip-list')).nativeElement;
|
||||
dispatchFakeEvent(nativeChipList, 'blur');
|
||||
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(false, 'Expected the control to stay untouched.');
|
||||
});
|
||||
|
||||
it('should set the control to dirty when the chip list\'s value changes in the DOM', () => {
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(false, `Expected control to start out pristine.`);
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(true, `Expected control to be dirty after value was changed by user.`);
|
||||
});
|
||||
|
||||
it('should not set the control to dirty when the value changes programmatically', () => {
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(false, `Expected control to start out pristine.`);
|
||||
|
||||
fixture.componentInstance.control.setValue('pizza-1');
|
||||
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(false, `Expected control to stay pristine after programmatic change.`);
|
||||
});
|
||||
|
||||
|
||||
it('should set an asterisk after the placeholder if the control is required', () => {
|
||||
let requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
|
||||
expect(requiredMarker)
|
||||
.toBeNull(`Expected placeholder not to have an asterisk, as control was not required.`);
|
||||
|
||||
fixture.componentInstance.isRequired = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
|
||||
expect(requiredMarker)
|
||||
.not.toBeNull(`Expected placeholder to have an asterisk, as control was required.`);
|
||||
});
|
||||
|
||||
it('should be able to programmatically select a falsy option', () => {
|
||||
fixture.destroy();
|
||||
|
||||
const falsyFixture = TestBed.createComponent(FalsyValueChipList);
|
||||
falsyFixture.detectChanges();
|
||||
|
||||
falsyFixture.componentInstance.control.setValue([0]);
|
||||
falsyFixture.detectChanges();
|
||||
falsyFixture.detectChanges();
|
||||
|
||||
expect(falsyFixture.componentInstance.chips.first.selected)
|
||||
.toBe(true, 'Expected first option to be selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple selection', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MultiSelectionChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
|
||||
nativeChips = fixture.debugElement.queryAll(By.css('md-chip'))
|
||||
.map((chip) => chip.nativeElement);
|
||||
chips = fixture.componentInstance.chips;
|
||||
});
|
||||
|
||||
it('should take an initial view value with reactive forms', () => {
|
||||
fixture.componentInstance.control = new FormControl(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
const array = chips.toArray();
|
||||
|
||||
expect(array[1].selected).toBeTruthy('Expect pizza-1 chip to be selected');
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected).toBeFalsy('Expect chip to be not selected after toggle selected');
|
||||
});
|
||||
|
||||
it('should set the view value from the form', () => {
|
||||
const chipList = fixture.componentInstance.chipList;
|
||||
const array = chips.toArray();
|
||||
|
||||
expect(chipList.value).toBeFalsy('Expect chip list to have no initial value');
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected).toBeTruthy('Expect chip to be selected');
|
||||
});
|
||||
|
||||
it('should update the form value when the view changes', () => {
|
||||
|
||||
expect(fixture.componentInstance.control.value)
|
||||
.toEqual(null, `Expected the control's value to be empty initially.`);
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.control.value)
|
||||
.toEqual(['steak-0'], `Expected control's value to be set to the new option.`);
|
||||
});
|
||||
|
||||
it('should clear the selection when a nonexistent option value is selected', () => {
|
||||
const array = chips.toArray();
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeTruthy(`Expected chip with the value to be selected.`);
|
||||
|
||||
fixture.componentInstance.control.setValue(['gibberish']);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeFalsy(`Expected chip with the old value not to be selected.`);
|
||||
});
|
||||
|
||||
|
||||
it('should clear the selection when the control is reset', () => {
|
||||
const array = chips.toArray();
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.control.reset();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeFalsy(`Expected chip with the old value not to be selected.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chip list with chip input', () => {
|
||||
let formField: HTMLElement;
|
||||
let nativeChips: HTMLElement[];
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
|
||||
nativeChips = fixture.debugElement.queryAll(By.css('md-chip'))
|
||||
.map((chip) => chip.nativeElement);
|
||||
});
|
||||
|
||||
it('should take an initial view value with reactive forms', () => {
|
||||
fixture.componentInstance.control = new FormControl(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
const array = fixture.componentInstance.chips.toArray();
|
||||
|
||||
expect(array[1].selected).toBeTruthy('Expect pizza-1 chip to be selected');
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected).toBeFalsy('Expect chip to be not selected after toggle selected');
|
||||
});
|
||||
|
||||
it('should set the view value from the form', () => {
|
||||
const array = fixture.componentInstance.chips.toArray();
|
||||
|
||||
expect(array[1].selected).toBeFalsy('Expect chip to not be selected');
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected).toBeTruthy('Expect chip to be selected');
|
||||
});
|
||||
|
||||
it('should update the form value when the view changes', () => {
|
||||
|
||||
expect(fixture.componentInstance.control.value)
|
||||
.toEqual(null, `Expected the control's value to be empty initially.`);
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.control.value)
|
||||
.toEqual(['steak-0'], `Expected control's value to be set to the new option.`);
|
||||
});
|
||||
|
||||
it('should clear the selection when a nonexistent option value is selected', () => {
|
||||
const array = fixture.componentInstance.chips.toArray();
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeTruthy(`Expected chip with the value to be selected.`);
|
||||
|
||||
fixture.componentInstance.control.setValue(['gibberish']);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeFalsy(`Expected chip with the old value not to be selected.`);
|
||||
});
|
||||
|
||||
|
||||
it('should clear the selection when the control is reset', () => {
|
||||
const array = fixture.componentInstance.chips.toArray();
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.control.reset();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(array[1].selected)
|
||||
.toBeFalsy(`Expected chip with the old value not to be selected.`);
|
||||
});
|
||||
|
||||
it('should set the control to touched when the chip list is touched', async(() => {
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(false, 'Expected the control to start off as untouched.');
|
||||
|
||||
const nativeChipList = fixture.debugElement.query(By.css('.mat-chip-list')).nativeElement;
|
||||
|
||||
dispatchFakeEvent(nativeChipList, 'blur');
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(true, 'Expected the control to be touched.');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not set touched when a disabled chip list is touched', () => {
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(false, 'Expected the control to start off as untouched.');
|
||||
|
||||
fixture.componentInstance.control.disable();
|
||||
const nativeChipList = fixture.debugElement.query(By.css('.mat-chip-list')).nativeElement;
|
||||
dispatchFakeEvent(nativeChipList, 'blur');
|
||||
|
||||
expect(fixture.componentInstance.control.touched)
|
||||
.toBe(false, 'Expected the control to stay untouched.');
|
||||
});
|
||||
|
||||
it('should set the control to dirty when the chip list\'s value changes in the DOM', () => {
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(false, `Expected control to start out pristine.`);
|
||||
|
||||
dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(true, `Expected control to be dirty after value was changed by user.`);
|
||||
});
|
||||
|
||||
it('should not set the control to dirty when the value changes programmatically', () => {
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(false, `Expected control to start out pristine.`);
|
||||
|
||||
fixture.componentInstance.control.setValue(['pizza-1']);
|
||||
|
||||
expect(fixture.componentInstance.control.dirty)
|
||||
.toEqual(false, `Expected control to stay pristine after programmatic change.`);
|
||||
});
|
||||
|
||||
|
||||
it('should set an asterisk after the placeholder if the control is required', () => {
|
||||
let requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
|
||||
expect(requiredMarker)
|
||||
.toBeNull(`Expected placeholder not to have an asterisk, as control was not required.`);
|
||||
|
||||
fixture.componentInstance.isRequired = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
|
||||
expect(requiredMarker)
|
||||
.not.toBeNull(`Expected placeholder to have an asterisk, as control was required.`);
|
||||
});
|
||||
|
||||
describe('keyboard behavior', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputChipList);
|
||||
fixture.detectChanges();
|
||||
chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
|
||||
chipListInstance = chipListDebugElement.componentInstance;
|
||||
chips = chipListInstance.chips;
|
||||
manager = fixture.componentInstance.chipList._keyManager;
|
||||
});
|
||||
|
||||
describe('when the input has focus', () => {
|
||||
|
||||
it('should focus the last chip when press DELETE', () => {
|
||||
let nativeInput = fixture.nativeElement.querySelector('input');
|
||||
let DELETE_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', DELETE, nativeInput);
|
||||
|
||||
// Focus the input
|
||||
nativeInput.focus();
|
||||
expect(manager.activeItemIndex).toBe(-1);
|
||||
|
||||
// Press the DELETE key
|
||||
chipListInstance._keydown(DELETE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the last chip
|
||||
expect(manager.activeItemIndex).toEqual(chips.length - 1);
|
||||
});
|
||||
|
||||
it('should focus the last chip when press BACKSPACE', () => {
|
||||
let nativeInput = fixture.nativeElement.querySelector('input');
|
||||
let BACKSPACE_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', BACKSPACE, nativeInput);
|
||||
|
||||
// Focus the input
|
||||
nativeInput.focus();
|
||||
expect(manager.activeItemIndex).toBe(-1);
|
||||
|
||||
// Press the BACKSPACE key
|
||||
chipListInstance._keydown(BACKSPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the last chip
|
||||
expect(manager.activeItemIndex).toEqual(chips.length - 1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupStandardList() {
|
||||
fixture = TestBed.createComponent(StandardChipList);
|
||||
fixture.detectChanges();
|
||||
@@ -370,3 +892,147 @@ class StandardChipList {
|
||||
})
|
||||
class FormFieldChipList {
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'basic-chip-list',
|
||||
template: `
|
||||
<md-form-field>
|
||||
<md-chip-list placeholder="Food" [formControl]="control" [required]="isRequired"
|
||||
[tabIndex]="tabIndexOverride" [selectable]="selectable">
|
||||
<md-chip *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
|
||||
{{ food.viewValue }}
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
</md-form-field>
|
||||
`
|
||||
})
|
||||
class BasicChipList {
|
||||
foods: any[] = [
|
||||
{ value: 'steak-0', viewValue: 'Steak' },
|
||||
{ value: 'pizza-1', viewValue: 'Pizza' },
|
||||
{ value: 'tacos-2', viewValue: 'Tacos', disabled: true },
|
||||
{ value: 'sandwich-3', viewValue: 'Sandwich' },
|
||||
{ value: 'chips-4', viewValue: 'Chips' },
|
||||
{ value: 'eggs-5', viewValue: 'Eggs' },
|
||||
{ value: 'pasta-6', viewValue: 'Pasta' },
|
||||
{ value: 'sushi-7', viewValue: 'Sushi' },
|
||||
];
|
||||
control = new FormControl();
|
||||
isRequired: boolean;
|
||||
tabIndexOverride: number;
|
||||
selectable: boolean;
|
||||
|
||||
@ViewChild(MdChipList) chipList: MdChipList;
|
||||
@ViewChildren(MdChip) chips: QueryList<MdChip>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'multi-selection-chip-list',
|
||||
template: `
|
||||
<md-form-field>
|
||||
<md-chip-list [multiple]="true" placeholder="Food" [formControl]="control"
|
||||
[required]="isRequired"
|
||||
[tabIndex]="tabIndexOverride" [selectable]="selectable">
|
||||
<md-chip *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
|
||||
{{ food.viewValue }}
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
</md-form-field>
|
||||
`
|
||||
})
|
||||
class MultiSelectionChipList {
|
||||
foods: any[] = [
|
||||
{ value: 'steak-0', viewValue: 'Steak' },
|
||||
{ value: 'pizza-1', viewValue: 'Pizza' },
|
||||
{ value: 'tacos-2', viewValue: 'Tacos', disabled: true },
|
||||
{ value: 'sandwich-3', viewValue: 'Sandwich' },
|
||||
{ value: 'chips-4', viewValue: 'Chips' },
|
||||
{ value: 'eggs-5', viewValue: 'Eggs' },
|
||||
{ value: 'pasta-6', viewValue: 'Pasta' },
|
||||
{ value: 'sushi-7', viewValue: 'Sushi' },
|
||||
];
|
||||
control = new FormControl();
|
||||
isRequired: boolean;
|
||||
tabIndexOverride: number;
|
||||
selectable: boolean;
|
||||
|
||||
@ViewChild(MdChipList) chipList: MdChipList;
|
||||
@ViewChildren(MdChip) chips: QueryList<MdChip>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'input-chip-list',
|
||||
template: `
|
||||
<md-form-field>
|
||||
<md-chip-list [multiple]="true"
|
||||
placeholder="Food" [formControl]="control" [required]="isRequired" #chipList1>
|
||||
<md-chip *ngFor="let food of foods" [value]="food.value">
|
||||
{{ food.viewValue }}
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
<input placeholder="New food..."
|
||||
[mdChipInputFor]="chipList1"
|
||||
[mdChipInputSeparatorKeyCodes]="separatorKeyCodes"
|
||||
[mdChipInputAddOnBlur]="addOnBlur"
|
||||
(mdChipInputTokenEnd)="add($event)" />/>
|
||||
</md-form-field>
|
||||
`
|
||||
})
|
||||
class InputChipList {
|
||||
foods: any[] = [
|
||||
{ value: 'steak-0', viewValue: 'Steak' },
|
||||
{ value: 'pizza-1', viewValue: 'Pizza' },
|
||||
{ value: 'tacos-2', viewValue: 'Tacos', disabled: true },
|
||||
{ value: 'sandwich-3', viewValue: 'Sandwich' },
|
||||
{ value: 'chips-4', viewValue: 'Chips' },
|
||||
{ value: 'eggs-5', viewValue: 'Eggs' },
|
||||
{ value: 'pasta-6', viewValue: 'Pasta' },
|
||||
{ value: 'sushi-7', viewValue: 'Sushi' },
|
||||
];
|
||||
control = new FormControl();
|
||||
|
||||
separatorKeyCodes = [ENTER, SPACE];
|
||||
addOnBlur: boolean = true;
|
||||
isRequired: boolean;
|
||||
|
||||
add(event: MdChipInputEvent): void {
|
||||
let input = event.input;
|
||||
let value = event.value;
|
||||
|
||||
// Add our foods
|
||||
if ((value || '').trim()) {
|
||||
this.foods.push({
|
||||
value: `${value.trim().toLowerCase()}-${this.foods.length}`,
|
||||
viewValue: value.trim()
|
||||
});
|
||||
}
|
||||
|
||||
// Reset the input value
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
@ViewChild(MdChipList) chipList: MdChipList;
|
||||
@ViewChildren(MdChip) chips: QueryList<MdChip>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-form-field>
|
||||
<md-chip-list [formControl]="control">
|
||||
<md-chip *ngFor="let food of foods" [value]="food.value">{{ food.viewValue }}</md-chip>
|
||||
</md-chip-list>
|
||||
</md-form-field>
|
||||
`
|
||||
})
|
||||
class FalsyValueChipList {
|
||||
foods: any[] = [
|
||||
{ value: 0, viewValue: 'Steak' },
|
||||
{ value: 1, viewValue: 'Pizza' },
|
||||
];
|
||||
control = new FormControl();
|
||||
@ViewChildren(MdChip) chips: QueryList<MdChip>;
|
||||
}
|
||||
|
||||
+514
-109
@@ -5,27 +5,55 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {FocusKeyManager} from '@angular/cdk/a11y';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {SelectionModel} from '@angular/cdk/collections';
|
||||
import {startWith} from '@angular/cdk/rxjs';
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectorRef,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChildren,
|
||||
Input,
|
||||
QueryList,
|
||||
ViewEncapsulation,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
Renderer2,
|
||||
Self,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
|
||||
import {
|
||||
BACKSPACE,
|
||||
DELETE,
|
||||
Directionality,
|
||||
LEFT_ARROW,
|
||||
RIGHT_ARROW,
|
||||
UP_ARROW
|
||||
} from '@angular/material/core';
|
||||
import {MdFormFieldControl} from '@angular/material/form-field';
|
||||
|
||||
import {MdChip} from './chip';
|
||||
import {FocusKeyManager} from '@angular/cdk/a11y';
|
||||
import {BACKSPACE, DELETE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW} from '@angular/material/core';
|
||||
import {Directionality} from '@angular/cdk/bidi';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
|
||||
import {MdChip, MdChipEvent, MdChipSelectionChange} from './chip';
|
||||
import {MdChipInput} from './chip-input';
|
||||
|
||||
// Increasing integer for generating unique ids for chip-list components.
|
||||
let nextUniqueId = 0;
|
||||
|
||||
/** Change event object that is emitted when the chip list value has changed. */
|
||||
export class MdChipListChange {
|
||||
constructor(public source: MdChipList, public value: any) { }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A material design chips component (named ChipList for it's similarity to the List component).
|
||||
@@ -37,21 +65,34 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
exportAs: 'mdChipList, matChipList',
|
||||
host: {
|
||||
'[attr.tabindex]': '_tabIndex',
|
||||
'[attr.aria-describedby]': '_ariaDescribedby || null',
|
||||
'[attr.aria-required]': 'required.toString()',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'[attr.aria-invalid]': 'errorState',
|
||||
'[attr.aria-multiselectable]': 'multiple',
|
||||
'[class.mat-chip-list-disabled]': 'disabled',
|
||||
'[class.mat-chip-list-invalid]': 'errorState',
|
||||
'[class.mat-chip-list-required]': 'required',
|
||||
'role': 'listbox',
|
||||
'[attr.aria-orientation]': 'ariaOrientation',
|
||||
'class': 'mat-chip-list',
|
||||
|
||||
'(focus)': 'focus()',
|
||||
'(blur)': '_blur()',
|
||||
'(keydown)': '_keydown($event)'
|
||||
},
|
||||
queries: {
|
||||
chips: new ContentChildren(MdChip)
|
||||
},
|
||||
providers: [{provide: MdFormFieldControl, useExisting: MdChipList}],
|
||||
styleUrls: ['chips.css'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
export class MdChipList implements MdFormFieldControl<any>, ControlValueAccessor,
|
||||
AfterContentInit, OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Stream that emits whenever the state of the input changes such that the wrapping `MdFormField`
|
||||
* needs to run change detection.
|
||||
*/
|
||||
stateChanges = new Subject<void>();
|
||||
|
||||
/** When a chip is destroyed, we track the index so we can focus the appropriate next chip. */
|
||||
protected _lastDestroyedIndex: number|null = null;
|
||||
@@ -62,10 +103,49 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
/** Subscription to tabbing out from the chip list. */
|
||||
private _tabOutSubscription = Subscription.EMPTY;
|
||||
|
||||
/** Subscription to changes in the chip list. */
|
||||
private _changeSubscription: Subscription;
|
||||
|
||||
/** Subscription to focus changes in the chips. */
|
||||
private _chipFocusSubscription: Subscription|null;
|
||||
|
||||
/** Subscription to blur changes in the chips. */
|
||||
private _chipBlurSubscription: Subscription|null;
|
||||
|
||||
/** Subscription to selection changes in chips. */
|
||||
private _chipSelectionSubscription: Subscription|null;
|
||||
|
||||
/** Subscription to remove changes in chips. */
|
||||
private _chipRemoveSubscription: Subscription|null;
|
||||
|
||||
/** Whether or not the chip is selectable. */
|
||||
protected _selectable: boolean = true;
|
||||
|
||||
protected _inputElement: HTMLInputElement;
|
||||
/** Whether the component is in multiple selection mode. */
|
||||
private _multiple: boolean = false;
|
||||
|
||||
/** The chip input to add more chips */
|
||||
protected _chipInput: MdChipInput;
|
||||
|
||||
/** The aria-describedby attribute on the chip list for improved a11y. */
|
||||
protected _ariaDescribedby: string;
|
||||
|
||||
/** Id of the chip list */
|
||||
protected _id: string;
|
||||
|
||||
/** Uid of the chip list */
|
||||
protected _uid: string = `md-chip-list-${nextUniqueId++}`;
|
||||
|
||||
/** Whether this is required */
|
||||
protected _required: boolean = false;
|
||||
|
||||
/** Whether this is disabled */
|
||||
protected _disabled: boolean = false;
|
||||
|
||||
protected _value: any;
|
||||
|
||||
/** Placeholder for the chip list. Alternatively, placeholder can be set on MdChipInput */
|
||||
protected _placeholder: string;
|
||||
|
||||
/** Tab index for the chip list. */
|
||||
_tabIndex = 0;
|
||||
@@ -79,17 +159,169 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
/** The FocusKeyManager which handles focus. */
|
||||
_keyManager: FocusKeyManager<MdChip>;
|
||||
|
||||
/** The chip components contained within this chip list. */
|
||||
chips: QueryList<MdChip>;
|
||||
/** Function when touched */
|
||||
_onTouched = () => {};
|
||||
|
||||
/** Function when changed */
|
||||
_onChange: (value: any) => void = () => {};
|
||||
|
||||
_selectionModel: SelectionModel<MdChip>;
|
||||
|
||||
/** Comparison function to specify which option is displayed. Defaults to object equality. */
|
||||
private _compareWith = (o1: any, o2: any) => o1 === o2;
|
||||
|
||||
/** The array of selected chips inside chip list. */
|
||||
get selected(): MdChip[] | MdChip {
|
||||
return this.multiple ? this._selectionModel.selected : this._selectionModel.selected[0];
|
||||
}
|
||||
|
||||
/** Whether the user should be allowed to select multiple chips. */
|
||||
@Input()
|
||||
get multiple(): boolean { return this._multiple; }
|
||||
set multiple(value: boolean) {
|
||||
this._multiple = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to compare the option values with the selected values. The first argument
|
||||
* is a value from an option. The second is a value from the selection. A boolean
|
||||
* should be returned.
|
||||
*/
|
||||
@Input()
|
||||
get compareWith() { return this._compareWith; }
|
||||
set compareWith(fn: (o1: any, o2: any) => boolean) {
|
||||
this._compareWith = fn;
|
||||
if (this._selectionModel) {
|
||||
// A different comparator means the selection could change.
|
||||
this._initializeSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/** Required for FormFieldControl */
|
||||
@Input()
|
||||
get value() { return this._value; }
|
||||
set value(newValue: any) {
|
||||
this.writeValue(newValue);
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
/** Required for FormFieldControl. The ID of the chip list */
|
||||
@Input()
|
||||
set id(value: string) {
|
||||
this._id = value;
|
||||
this.stateChanges.next();
|
||||
}
|
||||
get id() { return this._id || this._uid; }
|
||||
|
||||
/** Required for FormFieldControl. Whether the chip list is required. */
|
||||
@Input()
|
||||
set required(value: any) {
|
||||
this._required = coerceBooleanProperty(value);
|
||||
this.stateChanges.next();
|
||||
}
|
||||
get required() {
|
||||
return this._required;
|
||||
}
|
||||
|
||||
/** For FormFieldControl. Use chip input's placholder if there's a chip input */
|
||||
@Input()
|
||||
set placeholder(value: string) {
|
||||
this._placeholder = value;
|
||||
this.stateChanges.next();
|
||||
}
|
||||
get placeholder() {
|
||||
return this._chipInput ? this._chipInput.placeholder : this._placeholder;
|
||||
}
|
||||
|
||||
/** Whether any chips or the mdChipInput inside of this chip-list has focus. */
|
||||
get focused() {
|
||||
return this.chips.some(chip => chip._hasFocus) ||
|
||||
(this._chipInput && this._chipInput.focused);
|
||||
}
|
||||
|
||||
/** Whether this chip-list contains no chips and no mdChipInput. */
|
||||
get empty(): boolean {
|
||||
return (!this._chipInput || this._chipInput.empty) && this.chips.length === 0;
|
||||
}
|
||||
|
||||
/** Whether this chip-list is disabled. */
|
||||
@Input()
|
||||
get disabled() { return this.ngControl ? this.ngControl.disabled : this._disabled; }
|
||||
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
|
||||
|
||||
/** Whether the chip list is in an error state. */
|
||||
get errorState(): boolean {
|
||||
const isInvalid = this.ngControl && this.ngControl.invalid;
|
||||
const isTouched = this.ngControl && this.ngControl.touched;
|
||||
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
|
||||
(this._parentForm && this._parentForm.submitted);
|
||||
return !!(isInvalid && (isTouched || isSubmitted));
|
||||
}
|
||||
|
||||
/** Orientation of the chip list. */
|
||||
@Input('aria-orientation') ariaOrientation: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
constructor(protected _renderer: Renderer2, protected _elementRef: ElementRef,
|
||||
@Optional() private _dir: Directionality) {
|
||||
/**
|
||||
* Whether or not this chip is selectable. When a chip is not selectable,
|
||||
* its selected state is always ignored.
|
||||
*/
|
||||
@Input()
|
||||
get selectable(): boolean { return this._selectable; }
|
||||
set selectable(value: boolean) { this._selectable = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
set tabIndex(value: number) {
|
||||
this._userTabIndex = value;
|
||||
this._tabIndex = value;
|
||||
}
|
||||
|
||||
/** Combined stream of all of the child chips' selection change events. */
|
||||
get chipSelectionChanges(): Observable<MdChipSelectionChange> {
|
||||
return merge(...this.chips.map(chip => chip.onSelectionChange));
|
||||
}
|
||||
|
||||
/** Combined stream of all of the child chips' focus change events. */
|
||||
get chipFocusChanges(): Observable<MdChipEvent> {
|
||||
return merge(...this.chips.map(chip => chip._onFocus));
|
||||
}
|
||||
|
||||
/** Combined stream of all of the child chips' blur change events. */
|
||||
get chipBlurChanges(): Observable<MdChipEvent> {
|
||||
return merge(...this.chips.map(chip => chip._onBlur));
|
||||
}
|
||||
|
||||
/** Combined stream of all of the child chips' remove change events. */
|
||||
get chipRemoveChanges(): Observable<MdChipEvent> {
|
||||
return merge(...this.chips.map(chip => chip.destroy));
|
||||
}
|
||||
|
||||
/** Event emitted when the selected chip list value has been changed by the user. */
|
||||
@Output() change: EventEmitter<MdChipListChange> = new EventEmitter<MdChipListChange>();
|
||||
|
||||
/**
|
||||
* Event that emits whenever the raw value of the chip-list changes. This is here primarily
|
||||
* to facilitate the two-way binding for the `value` input.
|
||||
* @docs-private
|
||||
*/
|
||||
@Output() valueChange = new EventEmitter<any>();
|
||||
|
||||
/** The chip components contained within this chip list. */
|
||||
@ContentChildren(MdChip) chips: QueryList<MdChip>;
|
||||
|
||||
constructor(protected _renderer: Renderer2,
|
||||
protected _elementRef: ElementRef,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Optional() private _dir: Directionality,
|
||||
@Optional() private _parentForm: NgForm,
|
||||
@Optional() private _parentFormGroup: FormGroupDirective,
|
||||
@Optional() @Self() public ngControl: NgControl) {
|
||||
if (this.ngControl) {
|
||||
this.ngControl.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
|
||||
this._keyManager = new FocusKeyManager<MdChip>(this.chips).withWrap();
|
||||
|
||||
// Prevents the chip list from capturing focus and redirecting
|
||||
@@ -99,20 +331,12 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
setTimeout(() => this._tabIndex = this._userTabIndex || 0);
|
||||
});
|
||||
|
||||
// Go ahead and subscribe all of the initial chips
|
||||
this._subscribeChips(this.chips);
|
||||
|
||||
// Make sure we set our tab index at the start
|
||||
this._updateTabIndex();
|
||||
|
||||
// When the list changes, re-subscribe
|
||||
this.chips.changes.subscribe((chips: QueryList<MdChip>) => {
|
||||
this._subscribeChips(chips);
|
||||
this._changeSubscription = startWith.call(this.chips.changes, null).subscribe(() => {
|
||||
this._resetChips();
|
||||
|
||||
// If we have 0 chips, attempt to focus an input (if available)
|
||||
if (chips.length === 0) {
|
||||
this._focusInput();
|
||||
}
|
||||
// Reset chips selected/deselected status
|
||||
this._initializeSelection();
|
||||
|
||||
// Check to see if we need to update our tab index
|
||||
this._updateTabIndex();
|
||||
@@ -122,32 +346,51 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._selectionModel = new SelectionModel<MdChip>(this.multiple, undefined, false);
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._tabOutSubscription.unsubscribe();
|
||||
|
||||
if (this._changeSubscription) {
|
||||
this._changeSubscription.unsubscribe();
|
||||
}
|
||||
this._dropSubscriptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this chip is selectable. When a chip is not selectable,
|
||||
* it's selected state is always ignored.
|
||||
*/
|
||||
@Input()
|
||||
get selectable(): boolean {
|
||||
return this._selectable;
|
||||
}
|
||||
|
||||
set selectable(value: boolean) {
|
||||
this._selectable = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set tabIndex(value: number) {
|
||||
this._userTabIndex = value;
|
||||
this._tabIndex = value;
|
||||
}
|
||||
|
||||
/** Associates an HTML input element with this chip list. */
|
||||
registerInput(inputElement: HTMLInputElement) {
|
||||
this._inputElement = inputElement;
|
||||
registerInput(inputElement: MdChipInput) {
|
||||
this._chipInput = inputElement;
|
||||
}
|
||||
|
||||
// Implemented as part of MdFormFieldControl.
|
||||
setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); }
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
writeValue(value: any): void {
|
||||
if (this.chips) {
|
||||
this._setSelectionByValue(value, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
registerOnChange(fn: (value: any) => void): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
setDisabledState(disabled: boolean): void {
|
||||
this.disabled = disabled;
|
||||
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', disabled);
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,17 +399,22 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
*/
|
||||
focus() {
|
||||
// TODO: ARIA says this should focus the first `selected` chip if any are selected.
|
||||
if (this.chips.length > 0) {
|
||||
// Focus on first element if there's no chipInput inside chip-list
|
||||
if (this._chipInput && this._chipInput.focused) {
|
||||
// do nothing
|
||||
} else if (this.chips.length > 0) {
|
||||
this._keyManager.setFirstItemActive();
|
||||
this.stateChanges.next();
|
||||
} else {
|
||||
this._focusInput();
|
||||
this.stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempt to focus an input if we have one. */
|
||||
_focusInput() {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.focus();
|
||||
if (this._chipInput) {
|
||||
this._chipInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,17 +450,9 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
this._keyManager.onKeydown(event);
|
||||
}
|
||||
}
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the list of chips and add them to our list of
|
||||
* subscribed chips.
|
||||
*
|
||||
* @param chips The list of chips to be subscribed.
|
||||
*/
|
||||
protected _subscribeChips(chips: QueryList<MdChip>): void {
|
||||
chips.forEach(chip => this._addChip(chip));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the tab index as you should not be allowed to focus an empty list.
|
||||
@@ -223,51 +463,26 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a specific chip to our subscribed list. If the chip has
|
||||
* already been subscribed, this ensures it is only subscribed
|
||||
* once.
|
||||
*
|
||||
* @param chip The chip to be subscribed (or checked for existing
|
||||
* subscription).
|
||||
* Update key manager's active item when chip is deleted.
|
||||
* If the deleted chip is the last chip in chip list, focus the new last chip.
|
||||
* Otherwise focus the next chip in the list.
|
||||
* Save `_lastDestroyedIndex` so we can set the correct focus.
|
||||
*/
|
||||
protected _addChip(chip: MdChip) {
|
||||
// If we've already been subscribed to a parent, do nothing
|
||||
if (this._chipSet.has(chip)) {
|
||||
return;
|
||||
protected _updateKeyManager(chip: MdChip) {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
if (chip._hasFocus) {
|
||||
// Check whether the chip is not the last item
|
||||
if (chipIndex < this.chips.length - 1) {
|
||||
this._keyManager.setActiveItem(chipIndex);
|
||||
} else if (chipIndex - 1 >= 0) {
|
||||
this._keyManager.setActiveItem(chipIndex - 1);
|
||||
}
|
||||
}
|
||||
if (this._keyManager.activeItemIndex === chipIndex) {
|
||||
this._lastDestroyedIndex = chipIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for focus events outside of the keyboard navigation
|
||||
chip._onFocus.subscribe(() => {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
this._keyManager.updateActiveItemIndex(chipIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// On destroy, remove the item from our list, and setup our destroyed focus check
|
||||
chip.destroy.subscribe(() => {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
if (chip._hasFocus) {
|
||||
// Check whether the chip is the last item
|
||||
if (chipIndex < this.chips.length - 1) {
|
||||
this._keyManager.setActiveItem(chipIndex);
|
||||
} else if (chipIndex - 1 >= 0) {
|
||||
this._keyManager.setActiveItem(chipIndex - 1);
|
||||
}
|
||||
}
|
||||
if (this._keyManager.activeItemIndex === chipIndex) {
|
||||
this._lastDestroyedIndex = chipIndex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this._chipSet.delete(chip);
|
||||
chip.destroy.unsubscribe();
|
||||
});
|
||||
|
||||
this._chipSet.set(chip, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,11 +497,12 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
const newFocusIndex = Math.min(this._lastDestroyedIndex, chipsArray.length - 1);
|
||||
this._keyManager.setActiveItem(newFocusIndex);
|
||||
let focusChip = this._keyManager.activeItem;
|
||||
|
||||
// Focus the chip
|
||||
if (focusChip) {
|
||||
focusChip.focus();
|
||||
}
|
||||
} else if (chipsArray.length === 0) {
|
||||
this._focusInput();
|
||||
}
|
||||
|
||||
// Reset our destroyed index
|
||||
@@ -312,4 +528,193 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_setSelectionByValue(value: any, isUserInput: boolean = true) {
|
||||
this._clearSelection();
|
||||
this.chips.forEach(chip => chip.deselect());
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(currentValue => this._selectValue(currentValue, isUserInput));
|
||||
this._sortValues();
|
||||
} else {
|
||||
const correspondingChip = this._selectValue(value, isUserInput);
|
||||
|
||||
// Shift focus to the active item. Note that we shouldn't do this in multiple
|
||||
// mode, because we don't know what chip the user interacted with last.
|
||||
if (correspondingChip) {
|
||||
this._keyManager.setActiveItem(this.chips.toArray().indexOf(correspondingChip));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and selects the chip based on its value.
|
||||
* @returns Chip that has the corresponding value.
|
||||
*/
|
||||
private _selectValue(value: any, isUserInput: boolean = true): MdChip | undefined {
|
||||
|
||||
const correspondingChip = this.chips.find(chip => {
|
||||
return chip.value != null && this._compareWith(chip.value, value);
|
||||
});
|
||||
|
||||
if (correspondingChip) {
|
||||
isUserInput ? correspondingChip.selectViaInteraction() : correspondingChip.select();
|
||||
this._selectionModel.select(correspondingChip);
|
||||
}
|
||||
|
||||
return correspondingChip;
|
||||
}
|
||||
|
||||
private _initializeSelection(): void {
|
||||
// Defer setting the value in order to avoid the "Expression
|
||||
// has changed after it was checked" errors from Angular.
|
||||
Promise.resolve().then(() => {
|
||||
this._setSelectionByValue(this.ngControl ? this.ngControl.value : this._value, false);
|
||||
this.stateChanges.next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselects every chip in the list.
|
||||
* @param skip Chip that should not be deselected.
|
||||
*/
|
||||
private _clearSelection(skip?: MdChip): void {
|
||||
this._selectionModel.clear();
|
||||
this.chips.forEach(chip => {
|
||||
if (chip !== skip) {
|
||||
chip.deselect();
|
||||
}
|
||||
});
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the model values, ensuring that they keep the same
|
||||
* order that they have in the panel.
|
||||
*/
|
||||
private _sortValues(): void {
|
||||
if (this._multiple) {
|
||||
this._selectionModel.clear();
|
||||
|
||||
this.chips.forEach(chip => {
|
||||
if (chip.selected) {
|
||||
this._selectionModel.select(chip);
|
||||
}
|
||||
});
|
||||
this.stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits change event to set the model value. */
|
||||
private _propagateChanges(fallbackValue?: any): void {
|
||||
let valueToEmit: any = null;
|
||||
|
||||
if (Array.isArray(this.selected)) {
|
||||
valueToEmit = this.selected.map(chip => chip.value);
|
||||
} else {
|
||||
valueToEmit = this.selected ? this.selected.value : fallbackValue;
|
||||
}
|
||||
this._value = valueToEmit;
|
||||
this.change.emit(new MdChipListChange(this, valueToEmit));
|
||||
this.valueChange.emit(valueToEmit);
|
||||
this._onChange(valueToEmit);
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/** When blurred, mark the field as touched when focus moved outside the chip list. */
|
||||
_blur() {
|
||||
if (!this.disabled) {
|
||||
if (this._chipInput) {
|
||||
// If there's a chip input, we should check whether the focus moved to chip input.
|
||||
// If the focus is not moved to chip input, mark the field as touched. If the focus moved
|
||||
// to chip input, do nothing.
|
||||
// Timeout is needed to wait for the focus() event trigger on chip input.
|
||||
setTimeout(() => {
|
||||
if (!this.focused) {
|
||||
this._markAsTouched();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If there's no chip input, then mark the field as touched.
|
||||
this._markAsTouched();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Mark the field as touched */
|
||||
_markAsTouched() {
|
||||
this._onTouched();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
private _resetChips() {
|
||||
this._dropSubscriptions();
|
||||
this._listenToChipsFocus();
|
||||
this._listenToChipsSelection();
|
||||
this._listenToChipsRemoved();
|
||||
}
|
||||
|
||||
|
||||
private _dropSubscriptions() {
|
||||
if (this._chipFocusSubscription) {
|
||||
this._chipFocusSubscription.unsubscribe();
|
||||
this._chipFocusSubscription = null;
|
||||
}
|
||||
|
||||
if (this._chipBlurSubscription) {
|
||||
this._chipBlurSubscription.unsubscribe();
|
||||
this._chipBlurSubscription = null;
|
||||
}
|
||||
|
||||
if (this._chipSelectionSubscription) {
|
||||
this._chipSelectionSubscription.unsubscribe();
|
||||
this._chipSelectionSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Listens to user-generated selection events on each chip. */
|
||||
private _listenToChipsSelection(): void {
|
||||
this._chipSelectionSubscription = this.chipSelectionChanges.subscribe(event => {
|
||||
event.source.selected
|
||||
? this._selectionModel.select(event.source)
|
||||
: this._selectionModel.deselect(event.source);
|
||||
|
||||
// For single selection chip list, make sure the deselected value is unselected.
|
||||
if (!this.multiple) {
|
||||
this.chips.forEach(chip => {
|
||||
if (!this._selectionModel.isSelected(chip) && chip.selected) {
|
||||
chip.deselect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.isUserInput) {
|
||||
this._propagateChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Listens to user-generated selection events on each chip. */
|
||||
private _listenToChipsFocus(): void {
|
||||
this._chipFocusSubscription = this.chipFocusChanges.subscribe(event => {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(event.chip);
|
||||
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
this._keyManager.updateActiveItemIndex(chipIndex);
|
||||
}
|
||||
this.stateChanges.next();
|
||||
});
|
||||
|
||||
this._chipBlurSubscription = this.chipBlurChanges.subscribe(_ => {
|
||||
this._blur();
|
||||
this.stateChanges.next();
|
||||
});
|
||||
}
|
||||
|
||||
private _listenToChipsRemoved(): void {
|
||||
this._chipRemoveSubscription = this.chipRemoveChanges.subscribe((event) => {
|
||||
this._updateKeyManager(event.chip);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+14
-15
@@ -2,7 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {createKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {MdChipList, MdChip, MdChipEvent, MdChipsModule} from './index';
|
||||
import {MdChipList, MdChip, MdChipEvent, MdChipsModule, MdChipSelectionChange} from './index';
|
||||
import {SPACE, DELETE, BACKSPACE} from '@angular/material/core';
|
||||
import {Directionality} from '@angular/material/core';
|
||||
|
||||
@@ -113,14 +113,15 @@ describe('Chips', () => {
|
||||
});
|
||||
|
||||
it('allows selection', () => {
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
spyOn(testComponent, 'chipSelectionChange');
|
||||
expect(chipNativeElement.classList).not.toContain('mat-chip-selected');
|
||||
|
||||
testComponent.selected = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipNativeElement.classList).toContain('mat-chip-selected');
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledWith({chip: chipInstance});
|
||||
expect(testComponent.chipSelectionChange)
|
||||
.toHaveBeenCalledWith({source: chipInstance, isUserInput: false});
|
||||
});
|
||||
|
||||
it('allows removal', () => {
|
||||
@@ -143,26 +144,25 @@ describe('Chips', () => {
|
||||
|
||||
it('should selects/deselects the currently focused chip on SPACE', () => {
|
||||
const SPACE_EVENT: KeyboardEvent = createKeyboardEvent('keydown', SPACE) as KeyboardEvent;
|
||||
const CHIP_EVENT: MdChipEvent = {chip: chipInstance};
|
||||
const CHIP_EVENT: MdChipSelectionChange = {source: chipInstance, isUserInput: true};
|
||||
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
spyOn(testComponent, 'chipDeselect');
|
||||
spyOn(testComponent, 'chipSelectionChange');
|
||||
|
||||
// Use the spacebar to select the chip
|
||||
chipInstance._handleKeydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipInstance.selected).toBeTruthy();
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledTimes(1);
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledWith(CHIP_EVENT);
|
||||
expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(1);
|
||||
expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_EVENT);
|
||||
|
||||
// Use the spacebar to deselect the chip
|
||||
chipInstance._handleKeydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipInstance.selected).toBeFalsy();
|
||||
expect(testComponent.chipDeselect).toHaveBeenCalledTimes(1);
|
||||
expect(testComponent.chipDeselect).toHaveBeenCalledWith(CHIP_EVENT);
|
||||
expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(2);
|
||||
expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_EVENT);
|
||||
});
|
||||
|
||||
it('should have correct aria-selected', () => {
|
||||
@@ -184,14 +184,14 @@ describe('Chips', () => {
|
||||
it('SPACE ignores selection', () => {
|
||||
const SPACE_EVENT: KeyboardEvent = createKeyboardEvent('keydown', SPACE) as KeyboardEvent;
|
||||
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
spyOn(testComponent, 'chipSelectionChange');
|
||||
|
||||
// Use the spacebar to attempt to select the chip
|
||||
chipInstance._handleKeydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipInstance.selected).toBeFalsy();
|
||||
expect(testComponent.chipSelect).not.toHaveBeenCalled();
|
||||
expect(testComponent.chipSelectionChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not have the aria-selected attribute', () => {
|
||||
@@ -281,7 +281,7 @@ describe('Chips', () => {
|
||||
<md-chip [selectable]="selectable" [removable]="removable"
|
||||
[color]="color" [selected]="selected" [disabled]="disabled"
|
||||
(focus)="chipFocus($event)" (destroy)="chipDestroy($event)"
|
||||
(select)="chipSelect($event)" (deselect)="chipDeselect($event)"
|
||||
(onSelectionChange)="chipSelectionChange($event)"
|
||||
(remove)="chipRemove($event)">
|
||||
{{name}}
|
||||
</md-chip>
|
||||
@@ -299,8 +299,7 @@ class SingleChip {
|
||||
|
||||
chipFocus: (event?: MdChipEvent) => void = () => {};
|
||||
chipDestroy: (event?: MdChipEvent) => void = () => {};
|
||||
chipSelect: (event?: MdChipEvent) => void = () => {};
|
||||
chipDeselect: (event?: MdChipEvent) => void = () => {};
|
||||
chipSelectionChange: (event?: MdChipSelectionChange) => void = () => {};
|
||||
chipRemove: (event?: MdChipEvent) => void = () => {};
|
||||
}
|
||||
|
||||
|
||||
+79
-44
@@ -9,11 +9,9 @@
|
||||
import {FocusableOption} from '@angular/cdk/a11y';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {
|
||||
ContentChild,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
@@ -34,6 +32,12 @@ export interface MdChipEvent {
|
||||
chip: MdChip;
|
||||
}
|
||||
|
||||
/** Event object emitted by MdChip when selected or deselected. */
|
||||
export class MdChipSelectionChange {
|
||||
constructor(public source: MdChip, public isUserInput = false) { }
|
||||
}
|
||||
|
||||
|
||||
// Boilerplate for applying mixins to MdChip.
|
||||
/** @docs-private */
|
||||
export class MdChipBase {
|
||||
@@ -71,80 +75,106 @@ export class MdBasicChip { }
|
||||
'(click)': '_handleClick($event)',
|
||||
'(keydown)': '_handleKeydown($event)',
|
||||
'(focus)': '_hasFocus = true',
|
||||
'(blur)': '_hasFocus = false',
|
||||
}
|
||||
'(blur)': '_blur()',
|
||||
},
|
||||
|
||||
})
|
||||
export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestroy, CanColor,
|
||||
CanDisable {
|
||||
|
||||
@ContentChild(forwardRef(() => MdChipRemove)) _chipRemove: MdChipRemove;
|
||||
protected _value: any;
|
||||
|
||||
/** Whether the chip is selected. */
|
||||
@Input() get selected(): boolean { return this._selected; }
|
||||
set selected(value: boolean) {
|
||||
this._selected = coerceBooleanProperty(value);
|
||||
(this.selected ? this.select : this.deselect).emit({chip: this});
|
||||
}
|
||||
protected _selected: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether or not the chips are selectable. When a chip is not selectable,
|
||||
* changes to it's selected state are always ignored.
|
||||
*/
|
||||
@Input() get selectable(): boolean {
|
||||
return this._selectable;
|
||||
}
|
||||
|
||||
set selectable(value: boolean) {
|
||||
this._selectable = coerceBooleanProperty(value);
|
||||
}
|
||||
protected _selectable: boolean = true;
|
||||
|
||||
/**
|
||||
* Determines whether or not the chip displays the remove styling and emits (remove) events.
|
||||
*/
|
||||
@Input() get removable(): boolean {
|
||||
return this._removable;
|
||||
}
|
||||
|
||||
set removable(value: boolean) {
|
||||
this._removable = coerceBooleanProperty(value);
|
||||
}
|
||||
protected _removable: boolean = true;
|
||||
|
||||
/** Whether the chip has focus. */
|
||||
_hasFocus: boolean = false;
|
||||
|
||||
/** Whether the chip is selected. */
|
||||
@Input()
|
||||
get selected(): boolean { return this._selected; }
|
||||
set selected(value: boolean) {
|
||||
this._selected = coerceBooleanProperty(value);
|
||||
this.onSelectionChange.emit({source: this, isUserInput: false});
|
||||
}
|
||||
|
||||
/** The value of the chip. Defaults to the content inside <md-chip> tags. */
|
||||
@Input()
|
||||
get value(): any {
|
||||
return this._value != undefined
|
||||
? this._value
|
||||
: this._elementRef.nativeElement.textContent;
|
||||
}
|
||||
set value(newValue: any) { this._value = newValue; }
|
||||
|
||||
/**
|
||||
* Whether or not the chips are selectable. When a chip is not selectable,
|
||||
* changes to it's selected state are always ignored.
|
||||
*/
|
||||
@Input()
|
||||
get selectable(): boolean { return this._selectable; }
|
||||
set selectable(value: boolean) { this._selectable = coerceBooleanProperty(value); }
|
||||
|
||||
/**
|
||||
* Determines whether or not the chip displays the remove styling and emits (remove) events.
|
||||
*/
|
||||
@Input()
|
||||
get removable(): boolean { return this._removable; }
|
||||
set removable(value: boolean) { this._removable = coerceBooleanProperty(value); }
|
||||
|
||||
/** Emits when the chip is focused. */
|
||||
_onFocus = new Subject<MdChipEvent>();
|
||||
|
||||
/** Emitted when the chip is selected. */
|
||||
@Output() select = new EventEmitter<MdChipEvent>();
|
||||
/** Emits when the chip is blured. */
|
||||
_onBlur = new Subject<MdChipEvent>();
|
||||
|
||||
/** Emitted when the chip is deselected. */
|
||||
@Output() deselect = new EventEmitter<MdChipEvent>();
|
||||
/** Emitted when the chip is selected or deselected. */
|
||||
@Output() onSelectionChange = new EventEmitter<MdChipSelectionChange>();
|
||||
|
||||
/** Emitted when the chip is destroyed. */
|
||||
@Output() destroy = new EventEmitter<MdChipEvent>();
|
||||
|
||||
/** Emitted when a chip is to be removed. */
|
||||
@Output('remove') onRemove = new EventEmitter<MdChipEvent>();
|
||||
|
||||
get ariaSelected(): string | null {
|
||||
return this.selectable ? this.selected.toString() : null;
|
||||
}
|
||||
|
||||
constructor(renderer: Renderer2, elementRef: ElementRef) {
|
||||
super(renderer, elementRef);
|
||||
constructor(renderer: Renderer2, public _elementRef: ElementRef) {
|
||||
super(renderer, _elementRef);
|
||||
}
|
||||
|
||||
/** Emitted when a chip is to be removed. */
|
||||
@Output('remove') onRemove = new EventEmitter<MdChipEvent>();
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy.emit({chip: this});
|
||||
}
|
||||
|
||||
/** Selects the chip. */
|
||||
select(): void {
|
||||
this._selected = true;
|
||||
this.onSelectionChange.emit({source: this, isUserInput: false});
|
||||
}
|
||||
|
||||
/** Deselects the chip. */
|
||||
deselect(): void {
|
||||
this._selected = false;
|
||||
this.onSelectionChange.emit({source: this, isUserInput: false});
|
||||
}
|
||||
|
||||
/** Select this chip and emit selected event */
|
||||
selectViaInteraction() {
|
||||
this._selected = true;
|
||||
// Emit select event when selected changes.
|
||||
this.onSelectionChange.emit({source: this, isUserInput: true});
|
||||
}
|
||||
|
||||
/** Toggles the current selected state of this chip. */
|
||||
toggleSelected(): boolean {
|
||||
this.selected = !this.selected;
|
||||
toggleSelected(isUserInput: boolean = false): boolean {
|
||||
this._selected = !this.selected;
|
||||
this.onSelectionChange.emit({source: this, isUserInput});
|
||||
return this.selected;
|
||||
}
|
||||
|
||||
@@ -196,7 +226,7 @@ export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestr
|
||||
case SPACE:
|
||||
// If we are selectable, toggle the focused chip
|
||||
if (this.selectable) {
|
||||
this.toggleSelected();
|
||||
this.toggleSelected(true);
|
||||
}
|
||||
|
||||
// Always prevent space from scrolling the page since the list has focus
|
||||
@@ -204,6 +234,11 @@ export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestr
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_blur() {
|
||||
this._hasFocus = false;
|
||||
this._onBlur.next({chip: this});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ An exmaple of chip input placed inside the chip-list element.
|
||||
<!-- example(chips-input) -->
|
||||
|
||||
An example of chip input placed outside the chip-list element.
|
||||
|
||||
```html
|
||||
<md-form-field>
|
||||
<md-chip-list #chipList>
|
||||
|
||||
@@ -7,12 +7,16 @@ $mat-chip-remove-margin-before: 6px;
|
||||
$mat-chip-remove-margin-after: -4px;
|
||||
|
||||
$mat-chips-chip-margin: 8px;
|
||||
$mat-chips-chip-bottom-margin: 3px;
|
||||
|
||||
$mat-chip-input-width: 150px;
|
||||
$mat-chip-input-margin: 3px;
|
||||
|
||||
.mat-chip-list-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.mat-chip:not(.mat-basic-chip) {
|
||||
@@ -25,10 +29,10 @@ $mat-chips-chip-margin: 8px;
|
||||
|
||||
// Apply a margin to adjacent sibling chips.
|
||||
& + & {
|
||||
margin: 0 0 0 $mat-chips-chip-margin;
|
||||
margin: 0 0 $mat-chips-chip-bottom-margin $mat-chips-chip-margin;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin: 0 $mat-chips-chip-margin 0 0;
|
||||
margin: 0 $mat-chips-chip-margin $mat-chips-chip-bottom-margin 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,3 +94,8 @@ $mat-chips-chip-margin: 8px;
|
||||
margin-left: $mat-chip-remove-margin-after;
|
||||
}
|
||||
}
|
||||
|
||||
input.mat-chip-input {
|
||||
width: $mat-chip-input-width;
|
||||
margin: $mat-chip-input-margin;
|
||||
}
|
||||
|
||||
+19
-19
@@ -69,13 +69,13 @@ let nextUniqueId = 0;
|
||||
})
|
||||
export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, DoCheck {
|
||||
/** Variables used as cache for getters and setters. */
|
||||
private _type = 'text';
|
||||
private _disabled = false;
|
||||
private _required = false;
|
||||
private _id: string;
|
||||
private _uid = `md-input-${nextUniqueId++}`;
|
||||
private _errorOptions: ErrorOptions;
|
||||
private _previousNativeValue = this.value;
|
||||
protected _type = 'text';
|
||||
protected _disabled = false;
|
||||
protected _required = false;
|
||||
protected _id: string;
|
||||
protected _uid = `md-input-${nextUniqueId++}`;
|
||||
protected _errorOptions: ErrorOptions;
|
||||
protected _previousNativeValue = this.value;
|
||||
|
||||
/** Whether the input is focused. */
|
||||
focused = false;
|
||||
@@ -137,7 +137,7 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
|
||||
}
|
||||
}
|
||||
|
||||
private _neverEmptyInputTypes = [
|
||||
protected _neverEmptyInputTypes = [
|
||||
'date',
|
||||
'datetime',
|
||||
'datetime-local',
|
||||
@@ -146,12 +146,12 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
|
||||
'week'
|
||||
].filter(t => getSupportedInputTypes().has(t));
|
||||
|
||||
constructor(private _elementRef: ElementRef,
|
||||
private _renderer: Renderer2,
|
||||
private _platform: Platform,
|
||||
constructor(protected _elementRef: ElementRef,
|
||||
protected _renderer: Renderer2,
|
||||
protected _platform: Platform,
|
||||
@Optional() @Self() public ngControl: NgControl,
|
||||
@Optional() private _parentForm: NgForm,
|
||||
@Optional() private _parentFormGroup: FormGroupDirective,
|
||||
@Optional() protected _parentForm: NgForm,
|
||||
@Optional() protected _parentFormGroup: FormGroupDirective,
|
||||
@Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
|
||||
|
||||
// Force setter to be called in case id was not specified.
|
||||
@@ -216,7 +216,7 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
|
||||
}
|
||||
|
||||
/** Re-evaluates the error state. This is only relevant with @angular/forms. */
|
||||
private _updateErrorState() {
|
||||
protected _updateErrorState() {
|
||||
const oldState = this.errorState;
|
||||
const ngControl = this.ngControl;
|
||||
const parent = this._parentFormGroup || this._parentForm;
|
||||
@@ -229,7 +229,7 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
|
||||
}
|
||||
|
||||
/** Does some manual dirty checking on the native input `value` property. */
|
||||
private _dirtyCheckNativeValue() {
|
||||
protected _dirtyCheckNativeValue() {
|
||||
const newValue = this.value;
|
||||
|
||||
if (this._previousNativeValue !== newValue) {
|
||||
@@ -239,26 +239,26 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
|
||||
}
|
||||
|
||||
/** Make sure the input is a supported type. */
|
||||
private _validateType() {
|
||||
protected _validateType() {
|
||||
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) > -1) {
|
||||
throw getMdInputUnsupportedTypeError(this._type);
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks whether the input type is one of the types that are never empty. */
|
||||
private _isNeverEmpty() {
|
||||
protected _isNeverEmpty() {
|
||||
return this._neverEmptyInputTypes.indexOf(this._type) > -1;
|
||||
}
|
||||
|
||||
/** Checks whether the input is invalid based on the native validation. */
|
||||
private _isBadInput() {
|
||||
protected _isBadInput() {
|
||||
// The `validity` property won't be present on platform-server.
|
||||
let validity = (this._elementRef.nativeElement as HTMLInputElement).validity;
|
||||
return validity && validity.badInput;
|
||||
}
|
||||
|
||||
/** Determines if the component host is a textarea. If not recognizable it returns false. */
|
||||
private _isTextarea() {
|
||||
protected _isTextarea() {
|
||||
let nativeElement = this._elementRef.nativeElement;
|
||||
|
||||
// In Universal, we don't have access to `nodeName`, but the same can be achieved with `name`.
|
||||
|
||||
Reference in New Issue
Block a user