Skip to content

Instantly share code, notes, and snippets.

@2J
Created November 10, 2016 21:04
Show Gist options
  • Save 2J/fc4624e7820a05f6fe825ee1cb58c8e4 to your computer and use it in GitHub Desktop.
Save 2J/fc4624e7820a05f6fe825ee1cb58c8e4 to your computer and use it in GitHub Desktop.
Angular 2 search-field with autocomplete
import { Directive, HostListener, Output, OnInit, OnDestroy, ElementRef, EventEmitter } from '@angular/core';
@Directive({
selector: '[offClick]'
})
export class OffClickDirective implements OnInit, OnDestroy {
@Output('offClick') public offClick: EventEmitter<null> = new EventEmitter<null>();
constructor(
private elementRef: ElementRef
) {
this.documentClick = this.documentClick.bind(this); //bind scope to component
}
public documentClick($event: any): void {
if (!this.elementRef.nativeElement.contains($event.target)) {
this.offClick.emit();
}
}
public ngOnInit(): void {
setTimeout(() => { document.addEventListener('mousedown', this.documentClick); }, 0);
}
public ngOnDestroy(): void {
document.removeEventListener('mousedown', this.documentClick);
}
}
.clear-button {
pointer-events: auto;
cursor: pointer;
}
.search-field-select {
width: 100%;
height: auto;
max-height: 200px;
overflow-x: hidden;
margin-top: 0;
}
.search-field-option > a, .search-field-option > .autocomplete-item-text {
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
.search-field-option.active > a {
color: #fff;
text-decoration: none;
outline: 0;
background-color: #428bca;
}
<div class="search-field-container dropdown open" (offClick)="clickedOutside()">
<input [(ngModel)]="searchFieldVal"
#searchField="ngModel"
name="searchField"
[type]="type"
[placeholder]="placeholder"
(focus)="focused($event)"
(blur)="blurred($event)"
(change)="changed($event)"
(keyup)="keyupped($event)"
(keydown)="keydowned($event)"
autocomplete="off"
[required]="required"
[disabled]="disabled"
[readonly]="readonly"
class="form-control search-field" />
<span class="clear-button glyphicon glyphicon-remove-circle form-control-feedback" *ngIf="clearButton && isFocused && !!searchFieldVal" (click)="query = ''"></span>
<ul class="search-field-select dropdown-menu" *ngIf="isFocused && !disabled && !readonly" role="menu">
<li *ngIf="autocompleteItems && autocompleteItems.length === 0" class="search-field-option-container" role="menuitem">
<div class="search-field-option dropdown-item">
<span class="autocomplete-item-text">
<i>{{ emptyText }}</i>
</span>
</div>
</li>
<li *ngFor="let item of autocompleteItems; let index=index"
class="search-field-option-container"
role="menuitem">
<div class="search-field-option dropdown-item"
[class.active]="isActive(item)"
(mouseenter)="selectActive(item)">
<a href="javascript:void(0)" (click)="itemSelected(item)">
<template [ngIf]="displayTemplate == null">
{{ getItemProperty(item) }}
</template>
<template *ngIf="displayTemplate != null"
[ngTemplateOutlet]="displayTemplate"
[ngOutletContext]="{
$implicit: item,
index: index,
query: query
}">
</template>
</a>
</div>
</li>
</ul>
</div>
import { Component, Input, Output, EventEmitter, ContentChild, TemplateRef, ViewChild, ElementRef, DoCheck } from '@angular/core';
import * as _ from 'lodash';
@Component({
moduleId: module.id,
selector: 'search-field',
templateUrl: 'search-field.component.html',
styleUrls: ['search-field.component.css']
})
export class SearchFieldComponent {
//2-way bound filteredItems array
//Array of items after searching
private _filteredItems: Array<any> = new Array<any>();
@Input()
get filteredItems(): Array<any> {
return this._filteredItems;
}
@Output() private filteredItemsChange: EventEmitter<Array<any>> = new EventEmitter<Array<any>>(true);
set filteredItems(filteredItems: Array<any>) {
this._filteredItems = filteredItems;
this.setAutocompleteItems();
this.filteredItemsChange.emit(this.filteredItems);
}
//search query
private _query: string = "";
@Input()
get query(): string {
return this._query;
}
@Output() private queryChange: EventEmitter<string> = new EventEmitter<string>(true);
set query(query: string) {
this._query = query;
this.setFieldVal();
this.filter(this.query);
this.queryChange.emit(this.query);
}
//search field value
private searchFieldVal: string = "";
//Property to navigate to for search
@Input() property: string = null;
//List of items that the component is going to search through
private _items: Array<any>;
private itemsOld: Array<any>; //Used to detect changes in items array
@Input()
get items(): Array<any> {
return this._items;
}
set items(items: Array<any>) {
this._items = items;
this.itemsOld = _.clone(this.items);
this.filter(this.query); //Filter when items array changed
}
//If set, limits which items to show autocomplete for
private _autocompleteOptions: Array<any>;
@Input()
get autocompleteOptions(): Array<any> {
return this._autocompleteOptions;
};
set autocompleteOptions(autocompleteOptions: Array<any>) {
this._autocompleteOptions = autocompleteOptions;
this.setAutocompleteItems();
}
//list of items to show autocomplete for
private autocompleteItems: Array<any>;
//Input field type
@Input() type: string = "search"; //Input type
//If set to true, matches entire string
@Input() exactMatch: boolean = false;
//If set to true, returns all objects if query is empty
@Input() returnAllOnEmpty: boolean = true;
//Placeholder string to show on input field
@Input() placeholder: string = '';
//text to show when no results found
@Input() emptyText: string = 'No items match the search query';
//Shows clear button when typing
@Input() clearButton: boolean = true;
//Blur string
//Sets field to 'blurString' if not in focus
private _blurString: string = null;
@Input()
get blurString(): string {
return this._blurString;
}
set blurString(blurString: string) {
this._blurString = blurString;
this.setFieldVal(); //set blue string if string has changed
}
//If set to true, puts required field on input field
@Input() required: boolean = null;
//If set to true, puts disabled field on input field
@Input() disabled: boolean = null;
//If set to true, puts readonly field on input field
@Input() readonly: boolean = null;
//Template that the list will show. Shows property if template does not exist
@ContentChild(TemplateRef) displayTemplate: TemplateRef<any>;
//Returns array of filtered objects every time the items are filtered
@Output() private search: EventEmitter<Array<any>> = new EventEmitter<Array<any>>(true);
//Returns object if selected
@Output() private itemSelect: EventEmitter<any> = new EventEmitter<any>(true);
@ViewChild('searchField') searchField: ElementRef;
//Used to see whether or not input field is currently focused
private _isFocused: boolean = false;
get isFocused(): boolean {
return this._isFocused;
}
set isFocused(isFocused: boolean) {
this._isFocused = isFocused;
this.setFieldVal();
}
private activeItem: any;
@Output() private focus: EventEmitter<any> = new EventEmitter<any>(true);
@Output() private blur: EventEmitter<any> = new EventEmitter<any>(true);
@Output() private change: EventEmitter<any> = new EventEmitter<any>(true);
@Output() private keyup: EventEmitter<any> = new EventEmitter<any>(true);
@Output() private keydown: EventEmitter<any> = new EventEmitter<any>(true);
ngDoCheck(): void {
//When items array has changed
if (!_.isEqual(this.itemsOld, this.items)) {
this.itemsOld = _.clone(this.items);
this.filter(this.query);
}
}
private setAutocompleteItems(): void {
if (this.autocompleteOptions == null) {
this.autocompleteItems = this.filteredItems;
} else {
this.autocompleteItems = _.intersection(this.filteredItems, this.autocompleteOptions);
}
}
private focused(event: any): void {
this.isFocused = true;
this.focus.emit(event);
}
private blurred(event: any): void {
this.blur.emit(event);
}
private clickedOutside(): void {
this.isFocused = false;
}
private setFieldVal(): void {
if (!this.isFocused && (this.blurString !== null)) {
this.searchFieldVal = this.blurString;
} else {
this.searchFieldVal = this.query;
}
}
private changed(event: any): void {
this.change.emit(event);
}
private keyupped(event: any): void {
this.query = event.target.value;
this.keyup.emit(event);
}
private keydowned(event: any): void {
this.keydown.emit(event);
}
private filter(query: string): Array<any> {
let original = this._filteredItems;
if (_.trim(query) === '') { //if query is empty
if (this.returnAllOnEmpty) {
this.filteredItems = this.items;
} else {
this.filteredItems = this.items.slice(this.items.length); //return empty array with same type
}
} else {
this.filteredItems = _.filter(this.items, (item) => {
return this.isMatch(this.getItemProperty(item), query);
});
}
//Emit search if filtered items changed
if (!_.isEqual(original, this.filteredItems)) {
this.search.emit(this.filteredItems);
}
return this.filteredItems;
}
private getItemProperty(item: any): string {
if (this.property !== null) {
if (_.has(item, this.property)) { //If property exists within object
return String(_.get(item, this.property));
} else {
return '';
}
} else {
return String(item);
}
}
private isMatch(source: any, query: string): boolean {
//prevent null reference exception
if (source == null) source = '';
if (query == null) query = '';
if (this.exactMatch) {
return source.replace(/\s/g, '').toUpperCase() === query.replace(/\s/g, '').toUpperCase();
} else {
return source
.replace(/\s/g, '') //remove spaces
.toUpperCase() //ignore case
.indexOf(query.replace(/\s/g, '').toUpperCase()) !== -1; //contains query
}
}
private isActive(item: any): boolean {
return this.activeItem === item;
}
private selectActive(item: any): void {
this.activeItem = item;
}
private itemSelected(item: any): void {
//set query to whatever item's string is
this.query = this.getItemProperty(item);
this.isFocused = false;
this.itemSelect.emit(item);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment