File

src/app/shared/ng2-typeahead.ts

Metadata

providers TYPEAHEAD_CONTROL_VALUE_ACCESSOR
selector typeahead
styles .typeahead { position: relative; width: 100%; text-align: left; vertical-align: top; padding-bottom: 2.5em; } .typeahead-input { border-color: transparent; position: absolute; z-index: 10; background-color: transparent; background-repeat: no-repeat; background-position: right 10px; background-size: 28px 18px; } .typeahead-input-has-selection { background-color: #f5f5f5; border: 1px solid #4c4845; color: #008fca; } .typeahead-typeahead { color: rgb(128, 128, 128); position: absolute; z-index: 5; text-align: start; background-color: rgb(255, 255, 255); } .typeahead-suggestions { position: absolute; top: 42px; overflow-y: auto; color: #666666; border-radius: 3px; padding: 0; background-color: #f5f5f5; width: 100%; max-height: 18em !important; border: 1px solid #e0e0e0; z-index: 100; } .typeahead-suggestions ul { list-style-type: none; padding-left: 0; margin-top: 3px; } .typeahead-suggestions ul li { padding: 6px !important; font-size: 0.9em; border-bottom: 1px solid #e0e0e0; } .typeahead-suggestion-active { background-color: #008fca; color: #ffffff; }
template <div class="typeahead">
  <input #inputElement
         [placeholder]="placeholder"
         [(ngModel)]="input"
         type="text"
         [ngClass]="{'typeahead-input': true, 'typeahead-input-has-selection': hasSelection()}"
         typeahead="off"
         spellcheck="false"
         (keyup)="inputKeyUp($event)"
         (keydown)="inputKeyDown($event)"
         (focus)="inputFocus($event)"
         (blur)="inputBlur($event)"
         [disabled]="isDisabled">
  <input type="text"
         class="typeahead-typeahead"
         [(ngModel)]="typeahead"
         typeahead="off"
         spellcheck="false"
         disabled="true">
  <div #suggestionsContainer
       class="typeahead-suggestions"
       [hidden]="!areSuggestionsVisible">
    <ul (mouseout)="suggestionsMouseOut($event)">
      <li *ngFor="let suggestion of suggestions"
          (mouseover)="suggestionMouseOver(suggestion)"
          (mousedown)="suggestionMouseDown(suggestion)"
          [ngClass]="{'typeahead-suggestion-active': activeSuggestion===suggestion}">{{ suggestion[displayProperty] }}</li>
    </ul>
  </div>
</div>

Inputs

displayProperty

The property of a list item that should be displayed.

Type: string

Default value: name

list

The complete list of items.

Type: any[]

maxSuggestions

The maximum number of suggestions to display.

Type: number

placeholder

Input element placeholder text.

Type: string

searchProperty

The property of a list item that should be used for matching.

Type: string

Default value: name

Outputs

suggestionSelected

Event that occurs when a suggestion is selected.

$event type: EventEmitter

Constructor

constructor()

Creates and initializes a new typeahead component.

Methods

Public inputKeyDown
inputKeyDown(event: KeyboardEvent)

Called when a keydown event is fired on the input element.

Returns: void
Public setActiveSuggestion
setActiveSuggestion(suggestion: any)

Sets the active (highlighted) suggestion.

Returns: void
Public getActiveSuggestionIndex
getActiveSuggestionIndex()

Gets the index of the active suggestion within the suggestions list.

Returns: void
Public indexOfObject
indexOfObject(array: any[], property: string, value: string)

Gets the index of an object in a list by matching a property value.

Returns: void
Public inputKeyUp
inputKeyUp(event: KeyboardEvent)

Called when a keyup event is fired on the input element.

Returns: void
Public inputFocus
inputFocus(event: FocusEvent)

Called when a focus event is fired on the input element.

Returns: void
Public inputBlur
inputBlur(event: Event)

Called when a blur event is fired on the input element.

Returns: void
Public suggestionMouseOver
suggestionMouseOver(suggestion: any)

Called when a mouseover event is fired on a suggestion element.

Returns: void
Public suggestionMouseDown
suggestionMouseDown(suggestion: any)

Called when a mousedown event is fired on a suggestion element.

Returns: void
Public suggestionsMouseOut
suggestionsMouseOut(event: MouseEvent)

Called when a mouseout event is fired on the suggestions element.

Returns: void
Public populateSuggestions
populateSuggestions()

Fills the suggestions list with items matching the input pattern.

Returns: void
Public populateTypeahead
populateTypeahead()

Sets the typeahead input element's value based on the active suggestion.

Returns: void
Public selectSuggestion
selectSuggestion(suggestion: any)

Selects a suggestion.

Returns: void
Public blurInputElement
blurInputElement()

Blurs the input element in order to "lock" the value and prevent partial editing.

Returns: void
Public hasSelection
hasSelection()

Indicates whether a suggestion has been selected.

Returns: void

Properties

abstract
abstract: any
Private activeSuggestion
activeSuggestion: any

The active (highlighted) suggestion.

Private areSuggestionsVisible
areSuggestionsVisible: boolean
Default value: false

Indicates whether the suggestions are visible.

Private input
input: string

The input element's value.

Private inputElement
inputElement: any

Handle to the input element.

Private isDisabled
isDisabled: boolean
Default value: false

Indicates whether the control is disabled.

Private onChangeCallback
onChangeCallback: (_: any) => void
Default value: noop

Placeholder for a callback which is later provided by the Control Value Accessor.

Private onTouchedCallback
onTouchedCallback: () => void
Default value: noop

Placeholder for a callback which is later provided by the Control Value Accessor.

Private previousInput
previousInput: string

The previously entered input string.

Private selectedSuggestion
selectedSuggestion: any

The currently selected suggestion.

Private suggestions
suggestions: any[]

The filtered list of suggestions.

Private typeahead
typeahead: string

The typeahead element's value. This element is displayed behind the input element.

value
value: any

Get accessor.
Set accessor including call the onchange callback.

import { Component, Input, Output, EventEmitter, ViewChild, OnInit, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

const noop = () => {
};

export const TYPEAHEAD_CONTROL_VALUE_ACCESSOR:any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => Typeahead),
  multi: true
};


