File

projects/rebirth-ng/src/lib/auto-complete/auto-complete.directive.ts

Implements

OnInit OnDestroy ControlValueAccessor

Metadata

providers { : , : (() => ), : true }
selector [reAutoComplete]

Index

Properties
Methods
Inputs
Outputs
HostListeners

Constructor

constructor(elementRef: ElementRef, viewContainerRef: ViewContainerRef, componentFactoryResolver: ComponentFactoryResolver, renderer: Renderer2, injector: Injector, positionService: PositionService, rebirthNGConfig: RebirthNGConfig, changeDetectorRef: ChangeDetectorRef)
Parameters :
Name Type Optional
elementRef ElementRef no
viewContainerRef ViewContainerRef no
componentFactoryResolver ComponentFactoryResolver no
renderer Renderer2 no
injector Injector no
positionService PositionService no
rebirthNGConfig RebirthNGConfig no
changeDetectorRef ChangeDetectorRef no

Inputs

appendBody

Default value: false

cssClass

Type: string

dataSource

Type: []

delay

Type: number

disabled

Type: boolean

formatter

Type: function

itemTemplate

Type: TemplateRef<any>

minLength

Type: number

noResultItemTemplate

Type: TemplateRef<any>

onSearch

Type: function

placementElement

Type: any

Outputs

selectValueChange $event type: EventEmitter

HostListeners

blur
blur()
document:click
Arguments : '$event'
document:click($event: Event)
keydown.ArrowDown
Arguments : '$event'
keydown.ArrowDown($event: )
keydown.ArrowUp
Arguments : '$event'
keydown.ArrowUp($event: )
keydown.Enter
Arguments : '$event'
keydown.Enter($event: )
keydown.esc
Arguments : '$event'
keydown.esc($event: )

Methods

Private fillPopup
fillPopup(source?: any, term?: string)
Parameters :
Name Type Optional
source any yes
term string yes
Returns : void
Private hidePopup
hidePopup()
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
onSourceChange
onSourceChange(source: , term?: string)
Parameters :
Name Type Optional
source no
term string yes
Returns : void
Private onTermChange
onTermChange(term: )
Parameters :
Name Optional
term no
Returns : void
positionPopup
positionPopup()
Returns : void
Private registerInputEvent
registerInputEvent(elementRef: ElementRef)
Parameters :
Name Type Optional
elementRef ElementRef no
Returns : any
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any no
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any no
Returns : void
Private removePopView
removePopView()
Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean no
Returns : void
Private setupArraySource
setupArraySource()
Returns : void
toggle
toggle($event?: Event)
Parameters :
Name Type Optional
$event Event yes
Returns : void
Private unSubscription
unSubscription()
Returns : void
Private writeInputValue
writeInputValue(value: any)
Parameters :
Name Type Optional
value any no
Returns : void
writeValue
writeValue(obj: any)
Parameters :
Name Type Optional
obj any no
Returns : void

Properties

Private arraySource
arraySource: any[]
Type : any[]
Private onChange
onChange:
Default value : (_: any) => null
Private onTouched
onTouched:
Default value : () => null
Private placement
placement: string
Type : string
Default value : 'bottom-left'
Private popupRef
popupRef: ComponentRef<AutoCompletePopupComponent>
Type : ComponentRef<AutoCompletePopupComponent>
Private subscription
subscription: Subscription
Type : Subscription
term
term: string
Type : string
Private value
value: any
Type : any
Private valueChanges
valueChanges: Observable<any[]>
Type : Observable<any[]>

Accessors

