// proof of concept...
@Input() query;
@Output() change = new EventEmitter();
@Output() search = new EventEmitter();
@Output() typing = new EventEmitter<string>();
@ViewChild('employeeSearch') employeeSearch;
@Output() typeaheadSelected = new EventEmitter<string>();
@Input() typeaheadItemTpl: TemplateRef<any>; // allow custom template for suggestion
private showSuggestions: boolean = false;
private results: string[];
private suggestionIndex: number = 0;
private subscriptions: Subscription[];
private activeResult: string;
@ViewChild('suggestionsTplRef') suggestionsTplRef;
@HostListener('keydown', ['$event'])
handleEsc(event: KeyboardEvent) {
if (event.keyCode === Key.Escape) {
this.hideSuggestions();
event.preventDefault();
}
}
/** 1. element - to listen for input events
* 2. viewContainer - in order to render the template as a sibling
* 3. jsonp - start requests as a jsonp to the search API
* 4. cdr - change detection reference to apply changes within this component and its siblings
*/
constructor(private element: ElementRef, private viewContainer: ViewContainerRef, private jsonp: Jsonp, private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.subscriptions = [
this.filterEnterEvent(),
this.listenAndSuggest(),
this.navigateWithArrows()
];
this.renderTemplate();
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions.length = 0;
}
/** Uses the “viewContainer” in order to render the template as a sibling to the actual element.
* “createEmbeddedView” function takes a template reference and inserts it, compiled with the data,
* into the last view position in the html container. The actual “viewContainer” is the element that wraps the input element (in this case).
* A second argument determines the index at which this template should be rendered. */
renderTemplate() {
this.viewContainer.createEmbeddedView(this.suggestionsTplRef);
this.cdr.markForCheck();
}
/** support selection of a result with the enter key, this code listens to keydown strokes on the input element,
* allows only Enter key to pass on to the stream and then invokes the “handleSelectSuggestion” which eventually – emits
* an event for the selected result. */
filterEnterEvent() {
return Observable.fromEvent(this.element.nativeElement, 'keydown')
.filter((e: KeyboardEvent) => e.keyCode === Key.Enter)
.subscribe((event: Event) => {
event.preventDefault();
this.handleSelectSuggestion(this.activeResult);
});
}
/** Makes a jsonp call to get the list of suggestions according to the value of the input
* listens to keyup strokes (since we do want to allow a character to apply into the input),
* filtering out any non characters keys with “validateKeyCode()“. I use “distinctUntilChanged()”
* to filter the stream for the same value – which again prevents unnecessary requests.
* Then, the filter for an empty string becomes relevant before the code makes the actual request */
listenAndSuggest() {
return Observable.fromEvent(this.element.nativeElement, 'keyup')
.filter(this.validateKeyCode)
.map((e: any) => e.target.value)
.debounceTime(400)
.concat()
.distinctUntilChanged()
.filter((query: string) => query.length > 0)
.switchMap((query: string) => this.suggest(query)) // this.engageService.GetEmployee(term)
.subscribe((results: string[]) => {
this.results = results;
this.showSuggestions = true;
this.cdr.markForCheck();
});
}
navigateWithArrows() {
return Observable.fromEvent(this.element.nativeElement, 'keydown')
.filter((e: any) => e.keyCode === Key.ArrowDown || e.keyCode === Key.ArrowUp)
.map((e: any) => e.keyCode)
.subscribe((keyCode: number) => {
let step = keyCode === Key.ArrowDown ? 1 : -1;
const topLimit = 9;
const bottomLimit = 0;
this.suggestionIndex += step;
if (this.suggestionIndex === topLimit + 1) {
this.suggestionIndex = bottomLimit;
}
if (this.suggestionIndex === bottomLimit - 1) {
this.suggestionIndex = topLimit;
}
this.showSuggestions = true;
// this.renderTemplate();
this.cdr.markForCheck();
});
}
suggest(query: string) {
const url = 'http://suggestqueries.google.com/complete/search';
const searchConfig: URLSearchParams = new URLSearchParams();
const searchParams = {
hl: 'en',
ds: 'yt',
xhr: 't',
client: 'youtube',
q: query,
callback: 'JSONP_CALLBACK'
};
Object.keys(searchParams).forEach(param => searchConfig.set(param, searchParams[param]));
const options: RequestOptionsArgs = {
search: searchConfig
};
return this.jsonp.get(url, options)
.map(response => response.json()[1])
.map(results => results.map(result => result[0]));
}
markIsActive(index: number, result: string) {
const isActive = index === this.suggestionIndex;
if (isActive) {
this.activeResult = result;
}
}
handleSelectSuggestion(suggestion: string) {
this.hideSuggestions();
this.typeaheadSelected.emit(suggestion);
}
validateKeyCode(event: KeyboardEvent) {
return event.keyCode !== Key.Tab
&& event.keyCode !== Key.Shift
&& event.keyCode !== Key.ArrowLeft
&& event.keyCode !== Key.ArrowUp
&& event.keyCode !== Key.ArrowRight
&& event.keyCode !== Key.ArrowDown;
}
hideSuggestions() {
this.showSuggestions = false;
}
hasItemTemplate() {
return this.typeaheadItemTpl !== undefined;
}