@Component({
  selector: 'typeahead',
  template: `
    <div class="typeahead">
      <input #inputElement
             [placeholder]="placeholder"
             [(ngModel)]="input"
             type="text"
             [ngClass]="{'typeahead-input': true, 'typeahead-input-has-selection': hasSelection()}"
             typeahead="off"
             spellcheck="false"
             (keyup)="inputKeyUp($event)"
             (keydown)="inputKeyDown($event)"
             (focus)="inputFocus($event)"
             (blur)="inputBlur($event)"
             [disabled]="isDisabled">
      <input type="text"
             class="typeahead-typeahead"
             [(ngModel)]="typeahead"
             typeahead="off"
             spellcheck="false"
             disabled="true">
      <div #suggestionsContainer
           class="typeahead-suggestions"
           [hidden]="!areSuggestionsVisible">
        <ul (mouseout)="suggestionsMouseOut($event)">
          <li *ngFor="let suggestion of suggestions"
              (mouseover)="suggestionMouseOver(suggestion)"
              (mousedown)="suggestionMouseDown(suggestion)"
              [ngClass]="{'typeahead-suggestion-active': activeSuggestion===suggestion}">{{ suggestion[displayProperty] }}</li>
        </ul>
      </div>
    </div>
  `,
  styles: [`
    .typeahead {
      position: relative;
      width: 100%;
      text-align: left;
      vertical-align: top;
      padding-bottom: 2.5em;
    }
    .typeahead-input {
      border-color: transparent;
      position: absolute;
      z-index: 10;
      background-color: transparent;
      background-repeat: no-repeat;
      background-position: right 10px;
      background-size: 28px 18px;
    }
    .typeahead-input-has-selection {
      background-color: #f5f5f5;
      border: 1px solid #4c4845;
      color: #008fca;
    }
    .typeahead-typeahead {
      color: rgb(128, 128, 128);
      position: absolute;
      z-index: 5;
      text-align: start;
      background-color: rgb(255, 255, 255);
    }
    .typeahead-suggestions {
      position: absolute;
      top: 42px;
      overflow-y: auto;
      color: #666666;
      border-radius: 3px;
      padding: 0;
      background-color: #f5f5f5;
      width: 100%;
      max-height: 18em !important;
      border: 1px solid #e0e0e0;
      z-index: 100;
    }
    .typeahead-suggestions ul {
      list-style-type: none;
      padding-left: 0;
      margin-top: 3px;
    }
    .typeahead-suggestions ul li {
      padding: 6px !important;
      font-size: 0.9em;
      border-bottom: 1px solid #e0e0e0;
    }
    .typeahead-suggestion-active {
      background-color: #008fca;
      color: #ffffff;
    }
  `],
  providers: [TYPEAHEAD_CONTROL_VALUE_ACCESSOR]
})
export class Typeahead implements OnInit, ControlValueAccessor {
  abstract;

