-
-
Save Dozorengel/d9a1a2e49cf3832eeb4cddd2d1484810 to your computer and use it in GitHub Desktop.
Angular Material chip list with autocomplete, multiple selection, drag&drop, async fetching data from api (Angular)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<mat-form-field class="w-full" appearance="outline"> | |
<mat-label>{{ label }}</mat-label> | |
<mat-chip-list | |
#chipList | |
aria-label="Item selection" | |
cdkDropList | |
cdkDropListOrientation="horizontal" | |
(cdkDropListDropped)="drop($event)" | |
[cdkDropListDisabled]="!draggable" | |
> | |
<mat-chip *ngFor="let item of items" [removable]="true" (removed)="remove(item)" cdkDrag> | |
{{ item.title }} | |
<mat-icon matChipRemove>cancel</mat-icon> | |
</mat-chip> | |
<input | |
#itemInput | |
[disabled]="!multiple && items.length > 0" | |
[placeholder]="!multiple && items.length > 0 ? '' : placeholder" | |
[formControl]="itemCtrl" | |
[matAutocomplete]="auto" | |
[matChipInputFor]="chipList" | |
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" | |
(matChipInputTokenEnd)="enter($event)" | |
/> | |
</mat-chip-list> | |
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="select($event)" (opened)="scroll()"> | |
<mat-option *ngFor="let item of autocompleteItems" [value]="item"> | |
{{ item.title }} | |
</mat-option> | |
</mat-autocomplete> | |
</mat-form-field> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Component, OnInit, ElementRef, ViewChild, Input, forwardRef} from '@angular/core'; | |
import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; | |
import {ENTER} from '@angular/cdk/keycodes'; | |
import {FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; | |
import {MatAutocompleteSelectedEvent, MatAutocomplete, MatAutocompleteTrigger} from '@angular/material/autocomplete'; | |
import {MatChipInputEvent} from '@angular/material/chips'; | |
import {fromEvent, Observable} from 'rxjs'; | |
import {debounceTime, distinctUntilChanged, switchMap, takeUntil, takeWhile} from 'rxjs/operators'; | |
export interface DatalistItem<T> { | |
title: string; | |
meta: T; | |
} | |
@Component({ | |
selector: 'app-datalist', | |
templateUrl: './datalist.component.html', | |
styleUrls: ['./datalist.component.scss'], | |
providers: [ | |
{ | |
provide: NG_VALUE_ACCESSOR, | |
useExisting: forwardRef(() => DatalistComponent), | |
multi: true, | |
}, | |
], | |
}) | |
export class DatalistComponent implements OnInit, ControlValueAccessor { | |
@ViewChild('itemInput') itemInput: ElementRef<HTMLInputElement>; | |
@ViewChild('auto') matAutocomplete: MatAutocomplete; | |
@ViewChild(MatAutocompleteTrigger) matAutocompleteTrigger: MatAutocompleteTrigger; | |
@Input() label = ''; | |
@Input() placeholder = ''; | |
@Input() multiple = false; | |
@Input() draggable = false; | |
@Input() search: (title?: string, reset?: boolean) => Observable<DatalistItem<any>[]>; | |
separatorKeysCodes: number[] = [ENTER]; | |
itemCtrl = new FormControl(); | |
items: DatalistItem<any>[] = []; | |
autocompleteItems: DatalistItem<any>[] = []; | |
private onChange = (value: any) => {}; | |
constructor() {} | |
ngOnInit(): void { | |
this.getData(); | |
this.itemCtrl.valueChanges | |
.pipe(debounceTime(2000), distinctUntilChanged()) | |
.subscribe((value: DatalistItem<any> | string) => { | |
if (value === null) return; | |
// @ts-ignore | |
this.getData(value.title || value, true); | |
}); | |
} | |
writeValue(items: DatalistItem<any>[]): void { | |
this.items = items; | |
} | |
registerOnChange(fn: any): void { | |
this.onChange = fn; | |
} | |
registerOnTouched(fn: any): void {} | |
drop(event: CdkDragDrop<DatalistItem<any>[]>) { | |
moveItemInArray(this.items, event.previousIndex, event.currentIndex); | |
this.onChange(this.items); | |
} | |
enter(event: MatChipInputEvent): void { | |
const {value} = event; | |
this.autocompleteItems.find((item) => { | |
if (item.title === value.trim()) { | |
this.items.push(item); | |
this.onChange(this.items); | |
this.itemInput.nativeElement.value = ''; | |
this.itemCtrl.setValue(null); | |
if (this.multiple) { | |
this.getData('', true); | |
} else { | |
this.matAutocompleteTrigger.closePanel(); | |
} | |
} | |
}); | |
} | |
select(event: MatAutocompleteSelectedEvent): void { | |
this.items.push(event.option.value); | |
this.onChange(this.items); | |
this.itemInput.nativeElement.value = ''; | |
this.itemCtrl.setValue(null); | |
if (this.multiple) { | |
this.getData('', true); | |
setTimeout(() => { | |
this.matAutocompleteTrigger.openPanel(); | |
}); | |
} | |
} | |
remove(item: DatalistItem<any>): void { | |
this.items = this.items.filter((it) => it !== item); | |
this.onChange(this.items); | |
if (this.multiple) { | |
this.autocompleteItems.unshift(item); | |
} else { | |
this.getData('', true); | |
} | |
} | |
scroll(): void { | |
// settimeout is necessary as panel doesn't init immediately after event emits | |
setTimeout(() => { | |
const panel = this.matAutocomplete.panel.nativeElement; | |
fromEvent(panel, 'scroll') | |
.pipe( | |
debounceTime(50), | |
takeUntil(this.matAutocompleteTrigger.panelClosingActions), | |
switchMap(() => { | |
const atBottom = panel.scrollHeight <= panel.scrollTop + panel.clientHeight + 50; | |
return atBottom ? this.search(this.itemCtrl.value || '').pipe(takeWhile((arr) => !!arr.length)) : []; | |
}) | |
) | |
.subscribe((items) => { | |
this.autocompleteItems = [...this.autocompleteItems, ...items]; | |
}); | |
}); | |
} | |
private availableItems(newItems: DatalistItem<any>[]): DatalistItem<any>[] { | |
return newItems.filter((newItem) => !this.items.find((item) => item.title === newItem.title)); | |
} | |
private getData(title?: string, reset?: boolean): void { | |
this.search(title, reset).subscribe((items) => { | |
this.autocompleteItems = this.availableItems(items); | |
}); | |
} | |
} |
Author
Dozorengel
commented
Mar 13, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment