src/app/shared/autocomplete/autocomplete.component.ts

Implements

OnInit AfterViewInit

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs
HostListeners

Constructor

constructor(cdr: ChangeDetectorRef)
Parameters :
Name Type Optional
cdr ChangeDetectorRef No

Inputs

autocomplete
Type : string
Default value : 'off'
disabled
Type : boolean
Default value : false
emitOnlySelected
Type : boolean
Default value : false
enableLoadMore
Type : boolean
Default value : false
inputId
Type : string
Default value : ''
placeholder
Type : string
Default value : ''
tabindex
Type : string
Default value : ''
type
Type : string
Default value : 'text'
value
Type : string
Default value : ''

Outputs

onInputChange
Type : EventEmitter<any>
onInputKeydown
Type : EventEmitter<any>
onLoadingMore
Type : EventEmitter<OnLoadingMoreEvent>
valueChange
Type : EventEmitter<any>

HostListeners

window:resize
Arguments : '$event'
window:resize(event: any)

Methods

calculateInputPosition
calculateInputPosition()
Returns : void
closeDatalist
closeDatalist()
Returns : void
filterOptionsList
filterOptionsList(showAll?: boolean)
Parameters :
Name Type Optional
showAll boolean Yes
Returns : void
getOptionsMoveFocus
getOptionsMoveFocus(direction: "up" | "down")
Parameters :
Name Type Optional
direction "up" | "down" No
Returns : void
loadMore
loadMore()
Returns : void
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnInit
ngOnInit()
Returns : void
onDataListClick
onDataListClick(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onDataListMouseEnter
onDataListMouseEnter(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onDataListMouseLeave
onDataListMouseLeave(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onDataListScroll
onDataListScroll(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onInputClick
onInputClick(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onInputFired
onInputFired(event?: any, emitEvent?: boolean, initiator?: unknown)
Parameters :
Name Type Optional
event any Yes
emitEvent boolean Yes
initiator unknown Yes
Returns : void
onInputFocusin
onInputFocusin(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onInputFocusout
onInputFocusout(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onInputKeyup
onInputKeyup(event: any)
Parameters :
Name Type Optional
event any No
Returns : boolean | undefined
onInputMouseenter
onInputMouseenter(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
onMainKeydown
onMainKeydown(event: KeyboardEvent)
Parameters :
Name Type Optional
event KeyboardEvent No
Returns : void
onResize
onResize(event: any)
Decorators :
@HostListener('window:resize', ['$event'])
Parameters :
Name Type Optional
event any No
Returns : void
openDatalist
openDatalist(lockDelay: boolean)
Parameters :
Name Type Optional Default value
lockDelay boolean No true
Returns : void
resetDatalistFilter
resetDatalistFilter()
Returns : void
selectFocusedOption
selectFocusedOption()
Returns : void
setOptionValue
setOptionValue(label: string, value: string)
Parameters :
Name Type Optional
label string No
value string No
Returns : void
setToCloseDatalist
setToCloseDatalist()
Returns : void
unselectAllOptions
unselectAllOptions()
Returns : void

Properties

afterOpenLock
Type : boolean
Default value : false
afterOpenLockDelay
Type : number
Default value : 150
afterOpenLockDelayTimeout
Type : any
datalistCloseTimeout
Type : any
datalistOpen
Type : boolean
Default value : false
innerHeight
Type : number | undefined
inputClientRect
Type : { width?: number; bottom?: number }
Default value : {}
inputElement
Type : any
Decorators :
@ViewChild('input')
inputFocused
Type : boolean
Default value : false
loadingMore
Type : boolean
Default value : false
preventDatalistClose
Type : boolean
Default value : false
import {
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  EventEmitter,
  HostListener,
  Input,
  OnInit,
  Output,
  ViewChild,
  ViewEncapsulation
} from '@angular/core'

export type OnLoadingMoreEvent = {
  loadMoreFinished: (moreExists: boolean) => void
}

@Component({
  selector: 'app-autocomplete',
  templateUrl: './autocomplete.component.html',
  styleUrls: ['./autocomplete.component.scss'],
  encapsulation: ViewEncapsulation.None,
  standalone: false
})
export class AutocompleteComponent implements OnInit, AfterViewInit {
  @ViewChild('input') inputElement: any

  @Input() inputId: string = ''
  @Input() placeholder: string = ''
  @Input() value: string = ''
  @Input() disabled: boolean = false
  @Input() type: string = 'text'
  @Input() autocomplete: string = 'off'
  @Input() tabindex: string = ''
  @Input() emitOnlySelected: boolean = false
  @Input() enableLoadMore: boolean = false

  @Output() onInputKeydown: EventEmitter<any> = new EventEmitter()
  @Output() onInputChange: EventEmitter<any> = new EventEmitter()
  @Output() valueChange: EventEmitter<any> = new EventEmitter()
  @Output() onLoadingMore: EventEmitter<OnLoadingMoreEvent> = new EventEmitter()

  datalistOpen: boolean = false
  afterOpenLockDelay: number = 150
  afterOpenLockDelayTimeout: any
  afterOpenLock: boolean = false

  inputFocused: boolean = false
  inputClientRect: {
    width?: number
    bottom?: number
  } = {}
  innerHeight: number | undefined
  datalistCloseTimeout: any
  preventDatalistClose: boolean = false

  loadingMore: boolean = false

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit(): void {}

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.calculateInputPosition()
    }, 500)
  }

  onInputFired(event?: any, emitEvent?: boolean, initiator?: 'click') {
    this.openDatalist(initiator !== 'click')
    this.unselectAllOptions()
    this.filterOptionsList()

    if (!this.emitOnlySelected || emitEvent) {
      this.onInputChange.emit({ target: { value: this.value } })
      this.valueChange.emit(this.value)
    }
  }

  onInputClick(event: any) {
    this.openDatalist()
  }

  openDatalist(lockDelay: boolean = true) {
    this.datalistOpen = true

    if (lockDelay) {
      this.afterOpenLock = true

      this.afterOpenLockDelayTimeout = setTimeout(() => {
        this.afterOpenLock = false
      }, this.afterOpenLockDelay)
    }
  }

  closeDatalist() {
    clearTimeout(this.afterOpenLockDelayTimeout)
    this.afterOpenLock = false
    this.datalistOpen = false
  }

  resetDatalistFilter() {
    this.filterOptionsList(true)
  }

  onInputFocusin(event: any) {
    this.inputFocused = true
  }

  onInputFocusout(event: any) {
    this.inputFocused = false

    this.setToCloseDatalist()
  }

  onInputMouseenter(event: any) {
    // If width not set it means calculation failed on init (input was hidden)
    if (!this.inputClientRect.width) this.calculateInputPosition()
  }

  filterOptionsList(showAll?: boolean) {
    const options = document.querySelectorAll(
      `#datalist_${this.inputId} option`
    )

    for (let i = 0; i < options.length; i++) {
      const option = options[i]

      if (this.value.length === 0) {
        option.classList.remove('hidden')

        continue
      }

      if (showAll) {
        option.classList.remove('hidden')

        continue
      }

      const optionLabel = (option as any).innerText || ''
      const optionValue = (option as any).value || ''
      const combined = optionLabel + optionValue

      if (combined?.toLowerCase().includes(this.value.toLowerCase())) {
        option.classList.remove('hidden')
      } else {
        option.classList.add('hidden')
      }
    }
  }

  setToCloseDatalist() {
    this.datalistCloseTimeout = setTimeout(() => {
      if (this.preventDatalistClose || this.afterOpenLock) return

      this.closeDatalist()
      this.unselectAllOptions()
      this.resetDatalistFilter()
    }, 100)
  }

  calculateInputPosition() {
    const inputElement = this.inputElement.nativeElement
    const clientRect = inputElement?.getBoundingClientRect()

    if (clientRect.width > 0 && clientRect.height > 0) {
      this.inputClientRect = {
        width: clientRect.width,
        bottom: clientRect.bottom
      }
    }

    this.innerHeight = window.innerHeight
  }

  onDataListMouseEnter(event: any) {
    this.preventDatalistClose = true
  }

  onDataListMouseLeave(event: any) {
    this.preventDatalistClose = false
    this.inputElement.nativeElement.focus()
  }

  onDataListScroll(event: any) {
    this.setToCloseDatalist()
  }

  onDataListClick(event: any) {
    const clickedOption = event.target
    const itemType = clickedOption.dataset?.type

    if (itemType === 'load-more') {
      this.loadMore()
      return
    }

    if (clickedOption.tagName === 'OPTION') {
      this.setOptionValue(clickedOption.innerText, clickedOption.value)
      return
    }
  }

  loadMore() {
    this.onLoadingMore.emit({
      loadMoreFinished: (moreExists: boolean) => {
        this.loadingMore = false
        this.enableLoadMore = moreExists
      }
    })
    this.loadingMore = true
  }

  setOptionValue(label: string, value: string) {
    if (label || value) {
      if (!value) value = label
    }

    if (value) {
      this.preventDatalistClose = false
      this.value = value
      this.onInputFired(null, true, 'click')
      this.setToCloseDatalist()
    }
  }

  onMainKeydown(event: KeyboardEvent) {
    switch (event.key) {
      case 'ArrowUp': {
        this.getOptionsMoveFocus('up')
        break
      }
      case 'ArrowDown': {
        this.getOptionsMoveFocus('down')
        break
      }
      case 'Enter': {
        this.selectFocusedOption()
        break
      }
    }
  }

  onInputKeyup(event: any) {
    if (!this.datalistOpen) {
      return true
    }

    // We prevent propagation of Escape event
    // since it would close modal also if input is
    // inside of it (or trigger any other actions that are listening on escape)
    if (event.key === 'Escape') {
      event.stopPropagation()
      event.stopImmediatePropagation()

      this.setToCloseDatalist()

      return false
    }
  }

  unselectAllOptions() {
    const options = document.querySelectorAll(
      `#datalist_${this.inputId} option`
    )

    for (let i = 0; i < options.length; i++) {
      options[i].classList.remove('focused')
    }
  }

  selectFocusedOption() {
    const option = document.querySelector(
      `#datalist_${this.inputId} option.focused`
    )

    if (option) this.onDataListClick({ target: option })

    this.setToCloseDatalist()
  }

  getOptionsMoveFocus(direction: 'up' | 'down') {
    const options = document.querySelectorAll(
      `#datalist_${this.inputId} option:not(.hidden)`
    )
    let focusedIndex = 0

    for (let i = 0; i < options.length; i++) {
      const option = options[i]

      if (option.classList.contains('focused')) {
        focusedIndex = direction === 'down' ? i + 1 : i - 1
        if (focusedIndex < 0) focusedIndex = options.length - 1
        if (focusedIndex > options.length - 1) focusedIndex = 0
        option.classList.remove('focused')

        break
      }
    }

    options[focusedIndex].classList.add('focused')
  }

  @HostListener('window:resize', ['$event'])
  onResize(event: any) {
    this.calculateInputPosition()
  }
}
<div (keydown)="onMainKeydown($event)" class="autocomplete-wrapper">
  <input
    #input
    class="input-val"
    type="text"
    autocomplete="off"
    [id]="inputId"
    [attr.list]="'datalist_' + inputId"
    autofocus
    tabindex="0"
    [placeholder]="placeholder"
    [(ngModel)]="value"
    (click)="onInputClick($event)"
    (ngModelChange)="onInputFired($event)"
    (mouseenter)="onInputMouseenter($event)"
    (focus)="onInputFocusin($event)"
    (focusout)="onInputFocusout($event)"
    (keyup)="onInputKeyup($event)"
    [disabled]="disabled"
  />

  <div class="overlay" [class.hidden]="!datalistOpen"></div>

  <div
    (click)="onDataListClick($event)"
    (mouseenter)="onDataListMouseEnter($event)"
    (mouseleave)="onDataListMouseLeave($event)"
    (scroll)="onDataListScroll($event)"
    [ngStyle]="{
      'width.px': inputClientRect.width,
      'max-height.px':
        innerHeight && inputClientRect.bottom
          ? innerHeight - inputClientRect.bottom
          : undefined
    }"
    class="datalist"
    [class.hidden]="!datalistOpen"
    [id]="'datalist_' + inputId"
  >
    <div #options>
      <ng-content></ng-content>
    </div>

    @if (options.children.length > 0 && enableLoadMore) {
      <option data-type="load-more" class="load-more">
        {{ loadingMore ? 'Loading...' : 'LOAD MORE' }}
      </option>
    }
  </div>
</div>

./autocomplete.component.scss

Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""