  /**
   * The complete list of items.
   */
  @Input() list:any[] = [];

  /**
   * Input element placeholder text.
   */
  @Input() placeholder:string = '';

  /**
   * The property of a list item that should be used for matching.
   */
  @Input() searchProperty:string = 'name';

  /**
   * The property of a list item that should be displayed.
   */
  @Input() displayProperty:string = 'name';

  /**
   * The maximum number of suggestions to display.
   */
  @Input() maxSuggestions:number = -1;

  /**
   * Event that occurs when a suggestion is selected.
   */
  @Output() suggestionSelected = new EventEmitter<any>();

  /**
   * Handle to the input element.
   */
  @ViewChild('inputElement') private inputElement:any;

  /**
   * The input element's value.
   */
  private input:string;

  /**
   * The typeahead element's value. This element is displayed behind the input element.
   */
  private typeahead:string;

  /**
   * The previously entered input string.
   */
  private previousInput:string;

  /**
   * The filtered list of suggestions.
   */
  private suggestions:any[] = [];

  /**
   * Indicates whether the suggestions are visible.
   */
  private areSuggestionsVisible:boolean = false;

  /**
   * The currently selected suggestion.
   */
  private selectedSuggestion:any;

  /**
   * The active (highlighted) suggestion.
   */
  private activeSuggestion:any;

  /**
   * Indicates whether the control is disabled.
   */
  private isDisabled:boolean = false;

  /**
   * Creates and initializes a new typeahead component.
   */
  constructor() {
  }

  /**
   * Implement this interface to execute custom initialization logic after your
   * directive's data-bound properties have been initialized.
   *
   * ngOnInit is called right after the directive's data-bound properties have
   * been checked for the first time, and before any of its
   * children have been checked. It is invoked only once when the directive is
   * instantiated.
   */
  public ngOnInit() {
  }

  /**
   * Placeholder for a callback which is later provided by the Control Value Accessor.
   */
  private onTouchedCallback:() => void = noop;

  /**
   * Placeholder for a callback which is later provided by the Control Value Accessor.
   */
  private onChangeCallback:(_:any) => void = noop;

  /**
   * Get accessor.
   */
  get value():any {
    return this.selectedSuggestion;
  }

  /**
   * Set accessor including call the onchange callback.
   */
  set value(v:any) {
    if (v !== this.selectedSuggestion) {
      this.selectSuggestion(v);
      this.onChangeCallback(v);
    }
  }

  /**
   * From ControlValueAccessor interface.
   */
  writeValue(value:any) {
    if (value !== this.selectedSuggestion) {
      this.selectSuggestion(value);
    }
  }

  /**
   * From ControlValueAccessor interface.
   */
  registerOnChange(fn:any) {
    this.onChangeCallback = fn;
  }

  /**
   * From ControlValueAccessor interface.
   */
  registerOnTouched(fn:any) {
    this.onTouchedCallback = fn;
  }

  /**
   * Sets the disabled state of the control.
   * @param isDisabled
   */
  setDisabledState(isDisabled:boolean):void {
    this.isDisabled = isDisabled;
  }

