import { Input , OnDestroy } from '@angular/core' ; import { ControlValueAccessor , FormControl } from '@angular/forms' ; import { BehaviorSubject , EMPTY , Observable , Subject , Subscription , combineLatest , of , timer } from 'rxjs' ; import { catchError , debounce , distinctUntilChanged , filter , map , publishReplay , refCount , startWith , switchMap , take , withLatestFrom } from 'rxjs/operators' ; import { DataSource , OptionEntry } from './types' ; interface SearchResult { search : string ; list ? : OptionEntry [ ] ; errorMessage ? : string ; } type Searches = Observable < OptionEntry | string | null > ; export class SearchSelectBase implements ControlValueAccessor , OnDestroy { @ Input ( ) debounceTime = 75 ; @ Input ( ) set dataSource ( ds ) { this . incomingDataSources . next ( ds ) ; } searchControl = new FormControl ( ) ; loading : Observable < boolean > ; list : Observable < OptionEntry [ ] | undefined > ; empty : Observable < boolean > ; errorMessage : Observable < string | undefined > ; focus ( ) { this . selectedValueSub = this . selectedValue . subscribe ( this . checkAndPropagate . bind ( this ) ) ; } blur ( ) { this . onTouched ( ) ; if ( this . selectedValueSub ) { this . selectedValueSub . unsubscribe ( ) ; this . selectedValueSub = undefined ; } this . selectedValue . pipe ( take ( 1 ) ) . subscribe ( this . checkAndPropagate . bind ( this ) ) ; } displayWith ( value ) { return value ? value . display : '' ; } private incomingValues = new Subject < any > ( ) ; private incomingDataSources = new BehaviorSubject < DataSource > ( { displayValue : x => of ( x ) , search : ( ) => of ( [ ] ) } ) ; private incomingDataSourcesSub : Subscription ; private outsideValue : any ; private selectedValueSub : Subscription | undefined = undefined ; private selectedValue : Observable < any > ; constructor ( ) { const searches = this . searchControl . valueChanges . pipe ( startWith ( this . searchControl . value ) , distinctUntilChanged ( ) , debounce ( srch => { if ( typeof srch === 'string' ) { return timer ( this . debounceTime ) ; } return EMPTY ; } ) ) ; const options = combineLatest ( [ searches , this . incomingDataSources . pipe ( filter ( ds => ! ! ds ) ) ] ) . pipe ( switchMap ( ( [ srch , ds ] ) => { if ( srch === null ) { srch = '' ; } if ( typeof srch === 'string' ) { const search = srch ; return ds . search ( srch ) . pipe ( map ( list => ( { search , list } ) ) , catchError ( errorMessage => of ( { search , errorMessage } ) ) , startWith ( { search } ) ) ; } const entry = srch as OptionEntry ; return of < SearchResult > ( { search : srch . display , list : [ { ... entry } ] } ) ; } ) , publishReplay ( 1 ) , refCount ( ) ) ; function matcher ( search , entry ) { return entry . display === search ; } this . selectedValue = options . pipe ( filter ( result => ! ! result . list ) , withLatestFrom ( this . incomingDataSources ) , map ( ( [ result , ds ] ) => { const list = result . list || [ ] ; const matchFn = ds . match || matcher ; const entry = list . find ( option => matchFn ( result . search , option ) ) ; return ( entry && entry . value ) || null ; } ) , distinctUntilChanged ( ) ) ; this . loading = options . pipe ( map ( o => ! o . list && ! o . errorMessage ) ) ; this . list = options . pipe ( map ( o => o . list ) ) ; this . empty = options . pipe ( map ( o => ( o . list ? o . list . length === 0 : false ) ) ) ; this . errorMessage = options . pipe ( map ( o => o . errorMessage ) ) ; this . incomingDataSourcesSub = this . incomingValues . pipe ( withLatestFrom ( this . incomingDataSources ) , switchMap < [ any , DataSource ] , Observable < OptionEntry | null > > ( ( [ value , ds ] ) => ds . displayValue ( value ) ) ) . subscribe ( value => this . searchControl . setValue ( value ) ) ; } writeValue ( obj ) { if ( obj !== this . outsideValue ) { this . outsideValue = obj ; this . incomingValues . next ( obj ) ; } } registerOnChange ( fn ) { this . onChange = fn ; } registerOnTouched ( fn ) { this . onTouched = fn ; } setDisabledState ( isDisabled ) { if ( isDisabled ) { this . searchControl . disable ( ) ; } else { this . searchControl . enable ( ) ; } } ngOnDestroy ( ) { if ( this . incomingDataSourcesSub ) { this . incomingDataSourcesSub . unsubscribe ( ) ; } } private onChange = ( _ ) => { } ; private onTouched = ( ) => { } ; private checkAndPropagate ( value ) { if ( value !== this . outsideValue ) { this . outsideValue = value ; this . onChange ( value ) ; } } }