dataSource
setdataSource(dataSource: [])
Parameters :
Name Type Optional
dataSource [] no
Returns : void
import {
  ChangeDetectorRef,
  ComponentFactoryResolver,
  ComponentRef,
  Directive,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostListener,
  Injector,
  Input,
  OnDestroy,
  OnInit,
  Output,
  Renderer2,
  TemplateRef,
  ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { fromEvent } from 'rxjs';
import { of } from 'rxjs';
import { Observable } from 'rxjs';
import { Subscription } from 'rxjs';
import { map, filter, debounceTime, switchMap, tap } from 'rxjs/operators';
import { RebirthNGConfig } from '../rebirth-ng.config';
import { PositionService } from '../position/positioning.service';
import { AutoCompletePopupComponent } from './auto-complete-popup.component';
import { stopPropagationIfExist } from '../utils/dom-utils';

@Directive({
  selector: '[reAutoComplete]',
  exportAs: 'autoComplete',
  host: {
    'autocomplete': 'off',
    'autocapitalize': 'off',
    'autocorrect': 'off'
  },
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => AutoCompleteDirective),
    multi: true
  }]
})
export class AutoCompleteDirective implements OnInit, OnDestroy, ControlValueAccessor {
  @Input() disabled: boolean;
  @Input() cssClass: string;
  @Input() delay: number;
  @Input() minLength: number;
  @Input() appendBody = false;
  @Input() itemTemplate: TemplateRef<any>;
  @Input() noResultItemTemplate: TemplateRef<any>;
  @Input() formatter: (item: any) => string;
  @Input() onSearch: (term: string, target?: AutoCompleteDirective) => Observable<any[]>;
  @Output() selectValueChange = new EventEmitter<any>();
  @Input() placementElement: any;
  term: string;
  private valueChanges: Observable<any[]>;
  private value: any;
  private placement = 'bottom-left';
  private subscription: Subscription;
  private arraySource: any[];
  private popupRef: ComponentRef<AutoCompletePopupComponent>;
  private onChange = (_: any) => null;
  private onTouched = () => null;

  constructor(private elementRef: ElementRef, private viewContainerRef: ViewContainerRef,
              private componentFactoryResolver: ComponentFactoryResolver, private renderer: Renderer2,
              private injector: Injector, private positionService: PositionService,
              private rebirthNGConfig: RebirthNGConfig, private changeDetectorRef: ChangeDetectorRef) {
    this.delay = rebirthNGConfig.autoComplete.delay;
    this.minLength = rebirthNGConfig.autoComplete.minLength;
    this.itemTemplate = rebirthNGConfig.autoComplete.itemTemplate;
    this.noResultItemTemplate = rebirthNGConfig.autoComplete.noResultItemTemplate;
    this.formatter = rebirthNGConfig.autoComplete.formatter;
  }

  ngOnInit() {
    this.valueChanges = this.registerInputEvent(this.elementRef);

    this.subscription = this.valueChanges
      .subscribe(source => this.onSourceChange(source));

    const factory = this.componentFactoryResolver.resolveComponentFactory(AutoCompletePopupComponent);

    const viewContainerRef = this.appendBody ? this.rebirthNGConfig.rootContainer : this.viewContainerRef;
    // EXCEPTION: Expression has changed after it was checked when append to body;

    setTimeout(() => {
      this.popupRef = viewContainerRef.createComponent(factory, this.viewContainerRef.length, this.injector);
      this.fillPopup();
      this.positionPopup();

      this.popupRef.instance.registerOnChange(item => {
        this.value = item;
        this.writeInputValue(item);
        this.onChange(item);
        this.hidePopup();
        this.selectValueChange.emit(item);
      });
    }, 0);
  }

  @Input()
  set dataSource(dataSource: any[]) {
    if (dataSource) {
      this.arraySource = dataSource;
      this.setupArraySource();
    }
  }

  private setupArraySource() {
    if (this.popupRef) {
      this.popupRef.instance.source = this.arraySource;
    }

    this.onSearch = (term) => {
      return of(
        this.arraySource
          .filter(item => this.formatter(item).toLowerCase().indexOf((term || '').toLowerCase()) !== -1)
      );
    };
  }