  /**
   * Called when a keydown event is fired on the input element.
   */
  public inputKeyDown(event:KeyboardEvent) {
    if (event.which === 9 || event.keyCode === 9) { // TAB
      // Only enter this branch if suggestions are displayed
      if (!this.areSuggestionsVisible) {
        return;
      }

      // Select the first suggestion
      this.selectSuggestion(this.activeSuggestion);

      // Remove all but the first suggestion
      this.suggestions.splice(1);

      // Hide the suggestions
      this.areSuggestionsVisible = false;

      event.preventDefault();
    } else if (event.which === 38 || event.keyCode === 38) { // UP
      // Find the active suggestion in the list
      let activeSuggestionIndex = this.getActiveSuggestionIndex();

      // If not found, then activate the first suggestion
      if (activeSuggestionIndex === -1) {
        this.setActiveSuggestion(this.suggestions[0]);
        return;
      }

      if (activeSuggestionIndex === 0) {
        // Go to the last suggestion
        this.setActiveSuggestion(this.suggestions[this.suggestions.length - 1]);
      } else {
        // Decrement the suggestion index
        this.setActiveSuggestion(this.suggestions[activeSuggestionIndex - 1]);
      }
    } else if (event.which === 40 || event.keyCode === 40) { // DOWN
      // Find the active suggestion in the list
      let activeSuggestionIndex = this.getActiveSuggestionIndex();

      // If not found, then activate the first suggestion
      if (activeSuggestionIndex === -1) {
        this.setActiveSuggestion(this.suggestions[0]);
        return;
      }

      if (activeSuggestionIndex === (this.suggestions.length - 1)) {
        // Go to the first suggestion
        this.setActiveSuggestion(this.suggestions[0]);
      } else {
        // Increment the suggestion index
        this.setActiveSuggestion(this.suggestions[activeSuggestionIndex + 1]);
      }
    } else if ((event.which === 10 || event.which === 13 ||
      event.keyCode === 10 || event.keyCode === 13) &&
      this.areSuggestionsVisible) { // ENTER

      // Select the active suggestion
      this.selectSuggestion(this.activeSuggestion);

      // Hide the suggestions
      this.areSuggestionsVisible = false;

      event.preventDefault();
    }
  }

  /**
   * Sets the active (highlighted) suggestion.
   */
  public setActiveSuggestion(suggestion:any) {
    this.activeSuggestion = suggestion;
    this.populateTypeahead();
  }

  /**
   * Gets the index of the active suggestion within the suggestions list.
   */
  public getActiveSuggestionIndex() {
    let activeSuggestionIndex = -1;
    if (this.activeSuggestion != null) {
      activeSuggestionIndex = this.indexOfObject(this.suggestions, this.searchProperty, this.activeSuggestion[this.searchProperty]);
    }
    return activeSuggestionIndex;
  }

  /**
   * Gets the index of an object in a list by matching a property value.
   */
  public indexOfObject(array:any[], property:string, value:string) {
    if (array == null || array.length === 0) return -1;
    let index = -1;
    for (let i = 0; i < array.length; i++) {
      if (array[i][property] != null && array[i][property] === value) {
        index = i;
      }
    }
    return index;
  }

  /**
   * Called when a keyup event is fired on the input element.
   */
  public inputKeyUp(event:KeyboardEvent) {
    // Ignore TAB, UP, and DOWN since they are processed by the keydown handler
    if (event.which === 9 || event.keyCode === 9 ||
      event.which === 38 || event.keyCode === 38 ||
      event.which === 40 || event.keyCode === 40) {
      return;
    }

    // When the input is cleared
    if (this.input == null || this.input.length === 0) {
      console.debug(`When the input is cleared`);
      this.typeahead = '';
      this.populateSuggestions();
      return;
    }

    // If the suggestion matches the input, then return
    if (this.selectedSuggestion != null) {
      console.debug(`If the suggestion matches the input, then return`);
      if (this.selectedSuggestion[this.displayProperty] === this.input) {
        return;
      }
    }

    // Repopulate the suggestions
    this.previousInput = this.input;
    this.populateSuggestions();
    this.populateTypeahead();
  }

