2019-01-18 18:03:45 +01:00
import { FocusMonitor } from '@angular/cdk/a11y' ;
2019-02-14 17:56:06 +01:00
import { COMMA , ENTER } from '@angular/cdk/keycodes' ;
2019-01-18 18:03:45 +01:00
import { Component , ElementRef , EventEmitter , Input , OnDestroy , OnInit , Optional , Output , Self } from '@angular/core' ;
import { ControlValueAccessor , NgControl } from '@angular/forms' ;
2019-09-23 10:17:03 +02:00
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete' ;
import { MatFormFieldControl } from '@angular/material/form-field' ;
2019-01-18 18:03:45 +01:00
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' ;
2019-02-15 12:00:26 +01:00
import { switchMap } from 'rxjs/internal/operators/switchMap' ;
2019-01-18 18:03:45 +01:00
@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 < string > , ControlValueAccessor , OnDestroy {
static nextId = 0 ;
@Input ( ) configuration : SingleAutoCompleteConfiguration ;
// Selected Option Event
@Output ( ) optionSelected : EventEmitter < any > = new EventEmitter ( ) ;
id = ` single-autocomplete- ${ SingleAutoCompleteComponent . nextId ++ } ` ;
stateChanges = new Subject < void > ( ) ;
focused = false ;
errorState = false ;
controlType = 'single-autocomplete' ;
describedBy = '' ;
_inputValue : string ;
_inputSubject = new Subject < string > ( ) ;
loading = false ;
_items : Observable < any [ ] > ;
_groupedItems : Observable < AutoCompleteGroup [ ] > ;
private requestDelay = 200 ; //ms
private minFilteringChars = 1 ;
private loadDataOnStart = true ;
2019-02-14 17:56:06 +01:00
separatorKeysCodes : number [ ] = [ ENTER , COMMA ] ;
2019-01-18 18:03:45 +01:00
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 ;
2019-02-15 12:41:14 +01:00
this . _inputValue = ( value && value != "" ) ? " " : null ;
2019-01-18 18:03:45 +01:00
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 < any [ ] > {
// 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 ) {
2019-02-01 09:29:00 +01:00
this . _setValue ( this . configuration . valueAssign ? this . configuration . valueAssign ( event . option . value ) : event . option . value ) ;
2019-02-15 09:51:21 +01:00
//this._inputValue = " ";
2019-01-18 18:03:45 +01:00
this . stateChanges . next ( ) ;
this . optionSelected . emit ( event . option . value ) ;
2019-09-19 17:05:20 +02:00
if ( this . configuration . removeAfterSelection ) {
this . clearAutocomplete ( )
}
}
private clearAutocomplete() {
this . _setValue ( null ) ;
2019-01-18 18:03:45 +01:00
}
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 ; } ) ,
2019-02-15 12:00:26 +01:00
switchMap ( query = > {
2019-01-18 18:03:45 +01:00
// 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 ) {
2019-02-15 12:41:14 +01:00
//this._inputValue = value;
2019-01-18 18:03:45 +01:00
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 ( ) ;
}
}
2019-02-14 17:56:06 +01:00
chipRemove ( ) : void {
2019-02-15 12:41:14 +01:00
this . _setValue ( null ) ;
}
autoCompleteDisplayFn() {
return ( val ) = > "" ;
2019-02-14 17:56:06 +01:00
}
2019-01-18 18:03:45 +01:00
ngOnDestroy() {
this . stateChanges . complete ( ) ;
this . fm . stopMonitoring ( this . elRef . nativeElement ) ;
}
}