diff --git a/README.md b/README.md
index 826141e..61a509b 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Component | Documentation |
button | [button docs & demo](https://blox.src.zone/material/directives/button) |
card | [card docs & demo](https://blox.src.zone/material/directives/card) |
checkbox | [checkbox docs & demo](https://blox.src.zone/material/directives/checkbox) |
-chips | in tracker |
+chips | [chips docs & demo](https://blox.src.zone/material/directives/chips) |
dialog | [dialog docs & demo](https://blox.src.zone/material/directives/drawer) |
drawer | [drawer docs & demo](https://blox.src.zone/material/directives/drawer) |
elevation | [elevation docs & demo](https://blox.src.zone/material/directives/elevation) |
diff --git a/bundle/src/components/chips/mdc.chip-set.adapter.ts b/bundle/src/components/chips/mdc.chip-set.adapter.ts
new file mode 100644
index 0000000..22c70f8
--- /dev/null
+++ b/bundle/src/components/chips/mdc.chip-set.adapter.ts
@@ -0,0 +1,8 @@
+/** @docs-private */
+export interface MdcChipSetAdapter {
+ hasClass: (className: string) => boolean,
+ registerInteractionHandler: (evtType: string, handler: EventListener) => void,
+ deregisterInteractionHandler: (evtType: string, handler: EventListener) => void,
+ appendChip: (text: string, leadingIcon: Element, trailingIcon: Element) => Element,
+ removeChip: (chip: any) => void
+}
diff --git a/bundle/src/components/chips/mdc.chip.adapter.ts b/bundle/src/components/chips/mdc.chip.adapter.ts
new file mode 100644
index 0000000..ab17569
--- /dev/null
+++ b/bundle/src/components/chips/mdc.chip.adapter.ts
@@ -0,0 +1,18 @@
+/** @docs-private */
+export interface MdcChipAdapter {
+ addClass: (className: string) => void,
+ removeClass: (className: string) => void,
+ hasClass: (className: string) => boolean,
+ addClassToLeadingIcon: (className: string) => void,
+ removeClassFromLeadingIcon: (className: string) => void,
+ eventTargetHasClass: (target: EventTarget, className: string) => boolean,
+ registerEventHandler: (evtType: string, handler: EventListener) => void,
+ deregisterEventHandler: (evtType: string, handler: EventListener) => void,
+ registerTrailingIconInteractionHandler: (evtType: string, handler: EventListener) => void,
+ deregisterTrailingIconInteractionHandler: (evtType: string, handler: EventListener) => void,
+ notifyInteraction: () => void,
+ notifyTrailingIconInteraction: () => void,
+ notifyRemoval: () => void,
+ getComputedStyleValue: (propertyName: string) => string,
+ setStyleProperty: (propertyName: string, value: string) => void
+}
diff --git a/bundle/src/components/chips/mdc.chip.directive.spec.ts b/bundle/src/components/chips/mdc.chip.directive.spec.ts
new file mode 100644
index 0000000..052e6eb
--- /dev/null
+++ b/bundle/src/components/chips/mdc.chip.directive.spec.ts
@@ -0,0 +1,425 @@
+import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Component, DebugElement } from '@angular/core';
+import { MdcChipSetDirective, MdcChipDirective, MdcChipIconDirective, CHIP_DIRECTIVES } from './mdc.chip.directive';
+import { booleanAttributeStyleTest, hasRipple } from '../../testutils/page.test';
+
+describe('MdcChipDirective', () => {
+ @Component({
+ template: `
+
+
+
event
+
{{chip}}
+
cancel
+
+
+ `
+ })
+ class TestComponent {
+ includeLeadingIcon = false;
+ includeTrailingIcon = false;
+ type = 'action';
+ chips = ['chippie', 'chappie', 'choppie'];
+ interactions = [];
+ trailingIconInteractions = [];
+ valueChanges = [];
+ interact(chip: string) {
+ this.interactions.push(chip);
+ }
+ trailingIconInteract(chip: string) {
+ this.trailingIconInteractions.push(chip);
+ }
+ resetInteractions() {
+ this.interactions = [];
+ this.trailingIconInteractions = [];
+ }
+ remove(i: number) {
+ this.chips.splice(i, 1);
+ }
+ valueChange(chip: string, value: boolean) {
+ this.valueChanges.push({chip: chip, value: value});
+ }
+ }
+
+ function setup(testComponentType: any = TestComponent) {
+ const fixture = TestBed.configureTestingModule({
+ declarations: [...CHIP_DIRECTIVES, testComponentType]
+ }).createComponent(testComponentType);
+ fixture.detectChanges();
+ return { fixture };
+ }
+
+ it('apply correct styles for chip and chip-set', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ const chipSet: HTMLElement = fixture.nativeElement.querySelector('#set');
+ expect(chipSet.classList).toContain('mdc-chip-set');
+ const chips = chipSet.querySelectorAll('.mdc-chip');
+ expect(chips.length).toBe(3);
+ for (let i = 0; i !== chips.length; ++i) {
+ let icons = chips.item(i).querySelectorAll('i');
+ expect(icons.length).toBe(0);
+ }
+ testComponent.includeLeadingIcon = true;
+ testComponent.includeTrailingIcon = true;
+ fixture.detectChanges();
+ for (let i = 0; i !== chips.length; ++i) {
+ let icons = chips.item(i).querySelectorAll('i');
+ expect(icons.length).toBe(2);
+ expect(icons.item(0).classList).toContain('mdc-chip__icon');
+ expect(icons.item(1).classList).toContain('mdc-chip__icon');
+ expect(icons.item(0).classList).toContain('mdc-chip__icon--leading');
+ expect(icons.item(1).classList).toContain('mdc-chip__icon--trailing');
+ }
+ }));
+
+ it('chips have ripples', fakeAsync(() => {
+ const { fixture } = setup();
+ const chips = fixture.nativeElement.querySelectorAll('.mdc-chip');
+ expect(chips.length).toBe(3);
+ for (let i = 0; i !== chips.length; ++i)
+ expect(hasRipple(chips.item(i))).toBe(true);
+ }));
+
+ it('chipset type is one of choice, filter, input, or action', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ const chipSetComponent = fixture.debugElement.query(By.directive(MdcChipSetDirective)).injector.get(MdcChipSetDirective);
+ const chipSet: HTMLElement = fixture.nativeElement.querySelector('#set');
+
+ expect(chipSetComponent.mdcChipSet).toBe('action');
+ expect(chipSet.getAttribute('class')).toBe('mdc-chip-set');
+
+ for (let type of ['choice', 'filter', 'input']) {
+ testComponent.type = type;
+ fixture.detectChanges();
+ expect(chipSetComponent.mdcChipSet).toBe(type);
+ expect(chipSet.classList).toContain('mdc-chip-set--' + type);
+ }
+
+ testComponent.type = 'invalid';
+ fixture.detectChanges();
+ expect(chipSetComponent.mdcChipSet).toBe('action');
+ expect(chipSet.getAttribute('class')).toBe('mdc-chip-set');
+ }));
+
+ it('trailing icons get a tabindex and role=button', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.chips = ['chip'];
+ testComponent.includeLeadingIcon = true;
+ testComponent.includeTrailingIcon = true;
+ fixture.detectChanges();
+ let icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(2);
+ expect(icons[0].tabIndex).toBe(-1);
+ expect(icons[0].hasAttribute('role')).toBe(false);
+ expect(icons[1].tabIndex).toBe(0);
+ expect(icons[1].getAttribute('role')).toBe('button');
+
+ // role/tabIndex changes must be undone when the icon is not a trailing icon anymore:
+ const trailingIcon = fixture.debugElement.queryAll(By.directive(MdcChipIconDirective))[1].injector.get(MdcChipIconDirective);
+ expect(trailingIcon._elm.nativeElement).toBe(icons[1]);
+ trailingIcon._trailing = false;
+ expect(icons[1].tabIndex).toBe(-1);
+ expect(icons[1].hasAttribute('role')).toBe(false);
+ }));
+
+ it('unreachable code for our implementation must throw errors', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.chips = ['chip'];
+ fixture.detectChanges();
+ const chipSetComponent = fixture.debugElement.query(By.directive(MdcChipSetDirective)).injector.get(MdcChipSetDirective);
+ expect(() => {chipSetComponent._adapter.appendChip(null, null, null); }).toThrowError();
+ expect(() => {chipSetComponent._adapter.removeChip(null); }).toThrowError();
+ }));
+
+ it('click action chip triggers interaction event', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.chips = ['chip'];
+ fixture.detectChanges();
+ const chip = fixture.nativeElement.querySelector('.mdc-chip');
+ const trailingIcon = fixture.nativeElement.querySelector
+
+ expect(testComponent.interactions).toEqual([]);
+ expect(testComponent.trailingIconInteractions).toEqual([]);
+
+ chip.click();
+ expect(testComponent.interactions).toEqual(['chip']);
+ expect(testComponent.trailingIconInteractions).toEqual([]);
+ }));
+
+ it('trailing icon interactions trigger interaction and remove events', fakeAsync(() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'input';
+ testComponent.chips = ['chip'];
+ testComponent.includeTrailingIcon = true;
+ fixture.detectChanges();
+ const chip = fixture.nativeElement.querySelector('.mdc-chip');
+ const trailingIcon = fixture.nativeElement.querySelector('.mdc-chip i:last-child');
+ const chipComponent = fixture.debugElement.query(By.directive(MdcChipDirective)).injector.get(MdcChipDirective);
+
+ expect(testComponent.interactions).toEqual([]);
+ expect(testComponent.trailingIconInteractions).toEqual([]);
+
+ trailingIcon.click();
+ expect(testComponent.interactions).toEqual([]);
+ expect(testComponent.trailingIconInteractions).toEqual(['chip']);
+ // simulate transitionend event for exit transition of chip:
+ (chipComponent._foundation).handleTransitionEnd_({target: chip, propertyName: 'opacity'});
+ tick(20); // wait for requestAnimationFrame
+ (chipComponent._foundation).handleTransitionEnd_({target: chip, propertyName: 'width'});
+ expect(testComponent.chips).toEqual([]);
+ }));
+
+ it('filter chips get a checkmark icon on selection', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'filter';
+ fixture.detectChanges();
+ const chips = fixture.nativeElement.querySelectorAll('.mdc-chip');
+ const svgs = fixture.nativeElement.querySelectorAll('svg');
+
+ expect(svgs.length).toBe(3);
+ expect(chips[0].classList.contains('mdc-chip--selected')).toBe(false);
+ expect(chips[1].classList.contains('mdc-chip--selected')).toBe(false);
+ chips[0].click();
+ expect(chips[0].classList).toContain('mdc-chip--selected');
+ expect(chips[1].classList.contains('mdc-chip--selected')).toBe(false);
+ chips[1].click();
+ expect(chips[0].classList).toContain('mdc-chip--selected');
+ expect(chips[1].classList).toContain('mdc-chip--selected');
+ chips[0].click();
+ expect(chips[0].classList.contains('mdc-chip--selected')).toBe(false);
+ expect(chips[1].classList).toContain('mdc-chip--selected');
+ }));
+
+ it('filter chips selected value changes on clicks', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'filter';
+ fixture.detectChanges();
+ const chips = fixture.nativeElement.querySelectorAll('.mdc-chip');
+
+ expect(testComponent.valueChanges).toEqual([]);
+ chips[0].click();
+ chips[1].click();
+ chips[0].click();
+ expect(testComponent.valueChanges).toEqual([
+ {chip: 'chippie', value: true},
+ {chip: 'chappie', value: true},
+ {chip: 'chippie', value: false}
+ ]);
+ }));
+
+ it('choice chips selected value changes on clicks and clicks of other choices', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'choice';
+ fixture.detectChanges();
+ const chips = fixture.nativeElement.querySelectorAll('.mdc-chip');
+
+ expect(testComponent.valueChanges).toEqual([]);
+ chips[0].click();
+ expect(testComponent.valueChanges).toEqual([{chip: 'chippie', value: true}]);
+ testComponent.valueChanges = [];
+ chips[1].click();
+ expect(testComponent.valueChanges).toEqual([{chip: 'chippie', value: false}, {chip: 'chappie', value: true}]);
+ testComponent.valueChanges = [];
+ chips[1].click();
+ expect(testComponent.valueChanges).toEqual([{chip: 'chappie', value: false}]);
+ }));
+
+ it('filter/choice chips selected state can be changed', (() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'choice';
+ fixture.detectChanges();
+ const chips = fixture.nativeElement.querySelectorAll('.mdc-chip');
+ const chipComponents = fixture.debugElement.queryAll(By.directive(MdcChipDirective)).map(de => de.injector.get(MdcChipDirective));
+
+ expect(chipComponents[0].selected).toBe(false);
+ chipComponents[0].selected = true;
+ expect(testComponent.valueChanges).toEqual([{chip: 'chippie', value: true}]);
+ testComponent.valueChanges = [];
+
+ chipComponents[1].selected = true;
+ expect(testComponent.valueChanges).toEqual([{chip: 'chippie', value: false}, {chip: 'chappie', value: true}]);
+ expect(chipComponents[0].selected).toBe(false);
+ testComponent.valueChanges = [];
+
+ chipComponents[1].selected = true; // no change shouldn't trigger change events
+ expect(testComponent.valueChanges).toEqual([]);
+ expect(chipComponents[0].selected).toBe(false);
+ expect(chipComponents[1].selected).toBe(true);
+
+ chipComponents[1].selected = false;
+ expect(testComponent.valueChanges).toEqual([{chip: 'chappie', value: false}]);
+ expect(chipComponents[0].selected).toBe(false);
+ expect(chipComponents[1].selected).toBe(false);
+ }));
+
+ it('filter chips hide their leading icon on selection (to make place for the checkmark)', fakeAsync(() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'filter';
+ testComponent.chips = ['chip'];
+ testComponent.includeLeadingIcon = true;
+ fixture.detectChanges();
+ const chip = fixture.nativeElement.querySelector('.mdc-chip');
+ const chipComponent = fixture.debugElement.query(By.directive(MdcChipDirective)).injector.get(MdcChipDirective);
+ const svg = fixture.nativeElement.querySelector('svg');
+ const icon = fixture.nativeElement.querySelector('i');
+
+ expect(icon.classList.contains('mdc-chip__icon--leading-hidden')).toBe(false);
+
+ chip.click();
+ // simulate transitionend event for hide animation of icon:
+ (chipComponent._foundation).handleTransitionEnd_({target: icon, propertyName: 'opacity'});
+ expect(icon.classList.contains('mdc-chip__icon--leading-hidden')).toBe(true);
+
+ chip.click();
+ // simulate transitionend event for hide animation of checkmark:
+ (chipComponent._foundation).handleTransitionEnd_({target: svg.parentElement, propertyName: 'opacity'});
+ expect(icon.classList.contains('mdc-chip__icon--leading-hidden')).toBe(false);
+ }));
+
+ it('computeRippleBoundingRect returns correct values', fakeAsync(() => {
+ const { fixture } = setup();
+ const testComponent = fixture.debugElement.injector.get(TestComponent);
+ testComponent.type = 'filter';
+ testComponent.chips = ['chip'];
+ testComponent.includeLeadingIcon = true;
+ fixture.detectChanges();
+
+ let chipComponent = fixture.debugElement.query(By.directive(MdcChipDirective)).injector.get(MdcChipDirective);
+ let rect = chipComponent['computeRippleBoundingRect']();
+ expect(rect.width).toBeGreaterThan(rect.height);
+
+ testComponent.includeLeadingIcon = false;
+ fixture.detectChanges();
+ chipComponent = fixture.debugElement.query(By.directive(MdcChipDirective)).injector.get(MdcChipDirective);
+ rect = chipComponent['computeRippleBoundingRect']();
+ expect(rect.width).toBe(rect.height);
+ }));
+
+ @Component({
+ template: `
+
+
+
+ {{icon}}
+
+
+ {{icon}}
+
+
{{chip}}
+
+
+ {{icon}}
+
+
+ {{icon}}
+
+
+
+ `
+ })
+ class TestIconsComponent {
+ contained = false;
+ beforeIcons = [];
+ afterIcons = [];
+ }
+ it('leading/trailing icons are detected properly', (() => {
+ const { fixture } = setup(TestIconsComponent);
+ const testComponent = fixture.debugElement.injector.get(TestIconsComponent);
+
+ let icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(0);
+
+ testComponent.afterIcons = ['cancel'];
+ fixture.detectChanges();
+ icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(1);
+ expect(icons[0].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[0].classList.contains('mdc-chip__icon--trailing')).toBe(true);
+ testComponent.contained = true;
+ fixture.detectChanges();
+ icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(1);
+ expect(icons[0].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[0].classList.contains('mdc-chip__icon--trailing')).toBe(true);
+
+ testComponent.beforeIcons = ['event'];
+ testComponent.afterIcons = [];
+ fixture.detectChanges();
+ icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(1);
+ expect(icons[0].classList.contains('mdc-chip__icon--leading')).toBe(true);
+ expect(icons[0].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ testComponent.contained = true;
+ fixture.detectChanges();
+ icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(1);
+ expect(icons[0].classList.contains('mdc-chip__icon--leading')).toBe(true);
+ expect(icons[0].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+
+ testComponent.beforeIcons = ['event', 'event'];
+ testComponent.afterIcons = ['cancel', 'cancel'];
+ testComponent.contained = false;
+ fixture.detectChanges();
+ icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(4);
+ expect(icons[0].classList.contains('mdc-chip__icon--leading')).toBe(true);
+ expect(icons[0].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ expect(icons[1].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[1].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ expect(icons[2].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[2].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ expect(icons[3].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[3].classList.contains('mdc-chip__icon--trailing')).toBe(true);
+
+ testComponent.contained = true;
+ fixture.detectChanges();
+ icons = fixture.nativeElement.querySelectorAll('i');
+ expect(icons.length).toBe(4);
+ expect(icons[0].classList.contains('mdc-chip__icon--leading')).toBe(true);
+ expect(icons[0].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ expect(icons[1].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[1].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ expect(icons[2].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[2].classList.contains('mdc-chip__icon--trailing')).toBe(false);
+ expect(icons[3].classList.contains('mdc-chip__icon--leading')).toBe(false);
+ expect(icons[3].classList.contains('mdc-chip__icon--trailing')).toBe(true);
+ }));
+
+ @Component({
+ template: `
+
+ `
+ })
+ class TestTabbingComponent {
+ }
+ it('chips are tabbable by default, but this can be overridden', (() => {
+ const { fixture } = setup(TestTabbingComponent);
+ let chips = fixture.nativeElement.querySelectorAll('.mdc-chip');
+ expect(chips[0].tabIndex).toBe(0);
+ expect(chips[1].tabIndex).toBe(-1);
+ }));
+});
diff --git a/bundle/src/components/chips/mdc.chip.directive.ts b/bundle/src/components/chips/mdc.chip.directive.ts
new file mode 100644
index 0000000..ba46552
--- /dev/null
+++ b/bundle/src/components/chips/mdc.chip.directive.ts
@@ -0,0 +1,468 @@
+import { AfterContentInit, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, HostBinding, Input,
+ OnDestroy, Optional, Output, QueryList, Renderer2, Self, forwardRef, ChangeDetectorRef } from '@angular/core';
+import { MDCChipFoundation, MDCChipSetFoundation } from '@material/chips';
+import { AbstractMdcRipple } from '../ripple/abstract.mdc.ripple';
+import { MdcEventRegistry } from '../../utils/mdc.event.registry';
+import { asBoolean } from '../../utils/value.utils';
+import { MdcChipAdapter } from './mdc.chip.adapter';
+import { MdcChipSetAdapter } from './mdc.chip-set.adapter';
+import { MdcIconDirective } from '../../material.module';
+import { Subscription } from 'rxjs';
+
+/**
+ * Directive for the (optional) leading or trailing icon of an mdcChip.
+ * Depenending on the position within the mdcChip the icon will determine
+ * whether it is a leading or trailing icon.
+ * Trailing icons must implement the functionality to remove the chip from the set, and
+ * must only be added to input chips (mdcChipSet="input"). Chips with a trailing
+ * icon must implement the remove event.
+ */
+@Directive({
+ selector: '[mdcChipIcon]'
+})
+export class MdcChipIconDirective {
+ @HostBinding('class.mdc-chip__icon') _cls = true;
+ @HostBinding('class.mdc-chip__icon--leading') _leading = false;
+ /**
+ * Event emitted for trailing icon user interactions.
+ */
+ @Output() interact: EventEmitter = new EventEmitter();
+ private __trailing = false;
+ private _oldTabIndex: number;
+ private _oldRole: string;
+
+ constructor(public _elm: ElementRef, private _rndr: Renderer2, public _cdRef: ChangeDetectorRef) {}
+
+ @HostBinding('class.mdc-chip__icon--trailing') get _trailing() {
+ return this.__trailing;
+ }
+
+ set _trailing(val: boolean) {
+ if (val !== this._trailing) {
+ this.__trailing = val;
+ if (this._trailing) {
+ this._oldTabIndex = this._elm.nativeElement.tabIndex;
+ this._elm.nativeElement.tabIndex = 0;
+ this._oldRole = this._elm.nativeElement.getAttribute('role');
+ this._rndr.setAttribute(this._elm.nativeElement, 'role', 'button');
+ } else {
+ if (this._oldTabIndex) {
+ this._elm.nativeElement.tabIndex = this._oldTabIndex;
+ if (this._oldRole)
+ this._rndr.setAttribute(this._elm.nativeElement, 'role', this._oldRole);
+ else
+ this._rndr.removeAttribute(this._elm.nativeElement, 'role');
+ }
+ this._oldTabIndex = null;
+ this._oldRole = null;
+ }
+ }
+ }
+}
+
+/**
+ * Directive for the text of an mdcChip.
+ */
+@Directive({
+ selector: '[mdcChipText]'
+})
+export class MdcChipTextDirective {
+ @HostBinding('class.mdc-chip__text') _cls = true;
+
+ constructor(public _elm: ElementRef) {}
+}
+
+/**
+ * Directive for a chip. Chips must be child elements of an mdcChipSet.
+ */
+@Directive({
+ selector: '[mdcChip]'
+})
+export class MdcChipDirective extends AbstractMdcRipple implements AfterContentInit, OnDestroy {
+ @HostBinding('class.mdc-chip') _cls = true;
+ private initialized = false;
+ private selectedMem = false;
+ /**
+ * Event emitted for user interaction with the chip.
+ */
+ @Output() interact: EventEmitter = new EventEmitter();
+ /**
+ * Event emitted when the user has removed (by clicking the trailing icon) the chip.
+ * This event must be implemented when the chip has a trailing icon, and the implementation
+ * must remove the chip from the set. Without such implementation the directive will
+ * animate the chip out of vision, but will not remove the chip from the DOM.
+ */
+ @Output() remove: EventEmitter = new EventEmitter();
+ /**
+ * Event emitted when the chip changes from not-selected to selected state or vice versa
+ * (for filter and choice chips).
+ */
+ @Output() selectedChange: EventEmitter = new EventEmitter();
+ _set: MdcChipSetDirective;
+ private _checkmark: HTMLElement;
+ private _leadingIcon: MdcChipIconDirective;
+ private _trailingIcon: MdcChipIconDirective;
+ @ContentChildren(MdcChipIconDirective, {descendants: true}) _icons: QueryList;
+ @ContentChildren(MdcChipTextDirective, {descendants: true}) _texts: QueryList;
+ private _adapter: MdcChipAdapter = {
+ addClass: (className: string) => {
+ let selectedChanged = className === 'mdc-chip--selected' && !this._elm.nativeElement.classList.contains(className);
+ this._renderer.addClass(this._elm.nativeElement, className);
+ if (this.selectedChange)
+ this.selectedChange.emit(true);
+ },
+ removeClass: (className: string) => {
+ let selectedChanged = className === 'mdc-chip--selected' && this._elm.nativeElement.classList.contains(className);
+ this._renderer.removeClass(this._elm.nativeElement, className);
+ if (this.selectedChange)
+ this.selectedChange.emit(false);
+ },
+ hasClass: (className) => this._elm.nativeElement.classList.contains(className),
+ addClassToLeadingIcon: (className: string) => {
+ let icon = this._leadingIcon;
+ if (icon)
+ this._renderer.addClass(icon._elm.nativeElement, className);
+ },
+ removeClassFromLeadingIcon: (className: string) => {
+ let icon = this._leadingIcon;
+ if (icon)
+ this._renderer.removeClass(icon._elm.nativeElement, className);
+ },
+ eventTargetHasClass: (target: HTMLElement, className: string) => target.classList.contains(className),
+ registerEventHandler: (evt: string, handler: EventListener) => this._registry.listen(this._renderer, evt, handler, this._elm),
+ deregisterEventHandler: (evt: string, handler: EventListener) => this._registry.unlisten(evt, handler),
+ registerTrailingIconInteractionHandler: (evt: string, handler: EventListener) => {
+ let icon = this._trailingIcon;
+ if (icon)
+ this._registry.listen(this._renderer, evt, handler, icon._elm);
+ },
+ deregisterTrailingIconInteractionHandler: (evt: string, handler: EventListener) => this._registry.unlisten(evt, handler),
+ notifyInteraction: () => this.interact.emit(),
+ notifyTrailingIconInteraction: () => this._trailingIcon.interact.emit(),
+ notifyRemoval: () => this.remove.emit(),
+ getComputedStyleValue: (propertyName: string) => getComputedStyle(this._elm.nativeElement).getPropertyValue(propertyName),
+ setStyleProperty: (style: string, value: string) => this._renderer.setStyle(this._elm.nativeElement, style, value)
+ };
+ _foundation: {
+ init: Function,
+ destroy: Function,
+ isSelected: () => boolean,
+ setSelected: (selected: boolean) => void
+ } = new MDCChipFoundation(this._adapter);
+
+ constructor(private _elm: ElementRef, rndr: Renderer2, registry: MdcEventRegistry) {
+ super(_elm, rndr, registry);
+ }
+
+ ngAfterContentInit() {
+ if (!this._elm.nativeElement.hasAttribute('tabindex'))
+ // unless overridden, make the chip tabbable:
+ this._elm.nativeElement.tabIndex = 0;
+ this.initIcons();
+ this._icons.changes.subscribe(() => {
+ this._reInit();
+ });
+ this._texts.changes.subscribe(() => {
+ this._reInit();
+ });
+ this.initRipple();
+ this.initCheckMark();
+ this._foundation.init();
+ if (this.selectedMem)
+ // triggers setting the foundation selected state (and possibly for other [choice] chips too):
+ this.selected = this.selectedMem;
+ this.initialized = true;
+ }
+
+ ngOnDestroy() {
+ this.destroyRipple();
+ this._foundation.destroy();
+ }
+
+ _reInit() {
+ // if icons have changed, the foundation must be reinitialized, because
+ // trailingIconInteractionHandler might have to be removed and/or attached
+ // to another icon:
+ this._foundation.destroy();
+ this._foundation = new MDCChipFoundation(this._adapter);
+ this.initIcons();
+ this.initCheckMark();
+ this._foundation.init();
+ // no need to call setSelected again, as the previous state will still be available via
+ // the mdc-chip--selected class
+ }
+
+ private initIcons() {
+ let newLeading = this.computeLeadingIcon();
+ let newTrailing = this.computeTrailingIcon(newLeading);
+ if (newLeading !== this._leadingIcon || newTrailing !== this._trailingIcon) {
+ this._leadingIcon = newLeading;
+ this._trailingIcon = newTrailing;
+ this._icons.forEach(icon => {
+ let leading = icon === this._leadingIcon;
+ let trailing = icon === this._trailingIcon;
+ let changed = leading !== icon._leading || trailing !== icon._trailing;
+ icon._leading = icon === this._leadingIcon;
+ icon._trailing = icon === this._trailingIcon;
+ if (changed) // if we don't do this we get ExpressionChangedAfterItHasBeenCheckedError:
+ icon._cdRef.detectChanges();
+ });
+ this.removeCheckMark(); // checkmark may have changed position, will be readded later (for filter chips)
+ }
+ }
+
+ private computeLeadingIcon() {
+ if (this._icons.length > 0) {
+ let icon = this._icons.first;
+ let prev = this.previousElement(icon._elm.nativeElement);
+ let last = icon._elm.nativeElement;
+ while (true) {
+ // if it is contained in another element, check the siblings of the container too:
+ if (prev == null && last != null && last.parentElement !== this._elm.nativeElement)
+ prev = last.parentElement;
+ // no more elements before, must be the leading icon:
+ if (prev == null)
+ return icon;
+ // comes after the text, so it's not the leading icon:
+ if (this._text && (prev === this._text._elm.nativeElement || prev.contains(this._text._elm.nativeElement)))
+ return null;
+ last = prev;
+ prev = this.previousElement(prev);
+ }
+ }
+ return null;
+ }
+
+ private get _text() {
+ return this._texts.first;
+ }
+
+ private computeTrailingIcon(leading: MdcChipIconDirective) {
+ if (this._icons.length > 0) {
+ let icon = this._icons.last;
+ if (icon === leading)
+ return null;
+ // if not the leading icon, it must be the trailing icon:
+ return icon;
+ }
+ return null;
+ }
+
+ private previousElement(el: Element): Element {
+ let result = el.previousSibling;
+ while (result != null && !(result instanceof Element))
+ result = result.previousSibling;
+ return result;
+ }
+
+ private initCheckMark() {
+ if (this._set && this._set._type === 'filter')
+ this.addCheckMark();
+ else
+ this.removeCheckMark();
+ }
+
+ private addCheckMark() {
+ if (!this._checkmark) {
+ let path = this._renderer.createElement('path', 'svg');
+ this._renderer.addClass(path, 'mdc-chip__checkmark-path');
+ this._renderer.setAttribute(path, 'fill', 'none');
+ this._renderer.setAttribute(path, 'stroke', 'black');
+ this._renderer.setAttribute(path, 'd', 'M1.73,12.91 8.1,19.28 22.79,4.59');
+ let svg = this._renderer.createElement('svg', 'svg');
+ this._renderer.appendChild(svg, path);
+ this._renderer.addClass(svg, 'mdc-chip__checkmark-svg');
+ this._renderer.setAttribute(svg, 'viewBox', '-2 -3 30 30');
+ this._checkmark = this._renderer.createElement('div');
+ this._renderer.appendChild(this._checkmark, svg);
+ this._renderer.addClass(this._checkmark, 'mdc-chip__checkmark');
+ let beforeElement = this._text ? this._text._elm.nativeElement : null;
+ if (beforeElement)
+ // checkmark should go after leading icon:
+ this._renderer.insertBefore(this._elm.nativeElement, this._checkmark, beforeElement);
+ else
+ this._renderer.appendChild(this._elm.nativeElement, this._checkmark);
+ }
+ }
+
+ private removeCheckMark() {
+ if (this._checkmark) {
+ this._checkmark.parentElement.removeChild(this._checkmark);
+ this._checkmark = null;
+ }
+ }
+
+ /**
+ * The 'selected' state of the chip. Filter and choice chips are either selected or
+ * not selected. Making a choice chip selected, will make all other chips in that set
+ * not selected.
+ */
+ @Input() get selected(): any {
+ return this.initialized ? this._foundation.isSelected() : this.selectedMem;
+ }
+
+ set selected(val: any) {
+ let value = asBoolean(val);
+ this.selectedMem = value;
+ if (this.initialized && value !== this._foundation.isSelected()) {
+ if (value && this._set)
+ // this will also trigger deselection of other values for choice chips
+ this._set._foundation.select(this._foundation);
+ else
+ this._foundation.setSelected(val);
+ }
+ // when not initialized the selectedChange will be emitted via the foundation after
+ // initialization
+ }
+
+ /** @docs-private */
+ protected computeRippleBoundingRect() {
+ if (this._checkmark && !this._leadingIcon) {
+ const height = this._rippleElm.nativeElement.getBoundingClientRect().height;
+ // https://github.com/material-components/material-components-web/blob/cb373ad34857070734a7c02bf59116e21853842a/packages/mdc-chips/chip/index.js#L60:
+ // the checkmark should be square, but initiallly the width is set to 0
+ return { height: height, width: height };
+ }
+ return super.computeRippleBoundingRect();
+ }
+}
+
+/**
+ * Directive for a chip-set (a collection of mdcChip).
+ */
+@Directive({
+ selector: '[mdcChipSet]'
+})
+export class MdcChipSetDirective implements AfterContentInit, OnDestroy {
+ @HostBinding('class.mdc-chip-set') _cls = true;
+ @ContentChildren(MdcChipDirective, {descendants: true}) _chips: QueryList;
+ private _interactSubscriptions: Subscription[];
+ private _removeSubscriptions: Subscription[];
+ private _initialized = false;
+ private _interactionHandler: EventListener;
+ _type: 'choice' | 'filter' | 'input' | 'action' = 'action';
+ _adapter: MdcChipSetAdapter = {
+ hasClass: (className: string) => this._elm.nativeElement.classList.contains(className),
+ registerInteractionHandler: (evt: string, handler: EventListener) => {
+ if (evt === 'MDCChip:interaction')
+ this._interactionHandler = handler;
+ // 'MDCChip:removal' is not really used, we call deselect/removeChip directly on the angular eventemitter subscription
+ else
+ this._registry.listen(this._rndr, evt, handler, this._elm);
+ },
+ deregisterInteractionHandler: (evt: string, handler: EventListener) => {
+ if (evt === 'MDCChip:interaction')
+ this._interactionHandler = null;
+ else
+ this._registry.unlisten(evt, handler);
+ },
+ appendChip: (text: string, leadingIcon: Element, trailingIcon: Element) => {
+ throw new Error('this adapter method should be unreachable in the MdcChipSetDirective implementation');
+ },
+ removeChip: (chip: any) => {
+ throw new Error('this adapter method should be unreachable in the MdcChipSetDirective implementation');
+ }
+ };
+ _foundation: {
+ init: Function,
+ destroy: Function,
+ select: (chip: MDCChipFoundation) => void,
+ deselect: (chip: MDCChipFoundation) => void
+ } = new MDCChipSetFoundation(this._adapter);
+
+ constructor(private _elm: ElementRef, private _rndr: Renderer2, private _registry: MdcEventRegistry) {}
+
+ ngAfterContentInit() {
+ this._chips.changes.subscribe(() => {
+ this.initSubscriptions();
+ this.initChips();
+ });
+ this.initSubscriptions();
+ this.initChips();
+ this._foundation.init();
+ this._initialized = true;
+ }
+
+ ngOnDestroy() {
+ this._foundation.destroy();
+ this.destroySubscriptions();
+ this._initialized = false;
+ }
+
+ /**
+ * Chip sets by default contain 'action' chips. Set this value to choice,
+ * filter, input, or action to determine the kind
+ * of chips that are contained in the chip set.
+ */
+ @Input() get mdcChipSet() {
+ return this._type;
+ }
+
+ set mdcChipSet(value: any) {
+ if (value !== this._type) {
+ if (value === 'choice' || value === 'filter' || value ==='input')
+ this._type = value;
+ else
+ this._type = 'action';
+ if (this._initialized)
+ this.initChips(true);
+ }
+ }
+
+ private initChips(force = false) {
+ this._chips.forEach(chip => {
+ if (force || chip._set !== this) {
+ chip._set = this;
+ chip._reInit();
+ }
+ });
+ }
+
+ private destroySubscriptions() {
+ try {
+ if (this._interactSubscriptions)
+ this._interactSubscriptions.forEach(sub => sub.unsubscribe());
+ if (this._removeSubscriptions)
+ this._removeSubscriptions.forEach(sub => sub.unsubscribe());
+ } finally {
+ this._interactSubscriptions = null;
+ this._removeSubscriptions = null;
+ }
+ }
+
+ private initSubscriptions() {
+ this.destroySubscriptions();
+ this._interactSubscriptions = [];
+ this._removeSubscriptions = [];
+ this._chips.forEach(chip => {
+ this._interactSubscriptions.push(chip.interact.subscribe(event => {
+ // using the interactionHandler that was registered by the foundation:
+ if (this._interactionHandler)
+ this._interactionHandler({
+ detail: { chip: { foundation: chip._foundation }}
+ });
+ }));
+ this._removeSubscriptions.push(chip.remove.subscribe(event => {
+ // directly handling, bypassing event handlers:
+ this._foundation.deselect(chip._foundation);
+ // don't call this._adapter.removeChip(chip);
+ }));
+ });
+ }
+
+ @HostBinding('class.mdc-chip-set--choice') get _choice() {
+ return this._type === 'choice';
+ }
+
+ @HostBinding('class.mdc-chip-set--filter') get _filter() {
+ return this._type === 'filter';
+ }
+
+ @HostBinding('class.mdc-chip-set--input') get _input() {
+ return this._type === 'input';
+ }
+}
+
+export const CHIP_DIRECTIVES = [
+ MdcChipIconDirective, MdcChipTextDirective, MdcChipDirective, MdcChipSetDirective
+];
diff --git a/bundle/src/material.module.ts b/bundle/src/material.module.ts
index 6f133ca..ced33d1 100644
--- a/bundle/src/material.module.ts
+++ b/bundle/src/material.module.ts
@@ -10,6 +10,7 @@ import { MdcCardDirective,
MdcCardPrimaryActionDirective } from './components/card/mdc.card.directive';
import { MdcCheckboxDirective,
MdcCheckboxInputDirective } from './components/checkbox/mdc.checkbox.directive';
+import { CHIP_DIRECTIVES } from './components/chips/mdc.chip.directive';
import { DIALOG_DIRECTIVES } from './components/dialog/mdc.dialog.directive';
import { MdcDrawerDirective,
MdcDrawerContainerDirective,
@@ -84,6 +85,7 @@ export { MdcCardDirective,
MdcCardPrimaryActionDirective } from './components/card/mdc.card.directive';
export { MdcCheckboxDirective,
MdcCheckboxInputDirective } from './components/checkbox/mdc.checkbox.directive';
+export * from './components/chips/mdc.chip.directive';
export * from './components/dialog/mdc.dialog.directive';
export { MdcDrawerDirective,
MdcDrawerContainerDirective,
@@ -160,6 +162,7 @@ export { MdcEventRegistry } from './utils/mdc.event.registry';
MdcCardDirective, MdcCardMediaDirective, MdcCardMediaContentDirective,
MdcCardActionButtonsDirective, MdcCardActionIconsDirective, MdcCardActionsDirective, MdcCardPrimaryActionDirective,
MdcCheckboxDirective, MdcCheckboxInputDirective,
+ ...CHIP_DIRECTIVES,
...DIALOG_DIRECTIVES,
MdcDrawerDirective, MdcDrawerContainerDirective, MdcDrawerToolbarSpacerDirective, MdcDrawerHeaderDirective, MdcDrawerHeaderContentDirective, MdcDrawerContentDirective,
MdcElevationDirective,
@@ -191,6 +194,7 @@ export { MdcEventRegistry } from './utils/mdc.event.registry';
MdcCardDirective, MdcCardMediaDirective, MdcCardMediaContentDirective,
MdcCardActionButtonsDirective, MdcCardActionIconsDirective, MdcCardActionsDirective, MdcCardPrimaryActionDirective,
MdcCheckboxDirective, MdcCheckboxInputDirective,
+ ...CHIP_DIRECTIVES,
...DIALOG_DIRECTIVES,
MdcDrawerDirective, MdcDrawerContainerDirective, MdcDrawerToolbarSpacerDirective, MdcDrawerHeaderDirective, MdcDrawerHeaderContentDirective, MdcDrawerContentDirective,
MdcElevationDirective,
diff --git a/bundle/tools/ngbundler/index.ts b/bundle/tools/ngbundler/index.ts
index 157e067..cb11772 100644
--- a/bundle/tools/ngbundler/index.ts
+++ b/bundle/tools/ngbundler/index.ts
@@ -9,6 +9,7 @@ const globals = {
'@angular/router': 'ng.router',
'@material/animation': 'mdc.animation',
'@material/checkbox': 'mdc.checkbox',
+ '@material/chips': 'mdc.chips',
'@material/dialog': 'mdc.dialog',
'@material/drawer': 'mdc.drawer',
'@material/drawer/slidable': 'mdc.drawer',
diff --git a/site/src/app/app.module.ts b/site/src/app/app.module.ts
index e66641e..8acbaad 100644
--- a/site/src/app/app.module.ts
+++ b/site/src/app/app.module.ts
@@ -24,6 +24,7 @@ import {
SnippetButtonComponent,
SnippetCardComponent,
SnippetCheckboxComponent,
+ SnippetChipsComponent, SnippetChipsChoiceComponent, SnippetChipsFilterComponent, SnippetChipsInputComponent,
SnippetDialogComponent,
SnippetDrawerPermanentBelowComponent,
SnippetDrawerPermanentComponent,
@@ -77,6 +78,7 @@ import { ThemeService } from './services';
SnippetButtonComponent,
SnippetCardComponent,
SnippetCheckboxComponent,
+ SnippetChipsComponent, SnippetChipsChoiceComponent, SnippetChipsFilterComponent, SnippetChipsInputComponent,
SnippetDialogComponent,
SnippetDrawerPermanentBelowComponent,
SnippetDrawerPermanentComponent,
diff --git a/site/src/app/app.routes.ts b/site/src/app/app.routes.ts
index 20cf37c..bb75e00 100644
--- a/site/src/app/app.routes.ts
+++ b/site/src/app/app.routes.ts
@@ -12,6 +12,7 @@ import {
ButtonDirectivesComponent,
CardDirectivesComponent,
CheckboxDirectivesComponent,
+ ChipsDirectivesComponent,
DialogDirectivesComponent,
DrawerDirectivesComponent,
ElevationDirectivesComponent,
@@ -45,6 +46,7 @@ export const routes: Routes = [
{path: ButtonDirectivesComponent.DOC_HREF, component: ButtonDirectivesComponent},
{path: CardDirectivesComponent.DOC_HREF, component: CardDirectivesComponent},
{path: CheckboxDirectivesComponent.DOC_HREF, component: CheckboxDirectivesComponent},
+ {path: ChipsDirectivesComponent.DOC_HREF, component: ChipsDirectivesComponent},
{path: DialogDirectivesComponent.DOC_HREF, component: DialogDirectivesComponent},
{path: DrawerDirectivesComponent.DOC_HREF, component: DrawerDirectivesComponent},
{path: ElevationDirectivesComponent.DOC_HREF, component: ElevationDirectivesComponent},
diff --git a/site/src/app/components/directives.demo/chips.directives.component.html b/site/src/app/components/directives.demo/chips.directives.component.html
new file mode 100644
index 0000000..829d9da
--- /dev/null
+++ b/site/src/app/components/directives.demo/chips.directives.component.html
@@ -0,0 +1,24 @@
+Chips
+Chips are compact elements that allow users to enter information, select a choice, filter content, or trigger an action.
+
+
+ Action Chips Demo
+
+
+
+
+ Choice Chips Demo
+
+
+
+
+ Filter Chips Demo
+
+
+
+
+ Input Chips Demo
+
+
+
+${require('@blox/material/apidocs/chips.html')}
\ No newline at end of file
diff --git a/site/src/app/components/directives.demo/chips.directives.component.ts b/site/src/app/components/directives.demo/chips.directives.component.ts
new file mode 100644
index 0000000..2ae5581
--- /dev/null
+++ b/site/src/app/components/directives.demo/chips.directives.component.ts
@@ -0,0 +1,14 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'blox-chips-directives',
+ templateUrl: './chips.directives.component.html'
+})
+export class ChipsDirectivesComponent {
+ static DOC_SVG = require('assets/img/mdc-icons/chip.svg');
+ static DOC_TYPE = 'components';
+ static DOC_HREF = 'chips';
+
+ constructor() {
+ }
+}
diff --git a/site/src/app/components/directives.demo/index.ts b/site/src/app/components/directives.demo/index.ts
index a596118..f5639dc 100644
--- a/site/src/app/components/directives.demo/index.ts
+++ b/site/src/app/components/directives.demo/index.ts
@@ -1,6 +1,7 @@
import { ButtonDirectivesComponent } from './button.directives.component';
import { CardDirectivesComponent } from './card.directives.component';
import { CheckboxDirectivesComponent } from './checkbox.directives.component';
+import { ChipsDirectivesComponent } from './chips.directives.component';
import { DialogDirectivesComponent } from './dialog.directives.component';
import { DrawerDirectivesComponent } from './drawer.directives.component';
import { ElevationDirectivesComponent } from './elevation.directives.component';
@@ -24,6 +25,7 @@ import { UtilityDirectivesComponent } from './utility.directives.component';
export * from './button.directives.component';
export * from './card.directives.component';
export * from './checkbox.directives.component';
+export * from './chips.directives.component';
export * from './dialog.directives.component';
export * from './drawer.directives.component';
export * from './elevation.directives.component';
@@ -48,6 +50,7 @@ export const MDC_DIRECTIVE_DOC_COMPONENTS = [
ButtonDirectivesComponent,
CardDirectivesComponent,
CheckboxDirectivesComponent,
+ ChipsDirectivesComponent,
DialogDirectivesComponent,
DrawerDirectivesComponent,
ElevationDirectivesComponent,
diff --git a/site/src/app/components/snippets/directives/index.ts b/site/src/app/components/snippets/directives/index.ts
index 733fded..6088916 100644
--- a/site/src/app/components/snippets/directives/index.ts
+++ b/site/src/app/components/snippets/directives/index.ts
@@ -1,6 +1,10 @@
export * from './snippet.button.component';
export * from './snippet.card.component';
export * from './snippet.checkbox.component';
+export * from './snippet.chips.component';
+export * from './snippet.chips.choice.component';
+export * from './snippet.chips.filter.component';
+export * from './snippet.chips.input.component';
export * from './snippet.dialog.component';
export * from './snippet.drawer.permanent.below.component';
export * from './snippet.drawer.permanent.component';
diff --git a/site/src/app/components/snippets/directives/snippet.chips.choice.component.html b/site/src/app/components/snippets/directives/snippet.chips.choice.component.html
new file mode 100644
index 0000000..479ac78
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.choice.component.html
@@ -0,0 +1,9 @@
+
diff --git a/site/src/app/components/snippets/directives/snippet.chips.choice.component.ts b/site/src/app/components/snippets/directives/snippet.chips.choice.component.ts
new file mode 100644
index 0000000..d705def
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.choice.component.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+//snip:skip
+import { forwardRef } from '@angular/core';
+import { AbstractSnippetComponent } from '../abstract.snippet.component';
+//snip:endskip
+@Component({
+ //snip:skip
+ providers: [{provide: AbstractSnippetComponent, useExisting: forwardRef(() => SnippetChipsChoiceComponent)}],
+ //snip:endskip
+ selector: 'blox-snippet-chips-choice',
+ templateUrl: './snippet.chips.choice.component.html'
+})
+export class SnippetChipsChoiceComponent/*snip:skip*/extends AbstractSnippetComponent/*snip:endskip*/ {
+ //snip:skip
+ constructor() {
+ super({
+ 'html': require('!raw-loader!./snippet.chips.choice.component.html'),
+ 'typescript': require('!raw-loader!./snippet.chips.choice.component.ts')
+ });
+ }
+ //snip:endskip
+}
diff --git a/site/src/app/components/snippets/directives/snippet.chips.component.html b/site/src/app/components/snippets/directives/snippet.chips.component.html
new file mode 100644
index 0000000..53c8a18
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.component.html
@@ -0,0 +1,20 @@
+
diff --git a/site/src/app/components/snippets/directives/snippet.chips.component.ts b/site/src/app/components/snippets/directives/snippet.chips.component.ts
new file mode 100644
index 0000000..f1bbf8e
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.component.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+//snip:skip
+import { forwardRef } from '@angular/core';
+import { AbstractSnippetComponent } from '../abstract.snippet.component';
+//snip:endskip
+@Component({
+ //snip:skip
+ providers: [{provide: AbstractSnippetComponent, useExisting: forwardRef(() => SnippetChipsComponent)}],
+ //snip:endskip
+ selector: 'blox-snippet-chips',
+ templateUrl: './snippet.chips.component.html'
+})
+export class SnippetChipsComponent/*snip:skip*/extends AbstractSnippetComponent/*snip:endskip*/ {
+ //snip:skip
+ constructor() {
+ super({
+ 'html': require('!raw-loader!./snippet.chips.component.html'),
+ 'typescript': require('!raw-loader!./snippet.chips.component.ts')
+ });
+ }
+ //snip:endskip
+}
diff --git a/site/src/app/components/snippets/directives/snippet.chips.filter.component.html b/site/src/app/components/snippets/directives/snippet.chips.filter.component.html
new file mode 100644
index 0000000..50e79d4
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.filter.component.html
@@ -0,0 +1,20 @@
+
diff --git a/site/src/app/components/snippets/directives/snippet.chips.filter.component.ts b/site/src/app/components/snippets/directives/snippet.chips.filter.component.ts
new file mode 100644
index 0000000..c97ce64
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.filter.component.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+//snip:skip
+import { forwardRef } from '@angular/core';
+import { AbstractSnippetComponent } from '../abstract.snippet.component';
+//snip:endskip
+@Component({
+ //snip:skip
+ providers: [{provide: AbstractSnippetComponent, useExisting: forwardRef(() => SnippetChipsFilterComponent)}],
+ //snip:endskip
+ selector: 'blox-snippet-chips-filter',
+ templateUrl: './snippet.chips.filter.component.html'
+})
+export class SnippetChipsFilterComponent/*snip:skip*/extends AbstractSnippetComponent/*snip:endskip*/ {
+ //snip:skip
+ constructor() {
+ super({
+ 'html': require('!raw-loader!./snippet.chips.filter.component.html'),
+ 'typescript': require('!raw-loader!./snippet.chips.filter.component.ts')
+ });
+ }
+ //snip:endskip
+}
diff --git a/site/src/app/components/snippets/directives/snippet.chips.input.component.html b/site/src/app/components/snippets/directives/snippet.chips.input.component.html
new file mode 100644
index 0000000..2b5238c
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.input.component.html
@@ -0,0 +1,16 @@
+
diff --git a/site/src/app/components/snippets/directives/snippet.chips.input.component.ts b/site/src/app/components/snippets/directives/snippet.chips.input.component.ts
new file mode 100644
index 0000000..ecfc209
--- /dev/null
+++ b/site/src/app/components/snippets/directives/snippet.chips.input.component.ts
@@ -0,0 +1,38 @@
+import { Component } from '@angular/core';
+//snip:skip
+import { forwardRef } from '@angular/core';
+import { AbstractSnippetComponent } from '../abstract.snippet.component';
+//snip:endskip
+@Component({
+ //snip:skip
+ providers: [{provide: AbstractSnippetComponent, useExisting: forwardRef(() => SnippetChipsInputComponent)}],
+ //snip:endskip
+ selector: 'blox-snippet-chips-input',
+ templateUrl: './snippet.chips.input.component.html'
+})
+export class SnippetChipsInputComponent/*snip:skip*/extends AbstractSnippetComponent/*snip:endskip*/ {
+ chips = [ 'claire', 'pete', 'anne' ];
+ newChip: string;
+
+ //snip:skip
+ constructor() {
+ super({
+ 'html': require('!raw-loader!./snippet.chips.input.component.html'),
+ 'typescript': require('!raw-loader!./snippet.chips.input.component.ts')
+ });
+ }
+ //snip:endskip
+
+ addChip() {
+ if (this.newChip) {
+ let value = this.newChip.trim();
+ if (value.length)
+ this.chips.push(value);
+ }
+ this.newChip = null;
+ }
+
+ removeChip(index: number) {
+ this.chips.splice(index, 1);
+ }
+}
diff --git a/site/src/app/messages.json b/site/src/app/messages.json
index 6a8e646..6489632 100644
--- a/site/src/app/messages.json
+++ b/site/src/app/messages.json
@@ -11,6 +11,9 @@
"components.checkbox.title": "Checkbox",
"components.checkbox.description": "Multi selection controls",
"components.checkbox.meta.description": "Checkbox (mdcCheckbox) component of ${default.meta.description}",
+ "components.chips.title": "Chips",
+ "components.chips.description": "Chips for actions, selection, and input",
+ "components.chips.meta.description": "Chips (mdcChip, mdcChipSet) components of ${default.meta.description}",
"components.dialog.title": "Dialog",
"components.dialog.description": "Modal Dialogs",
"components.dialog.meta.description": "Dialog (mdcDialog) component of ${default.meta.description}",
diff --git a/site/src/style/_mdc.scss b/site/src/style/_mdc.scss
index 16ad329..831cf2a 100644
--- a/site/src/style/_mdc.scss
+++ b/site/src/style/_mdc.scss
@@ -1,20 +1,26 @@
@import "@material/button/mdc-button";
@import "@material/card/mdc-card";
@import "@material/checkbox/mdc-checkbox";
+@import "@material/chips/mdc-chips";
@import "@material/dialog/mdc-dialog";
@import "@material/drawer/mdc-drawer";
@import "@material/elevation/mdc-elevation";
@import "@material/fab/mdc-fab";
+@import "@material/floating-label/mdc-floating-label";
@import "@material/form-field/mdc-form-field";
//@import "@material/grid-list/mdc-grid-list";
@import "@material/icon-toggle/mdc-icon-toggle";
+//@import "@material/image-list/mdc-image-list";
//@import "@material/layout-grid/mdc-layout-grid";
+@import "@material/line-ripple/mdc-line-ripple";
@import "@material/linear-progress/mdc-linear-progress";
@import "@material/list/mdc-list";
@import "@material/menu/mdc-menu";
+@import "@material/notched-outline/mdc-notched-outline";
@import "@material/radio/mdc-radio";
@import "@material/ripple/mdc-ripple";
@import "@material/select/mdc-select";
+@import "@material/shape/mdc-shape";
@import "@material/slider/mdc-slider";
@import "@material/snackbar/mdc-snackbar";
@import "@material/switch/mdc-switch";
@@ -22,4 +28,11 @@
@import "@material/textfield/mdc-text-field";
@import "@material/theme/mdc-theme";
@import "@material/toolbar/mdc-toolbar";
+//@import "@material/top-app-bar/mdc-top-app-bar";
@import "@material/typography/mdc-typography";
+
+.mdc-chip.mdc-chip--selected {
+ // our theme color defaults otherwise have almost no contrast between
+ // selected/unselected state:
+ @include mdc-theme-prop(background-color, secondary);
+}