  /**
   * Called when a focus event is fired on the input element.
   */
  public inputFocus(event:FocusEvent) {
    // If the element is receiving focus and it has a selection, then
    // clear the selection. This helps prevent partial editing
    if (this.selectedSuggestion != null) {
      this.selectSuggestion(null);
      this.input = null;
      this.populateTypeahead();
    }

    // Re-populate the suggestions
    this.populateSuggestions();

    // If we have suggestions
    if (this.suggestions.length > 0) {
      // Set the typeahead to a slice of the first suggestion
      this.populateTypeahead();

      // Show/hide the suggestions
      this.areSuggestionsVisible = this.suggestions.length > 0;
    }
  }

  /**
   * Called when a blur event is fired on the input element.
   */
  public inputBlur(event:Event) {
    this.typeahead = '';
    this.areSuggestionsVisible = false;
    this.onTouchedCallback();
  }

  /**
   * Called when a mouseover event is fired on a suggestion element.
   */
  public suggestionMouseOver(suggestion:any) {
    this.setActiveSuggestion(suggestion);
  }

  /**
   * Called when a mousedown event is fired on a suggestion element.
   */
  public suggestionMouseDown(suggestion:any) {
    this.selectSuggestion(suggestion);
  }

  /**
   * Called when a mouseout event is fired on the suggestions element.
   */
  public suggestionsMouseOut(event:MouseEvent) {
    this.setActiveSuggestion(null);
  }

  /**
   * Fills the suggestions list with items matching the input pattern.
   */
  public populateSuggestions() {
    // Capture variables scoped to the component
    let searchProperty = this.searchProperty;
    let input = this.input;

    // Confirm that we have a search property
    if (searchProperty == null || searchProperty.length === 0) {
      console.error('The input attribute `searchProperty` must be provided');
      return;
    }

    // Handle empty input
    if (input == null || input.length === 0) {
      // No input yet
      this.suggestions = [];
      this.areSuggestionsVisible = false;
      return;
    }

    // Check that we have data
    if (this.list == null || this.list.length === 0) return;

    // Filter the suggestions
    this.suggestions = this.list.filter(function (item) {
      return item[searchProperty].toLowerCase().indexOf(input.toLowerCase()) > -1;
    });

    // Limit the suggestions (if applicable)
    if (this.maxSuggestions > -1) {
      this.suggestions = this.suggestions.slice(0, this.maxSuggestions);
    }

    if (this.suggestions.length === 0) {
      // No suggestions, so clear the typeahead
      this.typeahead = '';
    } else {
      // Set the typeahead value
      this.populateTypeahead();
      // Make the first suggestion active
      this.activeSuggestion = this.suggestions[0];
    }

    // Show/hide the suggestions
    this.areSuggestionsVisible = this.suggestions.length > 0;
  }

  /**
   * Sets the typeahead input element's value based on the active suggestion.
   */
  public populateTypeahead() {
    // Clear the typeahead when there is no active suggestion
    if (this.activeSuggestion == null || !this.areSuggestionsVisible) {
      this.typeahead = '';
      return;
    }
    // Set the typeahead value
    this.typeahead = this.input + (this.activeSuggestion[this.displayProperty] || '').slice(this.input.length);
  }

  /**
   * Selects a suggestion.
   */
  public selectSuggestion(suggestion:any) {
    // Set the variable
    this.selectedSuggestion = suggestion;

    // Hide the suggestions
    this.areSuggestionsVisible = false;

    // Notify the parent component
    this.suggestionSelected.emit(suggestion);

    // Other form operations
    if (this.selectedSuggestion != null) {
      // Set the values of the input elements
      this.input = suggestion[this.displayProperty];
      this.typeahead = suggestion[this.displayProperty];

      // Blur the input so we can "lock" the selected suggestion
      this.blurInputElement();
    }
  }

  /**
   * Blurs the input element in order to "lock" the value and prevent partial editing.
   */
  public blurInputElement() {
    if (this.inputElement && this.inputElement.nativeElement) {
      this.inputElement.nativeElement.blur();
    }
  }

  /**
   * Indicates whether a suggestion has been selected.
   */
  public hasSelection() {
    return this.selectedSuggestion != null;
  }
}

results matching ""

    No results matching ""