  writeValue(obj: any): void {
    this.value = obj || '';
    this.writeInputValue(this.value);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
    this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled);
    if (this.popupRef) {
      this.popupRef.instance.setDisabledState(isDisabled);
    }
  }

  ngOnDestroy() {
    this.unSubscription();
    this.removePopView();
  }

  private removePopView() {
    if (this.popupRef) {
      this.popupRef.destroy();
    }
  }

  @HostListener('blur', [])
  onBlur() {
    this.onTouched();
  }

  @HostListener('keydown.esc', ['$event'])
  onEscKeyup($event) {
    this.hidePopup();
  }

  @HostListener('keydown.Enter', ['$event'])
  onEnterKeyDown($event) {
    if (this.popupRef && this.popupRef.instance.isOpen) {
      $event.preventDefault();
      $event.stopPropagation();
      this.popupRef.instance.selectCurrentItem();
      this.hidePopup();
    }
  }

  @HostListener('keydown.ArrowUp', ['$event'])
  onArrowUpKeyDown($event) {
    if (this.popupRef) {
      $event.preventDefault();
      $event.stopPropagation();
      this.popupRef.instance.prev();
    }
  }

  @HostListener('keydown.ArrowDown', ['$event'])
  onArrowDownKeyDown($event) {
    if (this.popupRef) {
      $event.preventDefault();
      $event.stopPropagation();
      this.popupRef.instance.next();
    }
  }

  @HostListener('document:click', ['$event'])
  onDocumentClick($event: Event) {
    const hostElement = this.elementRef.nativeElement;
    if ($event.target !== hostElement) {
      this.hidePopup();
    }
  }

  onSourceChange(source, term?: string) {
    if (term !== undefined) {
      this.value = term;
    }

    const pop = this.popupRef.instance;
    pop.reset();
    this.fillPopup(source, this.value);
    if ((source && source.length) || this.noResultItemTemplate) {
      pop.show();
      this.positionPopup();
      this.changeDetectorRef.markForCheck();
    }
  }

  private hidePopup() {
    if (this.popupRef) {
      this.popupRef.instance.activeIndex = 0;
      this.popupRef.instance.hide();
    }
  }

  toggle($event?: Event) {
    stopPropagationIfExist($event);

    if (this.popupRef) {
      const pop = this.popupRef.instance;
      if (pop.isOpen) {
        this.hidePopup();
        return;
      }
      this.onSourceChange(this.arraySource);
    }
  }

  positionPopup() {
    const targetElement = this.popupRef.location.nativeElement;
    const hostElement = this.placementElement || this.elementRef.nativeElement;
    const clientRect = this.positionService.positionElements(hostElement, targetElement, this.placement, this.appendBody);
    this.renderer.setStyle(targetElement, 'left', `${clientRect.left}px`);
    this.renderer.setStyle(targetElement, 'top', `${clientRect.top}px`);
  }

  private fillPopup(source?: any, term?: string) {
    const pop = this.popupRef.instance;
    pop.source = source;
    pop.term = term;
    ['formatter', 'itemTemplate', 'noResultItemTemplate', 'cssClass']
      .forEach(key => {
        if (this[key] !== undefined) {
          pop[key] = this[key];
        }
      });
  }

  private writeInputValue(value: any) {
    const formatValue = value || '';
    this.renderer.setProperty(this.elementRef.nativeElement, 'value', this.formatter(formatValue) || '');
  }

  private unSubscription() {
    if (this.subscription) {
      this.subscription.unsubscribe();
      this.subscription = null;
    }
  }

  private onTermChange(term) {
    this.value = term;
    if (this.popupRef) {
      this.popupRef.instance.term = term;
    }
    this.onChange(term);
  }

  private registerInputEvent(elementRef: ElementRef) {
    return fromEvent(elementRef.nativeElement, 'input')
      .pipe(
        map((e: any) => e.target.value),
        tap(term => this.onTouched()),
        tap(term => this.term = term),
        filter(term => !this.disabled && this.onSearch && term.length >= this.minLength),
        debounceTime(this.delay),
        // distinctUntilChanged(),
        tap(term => this.onTermChange(term)),
        switchMap(term => this.onSearch(term, this))
      );
  }
}

results matching ""

    No results matching ""