import { FocusMonitor } from '@angular/cdk/a11y'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Optional, Output, Self } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatFormFieldControl } from '@angular/material/form-field'; import { Observable, of as observableOf, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, mergeMap, startWith, tap } from 'rxjs/operators'; import { AutoCompleteGroup } from '../auto-complete-group'; import { SingleAutoCompleteConfiguration } from './single-auto-complete-configuration'; import { switchMap } from 'rxjs/internal/operators/switchMap'; @Component({ selector: 'app-single-auto-complete', templateUrl: './single-auto-complete.component.html', styleUrls: ['./single-auto-complete.component.scss'], providers: [{ provide: MatFormFieldControl, useExisting: SingleAutoCompleteComponent }], }) export class SingleAutoCompleteComponent implements OnInit, MatFormFieldControl, ControlValueAccessor, OnDestroy { static nextId = 0; @Input() configuration: SingleAutoCompleteConfiguration; // Selected Option Event @Output() optionSelected: EventEmitter = new EventEmitter(); id = `single-autocomplete-${SingleAutoCompleteComponent.nextId++}`; stateChanges = new Subject(); focused = false; errorState = false; controlType = 'single-autocomplete'; describedBy = ''; _inputValue: string; _inputSubject = new Subject(); loading = false; _items: Observable; _groupedItems: Observable; private requestDelay = 200; //ms private minFilteringChars = 1; private loadDataOnStart = true; separatorKeysCodes: number[] = [ENTER, COMMA]; get empty() { return !this._inputValue || this._inputValue.length === 0; } get shouldLabelFloat() { return this.focused || !this.empty; } @Input() get placeholder() { return this._placeholder; } set placeholder(placeholder) { this._placeholder = placeholder; this.stateChanges.next(); } private _placeholder: string; @Input() get required() { return this._required; } set required(req) { this._required = !!(req); this.stateChanges.next(); } private _required = false; @Input() get disabled() { return this._disabled; } set disabled(dis) { this._disabled = !!(dis); this.stateChanges.next(); } private _disabled = false; @Input() get value(): any | null { return this._selectedValue; } set value(value: any | null) { this._selectedValue = value; this._inputValue = (value && value != "") ? " " : null; this.stateChanges.next(); } private _selectedValue; constructor( private fm: FocusMonitor, private elRef: ElementRef, @Optional() @Self() public ngControl: NgControl) { fm.monitor(elRef.nativeElement, true).subscribe((origin) => { this.focused = !!origin; this.stateChanges.next(); }); if (this.ngControl != null) { // Setting the value accessor directly (instead of using // the providers) to avoid running into a circular import. this.ngControl.valueAccessor = this; } } ngOnInit() { } filter(query: string): Observable { // If loadDataOnStart is enabled and query is empty we return the initial items. if (this.isNullOrEmpty(query) && this.loadDataOnStart) { return this.configuration.initialItems(this.configuration.extraData) || observableOf([]); } else if (query && query.length >= this.minFilteringChars) { if (this.configuration.filterFn) { return this.configuration.filterFn(query, this.configuration.extraData); } else { return this.configuration.initialItems(this.configuration.extraData) || observableOf([]); } } else { return observableOf([]); } } isNullOrEmpty(query: string): boolean { return typeof query !== 'string' || query === null || query.length === 0; } _displayFn(item: any): string { if (this.configuration.displayFn && item) { return this.configuration.displayFn(item); } return item; } _titleFn(item: any): string { if (this.configuration.titleFn && item) { return this.configuration.titleFn(item); } return item; } _subtitleFn(item: any): string { if (this.configuration.subtitleFn && item) { return this.configuration.subtitleFn(item); } return null; } _requestDelay(): number { return this.configuration.requestDelay || this.requestDelay; } _minFilteringChars(): number { return this.configuration.minFilteringChars || this.minFilteringChars; } _loadDataOnStart(): boolean { return this.configuration.loadDataOnStart || this.loadDataOnStart; } _optionSelected(event: MatAutocompleteSelectedEvent) { this._setValue(this.configuration.valueAssign ? this.configuration.valueAssign(event.option.value) : event.option.value); //this._inputValue = " "; this.stateChanges.next(); this.optionSelected.emit(event.option.value); if (this.configuration.removeAfterSelection) { this.clearAutocomplete() } } private clearAutocomplete() { this._setValue(null); } private _setValue(value: any) { this.value = value; this.pushChanges(this.value); } _onInputFocus() { // We set the items observable on focus to avoid the request being executed on component load. if (!this._items) { this._items = this._inputSubject.pipe( startWith(null), debounceTime(this.requestDelay), distinctUntilChanged(), tap(() => { this.loading = true; }), switchMap(query => { // If its a valid object, a selection just made and the object is set as the value of the form control. That means we should fire an extra request to the server. if (this._isValidObject(query)) { return observableOf([]); } // Since the object is changed we need to clear any existing selections, except for the first time. if (query !== null) { this.pushChanges(null); } return this.filter(query); }), tap(() => { this.loading = false; })); if (this.configuration.groupingFn) { this._groupedItems = this._items.pipe(map(items => this.configuration.groupingFn(items))); } } } _inputValueChange(value: string) { //this._inputValue = value; this._inputSubject.next(value); this.stateChanges.next(); } _isValidObject(value: any): boolean { try { if (!value) { return false; } if (typeof value !== 'object') { JSON.parse(value); } } catch (e) { return false; } return true; } onChange = (_: any) => { }; onTouched = () => { }; writeValue(value: any): void { this.value = value || ''; } pushChanges(value: any) { this.onChange(value); } registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } setDescribedByIds(ids: string[]) { this.describedBy = ids.join(' '); } onContainerClick(event: MouseEvent) { if ((event.target as Element).tagName.toLowerCase() !== 'input') { this.elRef.nativeElement.querySelector('input').focus(); } } chipRemove(): void { this._setValue(null); } autoCompleteDisplayFn() { return (val) => ""; } ngOnDestroy() { this.stateChanges.complete(); this.fm.stopMonitoring(this.elRef.nativeElement); } }