Skip to content

Commit 7a0882b

Browse files
Add MDC adapters and verify foundation and adapters communicating
1 parent 64f1963 commit 7a0882b

File tree

8 files changed

+302
-25
lines changed

8 files changed

+302
-25
lines changed

src/dev-app/mdc-chips/mdc-chips-demo-module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10+
import {MatCardModule} from '@angular/material/card';
11+
import {MatToolbarModule} from '@angular/material/toolbar';
1012
import {MatChipsModule} from '@angular/material-experimental/mdc-chips';
1113
import {RouterModule} from '@angular/router';
1214
import {MdcChipsDemo} from './mdc-chips-demo';
1315

1416
@NgModule({
1517
imports: [
18+
MatCardModule,
1619
MatChipsModule,
20+
MatToolbarModule,
1721
RouterModule.forChild([{path: '', component: MdcChipsDemo}]),
1822
],
1923
declarations: [MdcChipsDemo],
Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,25 @@
1-
<!-- TODO: copy in demo template from existing mat-chip demo. -->
2-
<mat-chip-grid></mat-chip-grid>
1+
<div class="demo-chips">
2+
<mat-card>
3+
<mat-toolbar color="primary">Static Chips</mat-toolbar>
4+
5+
<mat-card-content>
6+
<h4>Simple</h4>
7+
8+
<mat-chip-grid>
9+
<mat-chip-cell>Chip 1</mat-chip-cell>
10+
<mat-chip-cell>Chip 2</mat-chip-cell>
11+
<mat-chip-cell disabled>Chip 3</mat-chip-cell>
12+
</mat-chip-grid>
13+
14+
<h4>With avatar and icons</h4>
15+
16+
<mat-chip-grid>
17+
<mat-chip-cell disabled>
18+
<i matChipAvatar class="material-icons">home</i>
19+
Home
20+
<i matChipTrailingIcon class="material-icons">cancel</i>
21+
</mat-chip-cell>
22+
</mat-chip-grid>
23+
</mat-card-content>
24+
</mat-card>
25+
</div>
Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,31 @@
1-
// TODO: copy in demo styles from existing mat-chips demo.
1+
.demo-chips {
2+
.mat-chip-list-stacked {
3+
display: block;
4+
max-width: 200px;
5+
}
6+
7+
.mat-card {
8+
padding: 0;
9+
margin: 16px;
10+
11+
& .mat-toolbar {
12+
margin: 0;
13+
}
14+
15+
& .mat-card-content {
16+
padding: 24px;
17+
}
18+
}
19+
20+
.mat-basic-chip {
21+
margin: auto 10px;
22+
}
23+
24+
mat-chip-list input {
25+
width: 150px;
26+
}
27+
}
28+
29+
.demo-has-chip-list {
30+
width: 100%;
31+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="mdc-chip" [ngClass]="_classes" tabindex="0">
2+
<div class="mdc-chip__text"><ng-content></ng-content></div>
3+
</div>
4+

src/material-experimental/mdc-chips/chip-cell.ts

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,184 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component} from '@angular/core';
10-
import {MDCChipFoundation} from '@material/chips';
9+
import {AfterContentInit, AfterViewInit, Component, ChangeDetectorRef, ContentChild, Directive, ElementRef, EventEmitter, Input, OnDestroy, Output} from '@angular/core';
10+
import {MDCChipAdapter, MDCChipFoundation} from '@material/chips';
11+
import {Subject} from 'rxjs';
12+
import {takeUntil} from 'rxjs/operators';
13+
14+
let uid = 0;
15+
16+
/**
17+
* Dummy directive to add CSS class to chip leading icon.
18+
* @docs-private
19+
*/
20+
@Directive({
21+
selector: 'mat-chip-avatar, [matChipAvatar]',
22+
host: {
23+
'class': 'mat-chip-avatar mdc-chip__icon mdc-chip__icon--leading',
24+
}
25+
})
26+
export class MatChipAvatar {
27+
_classes: {[key: string]: boolean} = {};
28+
29+
/** Sets whether the given CSS class should be applied to the leading icon. */
30+
setClass(cssClass: string, active: boolean) {
31+
const element = this._elementRef.nativeElement;
32+
active ? element.addClass(cssClass) : element.removeClass(cssClass);
33+
this._changeDetectorRef.markForCheck();
34+
}
35+
36+
constructor(private _changeDetectorRef: ChangeDetectorRef,
37+
private _elementRef: ElementRef) {}
38+
}
39+
40+
/**
41+
* Dummy directive to add CSS class to chip trailing icon.
42+
* @docs-private
43+
*/
44+
@Directive({
45+
selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]',
46+
host: {
47+
'class': 'mat-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
48+
'tabindex': '0',
49+
'role': 'button',
50+
'(click)': 'interaction.emit($event)',
51+
'(keydown)': 'interaction.emit($event)',
52+
}
53+
})
54+
export class MatChipTrailingIcon {
55+
@Output() interaction = new EventEmitter<MouseEvent | KeyboardEvent>();
56+
}
1157

1258
@Component({
1359
selector: 'mat-chip-cell',
1460
templateUrl: 'chip-cell.html',
1561
styleUrls: ['chips.css'],
1662
host: {
1763
'class': 'mat-mdc-chip-cell',
64+
'[id]': 'id',
1865
'(click)': '_chipFoundation.handleInteraction($event)',
19-
'(keydown)': '_chipFoundation.handleInteraction($event)'
66+
'(keydown)': '_chipFoundation.handleInteraction($event)',
67+
'(transitionend)': '_chipFoundation.handleTransitionEnd($event)'
2068
},
2169
})
22-
export class MatChipCell {
70+
export class MatChipCell implements AfterContentInit, AfterViewInit, OnDestroy {
71+
private _uniqueId = `mat-mdc-chip-${uid++}`;
72+
73+
/** A unique id for the chip. If none is supplied, it will be auto-generated. */
74+
@Input() id: string = this._uniqueId;
75+
76+
/**
77+
* EventEmitters used to notify the parent chip-set of an event on the chip.
78+
*/
79+
@Output() removal = new EventEmitter<string>();
80+
@Output() trailingIconInteraction = new EventEmitter<string>();
81+
@Output() interaction = new EventEmitter<string>();
82+
@Output() selection = new EventEmitter<{id: string, selected: boolean}>();
83+
84+
/** Whether the chip is selected. */
85+
get selected(): boolean {
86+
return this._chipFoundation.isSelected();
87+
}
88+
89+
/** Sets selected state on the chip. */
90+
set selected(selected: boolean) {
91+
this._chipFoundation.setSelected(selected);
92+
}
93+
94+
/** Whether a trailing icon click should trigger exit/removal of the chip. */
95+
get shouldRemoveOnTrailingIconClick(): boolean {
96+
return this._chipFoundation.getShouldRemoveOnTrailingIconClick();
97+
}
98+
99+
/** Sets whether a trailing icon click should trigger exit/removal of the chip. */
100+
set shouldRemoveOnTrailingIconClick(shouldRemove: boolean) {
101+
this._chipFoundation.setShouldRemoveOnTrailingIconClick(shouldRemove);
102+
}
103+
104+
/** The MDC foundation containing business logic for MDC chip. */
23105
_chipFoundation: MDCChipFoundation;
24106

25-
constructor() {
26-
this._chipFoundation = new MDCChipFoundation();
107+
/**
108+
* Map from class to whether the class is enabled.
109+
* Enabled classes are set on the chip element.
110+
*/
111+
_classes: {[key: string]: boolean} = {};
112+
113+
/** Subject that emits when the component has been destroyed. */
114+
private _destroyed = new Subject<void>();
115+
116+
/** The chip's leading icon. */
117+
@ContentChild(MatChipAvatar, {static: false}) leadingIcon: MatChipAvatar;
118+
119+
/** The chip's trailing icon. */
120+
@ContentChild(MatChipTrailingIcon, {static: false}) trailingIcon: MatChipTrailingIcon;
121+
122+
/**
123+
* Implementation of the MDC chip adapter interface.
124+
* These methods are called by the chip foundation.
125+
*/
126+
private _chipAdapter: MDCChipAdapter = {
127+
addClass: (className) => this._setClass(className, true),
128+
removeClass: (className) => this._setClass(className, false),
129+
hasClass: (className) => className in this._classes,
130+
addClassToLeadingIcon: (className) => this.leadingIcon.setClass(className, true),
131+
removeClassFromLeadingIcon: (className) => this.leadingIcon.setClass(className, false),
132+
eventTargetHasClass: (target: EventTarget | null, className: string) => {
133+
return target ? (target as Element).classList.contains(className) : false;
134+
},
135+
notifyInteraction: () => this.interaction.emit(this.id),
136+
notifySelection: () => this.selection.emit({id: this.id, selected: this.selected}),
137+
notifyTrailingIconInteraction: () => this.trailingIconInteraction.emit(this.id),
138+
notifyRemoval: () => this.removal.emit(this.id),
139+
getComputedStyleValue: (propertyName) => {
140+
return window.getComputedStyle(this._elementRef.nativeElement).getPropertyValue(propertyName);
141+
},
142+
setStyleProperty: (propertyName: string, value: string) => {
143+
this._elementRef.nativeElement.style.setProperty(propertyName, value);
144+
},
145+
hasLeadingIcon: () => {return !!this.leadingIcon},
146+
getRootBoundingClientRect: () => this._elementRef.nativeElement.getBoundingClientRect(),
147+
getCheckmarkBoundingClientRect: () => {return null},
148+
};
149+
150+
constructor(
151+
private _changeDetectorRef: ChangeDetectorRef,
152+
private _elementRef: ElementRef) {
153+
this._chipFoundation = new MDCChipFoundation(this._chipAdapter);
154+
}
155+
156+
ngAfterContentInit() {
157+
if (this.trailingIcon) {
158+
this.trailingIcon.interaction
159+
.pipe(takeUntil(this._destroyed))
160+
.subscribe((event) => this._chipFoundation.handleTrailingIconInteraction(event));
161+
}
27162
}
28163

29164
ngAfterViewInit() {
30165
this._chipFoundation.init();
31166
}
32167

33168
ngOnDestroy() {
169+
this._destroyed.next();
170+
this._destroyed.complete();
34171
this._chipFoundation.destroy();
35172
}
173+
174+
/**
175+
* Begins the exit animation which leads to removal of the chip.
176+
* If shouldRemoveOnTrailingIconClick is set to false, you must manually call
177+
* this method to remove the chip.
178+
*/
179+
beginExit() {
180+
this._chipFoundation.beginExit();
181+
}
182+
183+
/** Sets whether the given CSS class should be applied to the chip. */
184+
private _setClass(cssClass: string, active: boolean) {
185+
this._classes[cssClass] = active;
186+
this._changeDetectorRef.markForCheck();
187+
}
36188
}
189+
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
<div class="mdc-chip-set">
2-
<div class="mdc-chip" tabindex="0">
3-
<div class="mdc-chip__text">Chip 1</div>
4-
</div>
5-
<div class="mdc-chip" tabindex="0">
6-
<div class="mdc-chip__text">Chip 2</div>
7-
</div>
1+
<div #chipSetWrapper class="mdc-chip-set">
2+
<ng-content></ng-content>
83
</div>
94

src/material-experimental/mdc-chips/chip-grid.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, ViewEncapsulation} from '@angular/core';
10-
import {MDCChipSetFoundation} from '@material/chips';
9+
import {AfterContentInit, AfterViewInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, OnDestroy, QueryList, ViewChild, ViewEncapsulation} from '@angular/core';
10+
import {MDCChipSetAdapter, MDCChipSetFoundation} from '@material/chips';
11+
import {MatChipCell} from './chip-cell';
12+
import {Subject} from 'rxjs';
13+
import {takeUntil} from 'rxjs/operators';
1114

1215
@Component({
1316
moduleId: module.id,
@@ -20,18 +23,83 @@ import {MDCChipSetFoundation} from '@material/chips';
2023
encapsulation: ViewEncapsulation.None,
2124
changeDetection: ChangeDetectionStrategy.OnPush,
2225
})
23-
export class MatChipGrid implements AfterViewInit, OnDestroy {
24-
_chipSetFoundation: MDCChipSetFoundation;
26+
export class MatChipGrid implements AfterContentInit, AfterViewInit, OnDestroy {
27+
/** Returns all chips in the set. */
28+
get chips(): ReadonlyArray<MatChipCell> {
29+
return this._chips.toArray();
30+
}
31+
32+
/** Returns the ids of all selected chips. */
33+
get selectedChipIds(): ReadonlyArray<string> {
34+
return this._chipSetFoundation.getSelectedChipIds();
35+
}
36+
37+
/** The chips that are part of this chip set.*/
38+
@ContentChildren(MatChipCell) _chips: QueryList<MatChipCell>;
39+
40+
/** The wrapper div around the chips. */
41+
@ViewChild('chipSetWrapper') _wrapper!: ElementRef;
42+
43+
/** The MDC foundation containing business logic for MDC chip-set. */
44+
private _chipSetFoundation: MDCChipSetFoundation;
45+
46+
/** Subject that emits when the component has been destroyed. */
47+
private _destroyed = new Subject<void>();
2548

26-
constructor() {
27-
this._chipSetFoundation = new MDCChipSetFoundation();
49+
/**
50+
* Implementation of the MDC chip-set adapter interface.
51+
* These methods are called by the chip set foundation.
52+
*/
53+
private _chipSetAdapter: MDCChipSetAdapter = {
54+
hasClass: (className) => this._elementRef.nativeElement.classList.contains(className),
55+
removeChip: (chipId) => {
56+
const chip = document.getElementById(chipId);
57+
this._wrapper.nativeElement.removeChild(chip);
58+
},
59+
setSelected: (chipId, selected) => {
60+
const chip = this._chips.find(c => c.id === chipId);
61+
if (chip) chip.selected = selected;
62+
},
63+
}
64+
65+
constructor(private _elementRef: ElementRef) {
66+
this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter);
2867
}
2968

3069
ngAfterViewInit() {
3170
this._chipSetFoundation.init();
3271
}
3372

73+
// TODO sync chips when content changes
74+
ngAfterContentInit() {
75+
this._chips.forEach(chip => {
76+
if (chip.selected) {
77+
this._chipSetFoundation.select(chip.id);
78+
}
79+
80+
chip.removal
81+
.pipe(takeUntil(this._destroyed))
82+
.subscribe((id: string) => {
83+
this._chipSetFoundation.handleChipRemoval(id);
84+
});
85+
86+
chip.interaction
87+
.pipe(takeUntil(this._destroyed))
88+
.subscribe((id: string) => {
89+
this._chipSetFoundation.handleChipInteraction(id);
90+
});
91+
92+
chip.selection
93+
.pipe(takeUntil(this._destroyed))
94+
.subscribe((event: {id: string, selected: boolean}) => {
95+
this._chipSetFoundation.handleChipSelection(event.id, event.selected);
96+
});
97+
});
98+
}
99+
34100
ngOnDestroy() {
101+
this._destroyed.next();
102+
this._destroyed.complete();
35103
this._chipSetFoundation.destroy();
36104
}
37105
}

0 commit comments

Comments
 (0)