src/app/shared/autocomplete/autocomplete.component.ts
| encapsulation | ViewEncapsulation.None |
| selector | app-autocomplete |
| styleUrls | ./autocomplete.component.scss |
| templateUrl | ./autocomplete.component.html |
Properties |
Inputs |
Outputs |
HostListeners |
constructor(cdr: ChangeDetectorRef)
|
||||||
|
Parameters :
|
| 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 : ''
|
|
| onInputChange | |
Type : EventEmitter<any>
|
|
| onInputKeydown | |
Type : EventEmitter<any>
|
|
| onLoadingMore | |
Type : EventEmitter<OnLoadingMoreEvent>
|
|
| valueChange | |
Type : EventEmitter<any>
|
|
| window:resize |
Arguments : '$event'
|
window:resize(event: any)
|
| calculateInputPosition |
calculateInputPosition()
|
|
Returns :
void
|
| closeDatalist |
closeDatalist()
|
|
Returns :
void
|
| filterOptionsList | ||||||
filterOptionsList(showAll?: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| getOptionsMoveFocus | ||||||
getOptionsMoveFocus(direction: "up" | "down")
|
||||||
|
Parameters :
Returns :
void
|
| loadMore |
loadMore()
|
|
Returns :
void
|
| ngAfterViewInit |
ngAfterViewInit()
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Returns :
void
|
| onDataListClick | ||||||
onDataListClick(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onDataListMouseEnter | ||||||
onDataListMouseEnter(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onDataListMouseLeave | ||||||
onDataListMouseLeave(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onDataListScroll | ||||||
onDataListScroll(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onInputClick | ||||||
onInputClick(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onInputFired |
onInputFired(event?: any, emitEvent?: boolean, initiator?: unknown)
|
|
Returns :
void
|
| onInputFocusin | ||||||
onInputFocusin(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onInputFocusout | ||||||
onInputFocusout(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onInputKeyup | ||||||
onInputKeyup(event: any)
|
||||||
|
Parameters :
Returns :
boolean | undefined
|
| onInputMouseenter | ||||||
onInputMouseenter(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onMainKeydown | ||||||
onMainKeydown(event: KeyboardEvent)
|
||||||
|
Parameters :
Returns :
void
|
| onResize | ||||||
onResize(event: any)
|
||||||
Decorators :
@HostListener('window:resize', ['$event'])
|
||||||
|
Parameters :
Returns :
void
|
| openDatalist | ||||||||
openDatalist(lockDelay: boolean)
|
||||||||
|
Parameters :
Returns :
void
|
| resetDatalistFilter |
resetDatalistFilter()
|
|
Returns :
void
|
| selectFocusedOption |
selectFocusedOption()
|
|
Returns :
void
|
| setOptionValue |
setOptionValue(label: string, value: string)
|
|
Returns :
void
|
| setToCloseDatalist |
setToCloseDatalist()
|
|
Returns :
void
|
| unselectAllOptions |
unselectAllOptions()
|
|
Returns :
void
|
| 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