From d262ff7df6d03e3e8be0cddcbb10bff2753ebea3 Mon Sep 17 00:00:00 2001
From: Yuan Gao
Date: Mon, 21 Aug 2017 13:50:08 -0700
Subject: [PATCH 1/6] feat(chip-list): Implement FormFieldControl
BREAKING CHANGES:
- Deprecated: @Output() select, and @Output() deselect
- Added @Output() onSelectionChange = EventEmitter
---
src/demo-app/chips/chips-demo.html | 63 +++-
src/demo-app/chips/chips-demo.scss | 4 +
src/demo-app/chips/chips-demo.ts | 18 +
src/lib/chips/chip-input.spec.ts | 3 +-
src/lib/chips/chip-input.ts | 53 ++-
src/lib/chips/chip-list.spec.ts | 561 ++++++++++++++++++++++++++++-
src/lib/chips/chip-list.ts | 541 +++++++++++++++++++++++-----
src/lib/chips/chip.spec.ts | 29 +-
src/lib/chips/chip.ts | 103 ++++--
src/lib/chips/chips.md | 29 +-
src/lib/chips/chips.scss | 15 +-
src/lib/input/input.ts | 38 +-
12 files changed, 1255 insertions(+), 202 deletions(-)
diff --git a/src/demo-app/chips/chips-demo.html b/src/demo-app/chips/chips-demo.html
index b55c55954aea..3ada94d8eee0 100644
--- a/src/demo-app/chips/chips-demo.html
+++ b/src/demo-app/chips/chips-demo.html
@@ -45,20 +45,22 @@ Form Field
<md-form-field>.
-
-
-
-
+ Set tabIndex to {{tabIndex === 0 ? -1 : 0}}
+
+
+
+
{{person.name}}
cancel
+
-
@@ -68,21 +70,37 @@ Form Field
With mdChipInput the input work with chip-list
-
-
- {{person.name}}
- cancel
-
-
-
+
+ {{person.name}}
+ cancel
+
+
+
+
+ Chips list without input
+
+
+
+
+
+
+ {{person.name}}
+ cancel
+
+
+
+
+
The example above has overridden the [separatorKeys] input to allow for
ENTER, COMMA and SEMI COLON keys.
@@ -108,6 +126,17 @@
Stacked Chips
{{aColor.name}}
+
+ NgModel with chip list
+
+
+ {{aColor.name}}
+ cancel
+
+
+
+ The selected colors are {{color}}.
diff --git a/src/demo-app/chips/chips-demo.scss b/src/demo-app/chips/chips-demo.scss
index 5fdb999211a1..5928eef7375b 100644
--- a/src/demo-app/chips/chips-demo.scss
+++ b/src/demo-app/chips/chips-demo.scss
@@ -25,3 +25,7 @@
width: 150px;
}
}
+
+.has-chip-list {
+ width: 100%;
+}
diff --git a/src/demo-app/chips/chips-demo.ts b/src/demo-app/chips/chips-demo.ts
index ce46def82be2..8f63a4f3efd9 100644
--- a/src/demo-app/chips/chips-demo.ts
+++ b/src/demo-app/chips/chips-demo.ts
@@ -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,22 @@ 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'];
}
diff --git a/src/lib/chips/chip-input.spec.ts b/src/lib/chips/chip-input.spec.ts
index 827edb24ffd1..b50edcd4dbb9 100644
--- a/src/lib/chips/chip-input.spec.ts
+++ b/src/lib/chips/chip-input.spec.ts
@@ -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: () => {
diff --git a/src/lib/chips/chip-input.ts b/src/lib/chips/chip-input.ts
index ed7ac8470e99..7a8902329ef4 100644
--- a/src/lib/chips/chip-input.ts
+++ b/src/lib/chips/chip-input.ts
@@ -6,7 +6,18 @@
* 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,
+ Optional,
+ Renderer2,
+ Self,
+} from '@angular/core';
+import {FormGroupDirective, NgControl, NgForm} from '@angular/forms';
+import {Platform} from '@angular/cdk/platform';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ENTER} from '@angular/material/core';
import {MdChipList} from './chip-list';
@@ -16,16 +27,21 @@ export interface MdChipInputEvent {
value: string;
}
+/**
+ * Directive that adds chip-specific behaviors to an input element inside .
+ * May be placed inside or outside of an .
+ */
@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 +49,7 @@ export class MdChipInput {
set chipList(value: MdChipList) {
if (value) {
this._chipList = value;
- this._chipList.registerInput(this._inputElement);
+ this._chipList.registerInput(this);
}
}
@@ -68,10 +84,22 @@ 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._elementRef.nativeElement.value;
+ return value == null || value === '';
+ }
+
/** The native input element to which this directive is attached. */
protected _inputElement: HTMLInputElement;
- constructor(protected _elementRef: ElementRef) {
+ constructor(protected _elementRef: ElementRef,
+ protected _renderer: Renderer2,
+ protected _platform: Platform,
+ @Optional() @Self() public ngControl: NgControl,
+ @Optional() protected _parentForm: NgForm,
+ @Optional() protected _parentFormGroup: FormGroupDirective) {
this._inputElement = this._elementRef.nativeElement as HTMLInputElement;
}
@@ -85,6 +113,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 +139,6 @@ export class MdChipInput {
}
}
}
+
+ focus() { this._elementRef.nativeElement.focus(); }
}
diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts
index 1a080fb4f708..3832fb40380f 100644
--- a/src/lib/chips/chip-list.spec.ts
+++ b/src/lib/chips/chip-list.spec.ts
@@ -1,13 +1,21 @@
+<<<<<<< HEAD
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 +24,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 +40,20 @@ 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,
+ FalsyValueChipList,
],
providers: [{
provide: Directionality, useFactory: () => {
@@ -311,6 +331,431 @@ 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.length).toBe(1, 'Expect one option to be selected');
+ expect(chipList.selected[0]).toBe(chips.first, 'Expected first option to be selected.');
+
+ fixture.componentInstance.foods = [];
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(chipList.selected.length)
+ .toBe(0, '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.length)
+ .toBe(0, 'Expect no selected chips');
+ });
+
+ });
+
+ describe('forms integration', () => {
+ 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);
+ 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('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;
+ const nativeInput = fixture.debugElement.query(By.css('.mat-chip-input')).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 +815,111 @@ class StandardChipList {
})
class FormFieldChipList {
}
+
+
+@Component({
+ selector: 'basic-chip-list',
+ template: `
+
+
+
+ {{ food.viewValue }}
+
+
+
+ `
+})
+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;
+}
+
+@Component({
+ selector: 'input-chip-list',
+ template: `
+
+
+
+ {{ food.viewValue }}
+
+
+ />
+
+ `
+})
+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;
+}
+
+@Component({
+ template: `
+
+
+ {{ food.viewValue }}
+
+
+ `
+})
+class FalsyValueChipList {
+ foods: any[] = [
+ { value: 0, viewValue: 'Steak' },
+ { value: 1, viewValue: 'Pizza' },
+ ];
+ control = new FormControl();
+ @ViewChildren(MdChip) chips: QueryList;
+}
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
index e2587427028e..443122120350 100644
--- a/src/lib/chips/chip-list.ts
+++ b/src/lib/chips/chip-list.ts
@@ -8,24 +8,47 @@
import {
AfterContentInit,
+ ChangeDetectorRef,
ChangeDetectionStrategy,
Component,
ContentChildren,
+ ElementRef,
+ EventEmitter,
Input,
- QueryList,
- ViewEncapsulation,
OnDestroy,
+ OnInit,
Optional,
- ElementRef,
+ Output,
+ QueryList,
Renderer2,
+ Self,
+ ViewEncapsulation,
} from '@angular/core';
+import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
-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 {Subscription} from 'rxjs/Subscription';
+import {SelectionModel} from '@angular/cdk/collections';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
+import {Subject} from 'rxjs/Subject';
+import {startWith} from '@angular/cdk/rxjs';
+import {Observable} from 'rxjs/Observable';
+import {merge} from 'rxjs/observable/merge';
+import {Subscription} from 'rxjs/Subscription';
+
+import {MdChip, MdChipEvent, MdChipSelectionChange} from './chip';
+import {MdChipInput} from './chip-input';
+import {MdFormFieldControl} from '../form-field/form-field-control';
+
+// 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 +60,34 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion';
exportAs: 'mdChipList',
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]': 'true',
+ '[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, 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();
/** When a chip is destroyed, we track the index so we can focus the appropriate next chip. */
protected _lastDestroyedIndex: number|null = null;
@@ -62,10 +98,43 @@ 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 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;
+ /** 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 +148,157 @@ export class MdChipList implements AfterContentInit, OnDestroy {
/** The FocusKeyManager which handles focus. */
_keyManager: FocusKeyManager;
- /** The chip components contained within this chip list. */
- chips: QueryList;
+ /** Function when touched */
+ _onTouched = () => {};
+
+ /** Function when changed */
+ _onChange: (value: any) => void = () => {};
+
+ _selectionModel: SelectionModel;
+
+ /** 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[] {
+ return this._selectionModel.selected;
+ }
+
+ /**
+ * 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 {
+ return merge(...this.chips.map(chip => chip.onSelectionChange));
+ }
+
+ /** Combined stream of all of the child chips' focus change events. */
+ get chipFocusChanges(): Observable {
+ return merge(...this.chips.map(chip => chip._onFocus));
+ }
+
+ /** Combined stream of all of the child chips' remove change events. */
+ get chipRemoveChanges(): Observable {
+ 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 = new EventEmitter();
+
+ /**
+ * 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();
+
+ /** The chip components contained within this chip list. */
+ @ContentChildren(MdChip) chips: QueryList;
+
+ 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(this.chips).withWrap();
// Prevents the chip list from capturing focus and redirecting
@@ -99,20 +308,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) => {
- 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 +323,51 @@ export class MdChipList implements AfterContentInit, OnDestroy {
});
}
+ ngOnInit() {
+ this._selectionModel = new SelectionModel(true, 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;
+
+ /** Associates an HTML input element with this chip list. */
+ registerInput(inputElement: MdChipInput) {
+ this._chipInput = inputElement;
}
- set selectable(value: boolean) {
- this._selectable = coerceBooleanProperty(value);
+ // 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);
+ }
}
- @Input()
- set tabIndex(value: number) {
- this._userTabIndex = value;
- this._tabIndex = value;
+ // Implemented as part of ControlValueAccessor
+ registerOnChange(fn: (value: any) => void): void {
+ this._onChange = fn;
}
- /** Associates an HTML input element with this chip list. */
- registerInput(inputElement: HTMLInputElement) {
- this._inputElement = inputElement;
+ // 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 +376,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 +427,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): void {
- chips.forEach(chip => this._addChip(chip));
- }
/**
* Check the tab index as you should not be allowed to focus an empty list.
@@ -223,51 +440,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;
- }
-
- // 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);
- }
+ 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;
- }
-
}
-
- this._chipSet.delete(chip);
- chip.destroy.unsubscribe();
- });
-
- this._chipSet.set(chip, true);
+ if (this._keyManager.activeItemIndex === chipIndex) {
+ this._lastDestroyedIndex = chipIndex;
+ }
+ }
}
/**
@@ -282,11 +474,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 +505,156 @@ 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();
+ }
+
+ /**
+ * 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 {
+ 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(): void {
+ let valueToEmit = this.selected.length === 0 ? null : this.selected.map(chip => chip.value);
+ 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._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);
+ 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();
+ });
+ }
+
+ private _listenToChipsRemoved(): void {
+ this._chipRemoveSubscription = this.chipRemoveChanges.subscribe((event) => {
+ this._updateKeyManager(event.chip);
+ });
+ }
}
diff --git a/src/lib/chips/chip.spec.ts b/src/lib/chips/chip.spec.ts
index 90c3bdfa873f..cb8b6f506658 100644
--- a/src/lib/chips/chip.spec.ts
+++ b/src/lib/chips/chip.spec.ts
@@ -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', () => {
{{name}}
@@ -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 = () => {};
}
diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts
index 3a88bd516064..089d0e3ecc94 100644
--- a/src/lib/chips/chip.ts
+++ b/src/lib/chips/chip.ts
@@ -34,6 +34,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 {
@@ -72,79 +78,102 @@ export class MdBasicChip { }
'(keydown)': '_handleKeydown($event)',
'(focus)': '_hasFocus = true',
'(blur)': '_hasFocus = false',
- }
+ },
+
})
export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestroy, CanColor,
CanDisable {
- @ContentChild(forwardRef(() => MdChipRemove)) _chipRemove: MdChipRemove;
+ protected _value: any;
+
+ protected _selected: boolean = false;
+
+ protected _selectable: boolean = true;
+
+ protected _removable: boolean = true;
+
+ /** Whether the chip has focus. */
+ _hasFocus: boolean = false;
/** Whether the chip is selected. */
- @Input() get selected(): boolean { return this._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});
+ this.onSelectionChange.emit({source: this, isUserInput: false});
}
- protected _selected: boolean = false;
+
+ /** The value of the chip. Defaults to the content inside tags. */
+ @Input()
+ get value(): any {
+ return this._value != undefined
+ ? this._value
+ : this._elementRef.nativeElement.textContent.trim();
+ }
+ 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);
- }
- protected _selectable: boolean = true;
+ @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);
- }
- protected _removable: boolean = true;
-
- /** Whether the chip has focus. */
- _hasFocus: boolean = false;
+ @Input()
+ get removable(): boolean { return this._removable; }
+ set removable(value: boolean) { this._removable = coerceBooleanProperty(value); }
/** Emits when the chip is focused. */
_onFocus = new Subject();
- /** Emitted when the chip is selected. */
- @Output() select = new EventEmitter();
-
- /** Emitted when the chip is deselected. */
- @Output() deselect = new EventEmitter();
+ /** Emitted when the chip is selected or deselected. */
+ @Output() onSelectionChange = new EventEmitter();
/** Emitted when the chip is destroyed. */
@Output() destroy = new EventEmitter();
+ /** Emitted when a chip is to be removed. */
+ @Output('remove') onRemove = new EventEmitter();
+
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();
-
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 +225,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
diff --git a/src/lib/chips/chips.md b/src/lib/chips/chips.md
index 1e26923b5b79..839f592dafa3 100644
--- a/src/lib/chips/chips.md
+++ b/src/lib/chips/chips.md
@@ -22,13 +22,38 @@ _Hint: `` receives the `mat-basic-chip` CSS class in addition to
Chips can be selected via the `selected` property. Selection can be disabled by setting
`selectable` to `false` on the ``.
-Selection emits the `(select)` output while deselecting emits the `(deselect)` output. Both outputs
-receive a ChipEvent object with a structure of `{ chip: alteredChip }`.
+Selection and deselecting emit the `(onSelectionChange)` output. The output receive a
+ChipSelectionChange object with a structure of `{ source: alteredChip, isUserInput: boolean }`.
### Disabled chips
Individual chips may be disabled by applying the `disabled` attribute to the chip. When disabled,
chips are neither selectable nor focusable. Currently, disabled chips receive no special styling.
+### Chip input
+Chip input can work with chip list to add new chips to the chip list. It implements chip-specified
+behaviors to an input element inside ``. Chip input may be placed inside or outside of
+an ``.
+
+```html
+
+
+ Chip 1
+ Chip 2
+
+
+
+```
+
+```html
+
+
+ Chip 1
+ Chip 2
+
+
+
+```
+
### Keyboard interaction
Users can move through the chips using the arrow keys and select/deselect them with the space. Chips
also gain focus when clicked, ensuring keyboard navigation starts at the appropriate chip.
diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss
index b170a27039e1..478caf88d71f 100644
--- a/src/lib/chips/chips.scss
+++ b/src/lib/chips/chips.scss
@@ -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;
+}
diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts
index 8a3df761e601..c3a3c72f3d07 100644
--- a/src/lib/input/input.ts
+++ b/src/lib/input/input.ts
@@ -69,13 +69,13 @@ let nextUniqueId = 0;
})
export class MdInput implements MdFormFieldControl, 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, OnChanges, OnDestroy, D
}
}
- private _neverEmptyInputTypes = [
+ protected _neverEmptyInputTypes = [
'date',
'datetime',
'datetime-local',
@@ -146,12 +146,12 @@ export class MdInput implements MdFormFieldControl, 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, 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, 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, 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`.
From ae03bfed94283b215ce6cd72b4b70164bc98d25b Mon Sep 17 00:00:00 2001
From: Yuan Gao
Date: Thu, 31 Aug 2017 18:26:36 -0700
Subject: [PATCH 2/6] Add multiple selection and single selection to chip list
---
src/demo-app/chips/chips-demo.html | 13 +-
src/demo-app/chips/chips-demo.ts | 1 +
src/lib/chips/chip-list.spec.ts | 338 +++++++++++++++++++----------
src/lib/chips/chip-list.ts | 65 ++++--
4 files changed, 292 insertions(+), 125 deletions(-)
diff --git a/src/demo-app/chips/chips-demo.html b/src/demo-app/chips/chips-demo.html
index 3ada94d8eee0..4673b48b256a 100644
--- a/src/demo-app/chips/chips-demo.html
+++ b/src/demo-app/chips/chips-demo.html
@@ -128,7 +128,7 @@ Stacked Chips
NgModel with chip list
-
+
{{aColor.name}}
@@ -137,6 +137,17 @@ NgModel with chip list
The selected colors are {{color}}.
+
+ NgModel with single selection chip list
+
+
+ {{aColor.name}}
+ cancel
+
+
+
+ The selected color is {{selectedColor}}.
diff --git a/src/demo-app/chips/chips-demo.ts b/src/demo-app/chips/chips-demo.ts
index 8f63a4f3efd9..4d5995aed4a3 100644
--- a/src/demo-app/chips/chips-demo.ts
+++ b/src/demo-app/chips/chips-demo.ts
@@ -94,4 +94,5 @@ export class ChipsDemo {
this.visible = false;
}
selectedColors: any[] = ['Primary', 'Warn'];
+ selectedColor = 'Accent';
}
diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts
index 3832fb40380f..e20f1aecec3c 100644
--- a/src/lib/chips/chip-list.spec.ts
+++ b/src/lib/chips/chip-list.spec.ts
@@ -53,6 +53,7 @@ describe('MdChipList', () => {
FormFieldChipList,
BasicChipList,
InputChipList,
+ MultiSelectionChipList,
FalsyValueChipList,
],
providers: [{
@@ -363,15 +364,14 @@ describe('MdChipList', () => {
fixture.detectChanges();
expect(instanceChips.first.selected).toBe(true, 'Expected first option to be selected.');
- expect(chipList.selected.length).toBe(1, 'Expect one option to be selected');
- expect(chipList.selected[0]).toBe(chips.first, '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.length)
- .toBe(0, 'Expected selection to be removed when option no longer exists.');
+ expect(chipList.selected)
+ .toBe(undefined, 'Expected selection to be removed when option no longer exists.');
});
}));
@@ -401,8 +401,8 @@ describe('MdChipList', () => {
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.length)
- .toBe(0, 'Expect no selected chips');
+ expect(fixture.componentInstance.chipList.selected)
+ .toBeUndefined('Expect no selected chips');
});
});
@@ -411,155 +411,238 @@ describe('MdChipList', () => {
let formField: HTMLElement;
let nativeChips: HTMLElement[];
- beforeEach(() => {
- fixture = TestBed.createComponent(BasicChipList);
- fixture.detectChanges();
+ 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'))
+ 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;
- });
+ chips = fixture.componentInstance.chips;
+ });
- it('should take an initial view value with reactive forms', () => {
- fixture.componentInstance.control = new FormControl(['pizza-1']);
- fixture.detectChanges();
+ it('should take an initial view value with reactive forms', () => {
+ fixture.componentInstance.control = new FormControl('pizza-1');
+ fixture.detectChanges();
- const array = chips.toArray();
+ const array = chips.toArray();
- expect(array[1].selected).toBeTruthy('Expect pizza-1 chip to be selected');
+ expect(array[1].selected).toBeTruthy('Expect pizza-1 chip to be selected');
- dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
- fixture.detectChanges();
+ dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
+ fixture.detectChanges();
- expect(array[1].selected).toBeFalsy('Expect chip to be not selected after toggle selected');
- });
+ 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();
+ 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');
+ expect(chipList.value).toBeFalsy('Expect chip list to have no initial value');
- fixture.componentInstance.control.setValue(['pizza-1']);
- fixture.detectChanges();
+ fixture.componentInstance.control.setValue('pizza-1');
+ fixture.detectChanges();
- expect(array[1].selected).toBeTruthy('Expect chip to be selected');
- });
+ expect(array[1].selected).toBeTruthy('Expect chip to be selected');
+ });
- it('should update the form value when the view changes', () => {
+ 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.`);
+ expect(fixture.componentInstance.control.value)
+ .toEqual(null, `Expected the control's value to be empty initially.`);
- dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
- fixture.detectChanges();
+ 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.`);
- });
+ 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();
+ it('should clear the selection when a nonexistent option value is selected', () => {
+ const array = chips.toArray();
- fixture.componentInstance.control.setValue(['pizza-1']);
- fixture.detectChanges();
+ fixture.componentInstance.control.setValue('pizza-1');
+ fixture.detectChanges();
- expect(array[1].selected)
- .toBeTruthy(`Expected chip with the value to be selected.`);
+ expect(array[1].selected)
+ .toBeTruthy(`Expected chip with the value to be selected.`);
- fixture.componentInstance.control.setValue(['gibberish']);
+ fixture.componentInstance.control.setValue('gibberish');
- fixture.detectChanges();
+ fixture.detectChanges();
- expect(array[1].selected)
- .toBeFalsy(`Expected chip with the old value not to be selected.`);
- });
+ 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();
+ 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.setValue('pizza-1');
+ fixture.detectChanges();
- fixture.componentInstance.control.reset();
- fixture.detectChanges();
+ fixture.componentInstance.control.reset();
+ fixture.detectChanges();
- expect(array[1].selected)
- .toBeFalsy(`Expected chip with the old value not to be selected.`);
- });
+ 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.');
+ 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');
+ 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.');
- });
+ 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.');
+ 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');
+ 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.');
- });
+ 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.`);
+ 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();
+ 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.`);
- });
+ 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.`);
+ 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']);
+ fixture.componentInstance.control.setValue('pizza-1');
- expect(fixture.componentInstance.control.dirty)
- .toEqual(false, `Expected control to stay pristine after programmatic change.`);
- });
+ 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.`);
+ 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();
+ 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.`);
+ 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');
+ });
});
- it('should be able to programmatically select a falsy option', () => {
- fixture.destroy();
+ 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.`);
+ });
- const falsyFixture = TestBed.createComponent(FalsyValueChipList);
- falsyFixture.detectChanges();
- falsyFixture.componentInstance.control.setValue([0]);
- falsyFixture.detectChanges();
- falsyFixture.detectChanges();
+ it('should clear the selection when the control is reset', () => {
+ const array = chips.toArray();
- expect(falsyFixture.componentInstance.chips.first.selected)
- .toBe(true, 'Expected first option to be selected');
+ 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.`);
+ });
});
});
@@ -649,7 +732,6 @@ describe('MdChipList', () => {
.toBe(false, 'Expected the control to start off as untouched.');
const nativeChipList = fixture.debugElement.query(By.css('.mat-chip-list')).nativeElement;
- const nativeInput = fixture.debugElement.query(By.css('.mat-chip-input')).nativeElement;
dispatchFakeEvent(nativeChipList, 'blur');
@@ -850,11 +932,47 @@ class BasicChipList {
@ViewChildren(MdChip) chips: QueryList;
}
+
+@Component({
+ selector: 'multi-selection-chip-list',
+ template: `
+
+
+
+ {{ food.viewValue }}
+
+
+
+ `
+})
+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;
+}
+
@Component({
selector: 'input-chip-list',
template: `
-
+
{{ food.viewValue }}
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
index 443122120350..4ce8acb597dc 100644
--- a/src/lib/chips/chip-list.ts
+++ b/src/lib/chips/chip-list.ts
@@ -64,7 +64,7 @@ export class MdChipListChange {
'[attr.aria-required]': 'required.toString()',
'[attr.aria-disabled]': 'disabled.toString()',
'[attr.aria-invalid]': 'errorState',
- '[attr.aria-multiselectable]': 'true',
+ '[attr.aria-multiselectable]': 'multiple',
'[class.mat-chip-list-disabled]': 'disabled',
'[class.mat-chip-list-invalid]': 'errorState',
'[class.mat-chip-list-required]': 'required',
@@ -113,6 +113,9 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
/** Whether or not the chip is selectable. */
protected _selectable: boolean = true;
+ /** Whether the component is in multiple selection mode. */
+ private _multiple: boolean = false;
+
/** The chip input to add more chips */
protected _chipInput: MdChipInput;
@@ -160,8 +163,15 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
private _compareWith = (o1: any, o2: any) => o1 === o2;
/** The array of selected chips inside chip list. */
- get selected(): MdChip[] {
- return this._selectionModel.selected;
+ 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);
}
/**
@@ -324,7 +334,7 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
}
ngOnInit() {
- this._selectionModel = new SelectionModel(true, undefined, false);
+ this._selectionModel = new SelectionModel(this.multiple, undefined, false);
this.stateChanges.next();
}
@@ -509,10 +519,19 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
_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));
+ }
}
- this._sortValues();
}
/**
@@ -561,19 +580,27 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
* order that they have in the panel.
*/
private _sortValues(): void {
- this._selectionModel.clear();
+ if (this._multiple) {
+ this._selectionModel.clear();
- this.chips.forEach(chip => {
- if (chip.selected) {
- this._selectionModel.select(chip);
- }
- });
- this.stateChanges.next();
+ this.chips.forEach(chip => {
+ if (chip.selected) {
+ this._selectionModel.select(chip);
+ }
+ });
+ this.stateChanges.next();
+ }
}
/** Emits change event to set the model value. */
- private _propagateChanges(): void {
- let valueToEmit = this.selected.length === 0 ? null : this.selected.map(chip => chip.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);
@@ -634,6 +661,16 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
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();
}
From fc5f6e9ca70205ec9f6e73a1c5df26dff7c1316c Mon Sep 17 00:00:00 2001
From: Yuan Gao
Date: Tue, 5 Sep 2017 15:55:57 -0700
Subject: [PATCH 3/6] try fix test
---
src/lib/chips/chip.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts
index 089d0e3ecc94..4a9702eca973 100644
--- a/src/lib/chips/chip.ts
+++ b/src/lib/chips/chip.ts
@@ -108,7 +108,7 @@ export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestr
get value(): any {
return this._value != undefined
? this._value
- : this._elementRef.nativeElement.textContent.trim();
+ : this._elementRef.nativeElement.textContent;
}
set value(newValue: any) { this._value = newValue; }
From 15106aafb2089def322793cd1fc1bc9fc59331a6 Mon Sep 17 00:00:00 2001
From: Yuan Gao
Date: Wed, 13 Sep 2017 10:25:56 -0700
Subject: [PATCH 4/6] not extend from MdInput
---
src/lib/chips/chip-input.ts | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/src/lib/chips/chip-input.ts b/src/lib/chips/chip-input.ts
index 7a8902329ef4..1c8aa79e6424 100644
--- a/src/lib/chips/chip-input.ts
+++ b/src/lib/chips/chip-input.ts
@@ -12,12 +12,7 @@ import {
Output,
EventEmitter,
Input,
- Optional,
- Renderer2,
- Self,
} from '@angular/core';
-import {FormGroupDirective, NgControl, NgForm} from '@angular/forms';
-import {Platform} from '@angular/cdk/platform';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ENTER} from '@angular/material/core';
import {MdChipList} from './chip-list';
@@ -87,19 +82,14 @@ export class MdChipInput {
@Input() placeholder: string = '';
get empty(): boolean {
- let value: string | null = this._elementRef.nativeElement.value;
+ let value: string | null = this._inputElement.value;
return value == null || value === '';
}
/** The native input element to which this directive is attached. */
protected _inputElement: HTMLInputElement;
- constructor(protected _elementRef: ElementRef,
- protected _renderer: Renderer2,
- protected _platform: Platform,
- @Optional() @Self() public ngControl: NgControl,
- @Optional() protected _parentForm: NgForm,
- @Optional() protected _parentFormGroup: FormGroupDirective) {
+ constructor(protected _elementRef: ElementRef) {
this._inputElement = this._elementRef.nativeElement as HTMLInputElement;
}
@@ -140,5 +130,5 @@ export class MdChipInput {
}
}
- focus() { this._elementRef.nativeElement.focus(); }
+ focus() { this._inputElement.focus(); }
}
From 1bfa13e1c78113d211b0f3b1115d282f46e77b0b Mon Sep 17 00:00:00 2001
From: Yuan Gao
Date: Wed, 13 Sep 2017 18:00:38 -0700
Subject: [PATCH 5/6] Fix blur & focus behavior
---
src/lib/chips/chip-list.spec.ts | 7 +------
src/lib/chips/chip-list.ts | 18 ++++++++++++++++++
src/lib/chips/chip.ts | 12 +++++++++---
3 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts
index e20f1aecec3c..947c59071f10 100644
--- a/src/lib/chips/chip-list.spec.ts
+++ b/src/lib/chips/chip-list.spec.ts
@@ -1,13 +1,8 @@
-<<<<<<< HEAD
import {FocusKeyManager} from '@angular/cdk/a11y';
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 {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {
BACKSPACE,
DELETE,
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
index 4ce8acb597dc..24d3b48e3046 100644
--- a/src/lib/chips/chip-list.ts
+++ b/src/lib/chips/chip-list.ts
@@ -104,6 +104,9 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
/** 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;
@@ -277,6 +280,11 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
return merge(...this.chips.map(chip => chip._onFocus));
}
+ /** Combined stream of all of the child chips' blur change events. */
+ get chipBlurChanges(): Observable {
+ return merge(...this.chips.map(chip => chip._onBlur));
+ }
+
/** Combined stream of all of the child chips' remove change events. */
get chipRemoveChanges(): Observable {
return merge(...this.chips.map(chip => chip.destroy));
@@ -649,6 +657,11 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
this._chipFocusSubscription = null;
}
+ if (this._chipBlurSubscription) {
+ this._chipBlurSubscription.unsubscribe();
+ this._chipBlurSubscription = null;
+ }
+
if (this._chipSelectionSubscription) {
this._chipSelectionSubscription.unsubscribe();
this._chipSelectionSubscription = null;
@@ -687,6 +700,11 @@ export class MdChipList implements MdFormFieldControl, ControlValueAccessor
}
this.stateChanges.next();
});
+
+ this._chipBlurSubscription = this.chipBlurChanges.subscribe(_ => {
+ this._blur();
+ this.stateChanges.next();
+ });
}
private _listenToChipsRemoved(): void {
diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts
index 4a9702eca973..d9c1b0cbccdc 100644
--- a/src/lib/chips/chip.ts
+++ b/src/lib/chips/chip.ts
@@ -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,
@@ -77,7 +75,7 @@ export class MdBasicChip { }
'(click)': '_handleClick($event)',
'(keydown)': '_handleKeydown($event)',
'(focus)': '_hasFocus = true',
- '(blur)': '_hasFocus = false',
+ '(blur)': '_blur()',
},
})
@@ -130,6 +128,9 @@ export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestr
/** Emits when the chip is focused. */
_onFocus = new Subject();
+ /** Emits when the chip is blured. */
+ _onBlur = new Subject();
+
/** Emitted when the chip is selected or deselected. */
@Output() onSelectionChange = new EventEmitter();
@@ -233,6 +234,11 @@ export class MdChip extends _MdChipMixinBase implements FocusableOption, OnDestr
break;
}
}
+
+ _blur() {
+ this._hasFocus = false;
+ this._onBlur.next({chip: this});
+ }
}
From 9cb4713cb68851ffc40cb24d8d5068b9f22bfff1 Mon Sep 17 00:00:00 2001
From: Yuan Gao
Date: Fri, 15 Sep 2017 10:35:57 -0700
Subject: [PATCH 6/6] .
---
src/lib/chips/chip-list.ts | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
index 24d3b48e3046..d101dc45669b 100644
--- a/src/lib/chips/chip-list.ts
+++ b/src/lib/chips/chip-list.ts
@@ -5,7 +5,10 @@
* 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,
@@ -25,21 +28,23 @@ import {
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 {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 {SelectionModel} from '@angular/cdk/collections';
-import {coerceBooleanProperty} from '@angular/cdk/coercion';
-import {Subject} from 'rxjs/Subject';
-import {startWith} from '@angular/cdk/rxjs';
import {Observable} from 'rxjs/Observable';
import {merge} from 'rxjs/observable/merge';
+import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {MdChip, MdChipEvent, MdChipSelectionChange} from './chip';
import {MdChipInput} from './chip-input';
-import {MdFormFieldControl} from '../form-field/form-field-control';
// Increasing integer for generating unique ids for chip-list components.
let nextUniqueId = 0;