Skip to content

Commit

Permalink
feat(typeahead): introduce TypeaheadMatch model
Browse files Browse the repository at this point in the history
TODO
  • Loading branch information
mixomat authored and valorkin committed Oct 7, 2016
1 parent fdddbde commit 80fccab
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 107 deletions.
8 changes: 6 additions & 2 deletions components/typeahead/typeahead-container.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { asNativeElements } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TypeaheadContainerComponent } from './typeahead-container.component';
import { TypeaheadOptions } from './typeahead-options.class';
import { asNativeElements } from '@angular/core';
import { TypeaheadMatch } from './typeahead-match.class';

describe('Component: TypeaheadContainer', () => {
let fixture:ComponentFixture<TypeaheadContainerComponent>;
Expand Down Expand Up @@ -65,7 +66,10 @@ describe('Component: TypeaheadContainer', () => {

beforeEach(() => {
component.query = 'fo';
component.matches = ['foo', 'food'];
component.matches = [
new TypeaheadMatch({id: 0, name: 'foo'}, 'foo'),
new TypeaheadMatch({id: 1, name: 'food'}, 'food')
];
fixture.detectChanges();

matches = asNativeElements(fixture.debugElement.queryAll(By.css('.dropdown-menu li')));
Expand Down
113 changes: 27 additions & 86 deletions components/typeahead/typeahead-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,30 @@ import { positionService } from '../position';
import { TypeaheadOptions } from './typeahead-options.class';
import { TypeaheadUtils } from './typeahead-utils';
import { TypeaheadDirective } from './typeahead.directive';
import { TypeaheadMatch } from './typeahead-match.class';

const bs4 = `
<div class="dropdown-menu"
[ngStyle]="{top: top, left: left, display: 'block'}"
(mouseleave)="focusLost()">
<template *ngIf="hasGroups()" ngFor let-group [ngForOf]="groups">
<h6 class="dropdown-header">{{group}}</h6>
<div *ngIf="!itemTemplate">
<template ngFor let-match let-i="index" [ngForOf]="matches">
<h6 *ngIf="match.isHeader()" class="dropdown-header">{{match}}</h6>
<div *ngIf="!match.isHeader() && !itemTemplate">
<a href="#"
*ngFor="let match of matchesByGroup(group)"
class="dropdown-item"
(click)="selectMatch(match, $event)"
(mouseenter)="selectActive(match)"
[class.active]="isActive(match)"
[innerHtml]="hightlight(match, query)"></a>
</div>
<div *ngIf="itemTemplate">
<div *ngIf="!match.isHeader() && itemTemplate">
<a href="#"
*ngFor="let match of matchesByGroup(group); let i = index"
class="dropdown-item"
(click)="selectMatch(match, $event)"
(mouseenter)="selectActive(match)"
[class.active]="isActive(match)">
<template [ngTemplateOutlet]="itemTemplate"
[ngOutletContext]="{item: match, index: i}">
</template>
</a>
</div>
</template>
<template [ngIf]="!hasGroups()">
<div *ngIf="!itemTemplate">
<a href="#"
*ngFor="let match of matches"
class="dropdown-item"
(click)="selectMatch(match, $event)"
(mouseenter)="selectActive(match)"
[class.active]="isActive(match)"
[innerHtml]="hightlight(match, query)"></a>
</div>
<div *ngIf="itemTemplate">
<a href="#"
*ngFor="let match of matches; let i = index"
class="dropdown-item"
(click)="selectMatch(match, $event)"
(mouseenter)="selectActive(match)"
[class.active]="isActive(match)">
<template [ngTemplateOutlet]="itemTemplate"
[ngOutletContext]="{item: match, index: i}">
[ngOutletContext]="{item: match.item, index: i}">
</template>
</a>
</div>
Expand All @@ -64,9 +40,9 @@ const bs3 = `
<ul class="dropdown-menu"
[ngStyle]="{top: top, left: left, display: 'block'}"
(mouseleave)="focusLost()">
<template *ngIf="hasGroups()" ngFor let-group [ngForOf]="groups">
<li class="dropdown-header">{{group}}</li>
<li *ngFor="let match of matchesByGroup(group); let i = index"
<template ngFor let-match let-i="index" [ngForOf]="matches">
<li *ngIf="match.isHeader()" class="dropdown-header">{{match}}</li>
<li *ngIf="!match.isHeader()"
[class.active]="isActive(match)"
(mouseenter)="selectActive(match)">
<a href="#"
Expand All @@ -79,26 +55,7 @@ const bs3 = `
(click)="selectMatch(match, $event)"
tabindex="-1">
<template [ngTemplateOutlet]="itemTemplate"
[ngOutletContext]="{item: match, index: i}">
</template>
</a>
</li>
</template>
<template [ngIf]="!hasGroups()">
<li *ngFor="let match of matches; let i = index"
[class.active]="isActive(match)"
(mouseenter)="selectActive(match)">
<a href="#"
*ngIf="!itemTemplate"
(click)="selectMatch(match, $event)"
tabindex="-1"
[innerHtml]="hightlight(match, query)"></a>
<a href="#"
*ngIf="itemTemplate"
(click)="selectMatch(match, $event)"
tabindex="-1">
<template [ngTemplateOutlet]="itemTemplate"
[ngOutletContext]="{item: match, index: i}">
[ngOutletContext]="{item: match.item, index: i}">
</template>
</a>
</li>
Expand All @@ -121,53 +78,34 @@ export class TypeaheadContainerComponent {
public left:string;
public display:string;

private _active:any;
private _matches:Array<any> = [];
private _field:string;
private _groupField:string;
private _groups:string[] = [];
private _active:TypeaheadMatch;
private _matches:Array<TypeaheadMatch> = [];
private placement:string;

public constructor(element:ElementRef, options:TypeaheadOptions) {
this.element = element;
Object.assign(this, options);
}

public get matches():Array<any> {
public get matches():Array<TypeaheadMatch> {
return this._matches;
}

public get groups():string[] {
return this._groups;
}

public matchesByGroup(group:string):Array<any> {
return this.matches.filter((match:any) => TypeaheadUtils.getValueFromObject(match, this._groupField) === group);
}

public get itemTemplate():TemplateRef<any> {
return this.parent ? this.parent.typeaheadItemTemplate : undefined;
}

public set matches(value:Array<any>) {
public set matches(value:Array<TypeaheadMatch>) {
this._matches = value;

if (this._matches.length > 0) {
this._active = this._matches[0];
if (this._active.isHeader()) {
this.nextActiveMatch();
}
}
}

public set field(value:string) {
this._field = value;
}

public set groupField(value:string) {
this._groupField = value;
}

public set groups(value:string[]) {
this._groups = value;
}

public position(hostEl:ElementRef):void {
this.top = '0px';
this.left = '0px';
Expand All @@ -188,22 +126,29 @@ export class TypeaheadContainerComponent {
this._active = this.matches[index - 1 < 0
? this.matches.length - 1
: index - 1];
if (this._active.isHeader()) {
this.prevActiveMatch();
}

}

public nextActiveMatch():void {
let index = this.matches.indexOf(this._active);
this._active = this.matches[index + 1 > this.matches.length - 1
? 0
: index + 1];
if (this._active.isHeader()) {
this.nextActiveMatch();
}
}

protected selectActive(value:any):void {
this.isFocused = true;
this._active = value;
}

protected hightlight(item:any, query:any):string {
let itemStr:string = TypeaheadUtils.getValueFromObject(item, this._field);
protected hightlight(match:TypeaheadMatch, query:any):string {
let itemStr:string = match.value;
let itemStrHelper:string = (this.parent && this.parent.typeaheadLatinize
? TypeaheadUtils.latinize(itemStr)
: itemStr).toLowerCase();
Expand Down Expand Up @@ -240,10 +185,6 @@ export class TypeaheadContainerComponent {
return this._active === value;
}

public hasGroups():boolean {
return this._groups && this._groups.length > 0;
}

private selectMatch(value:any, e:Event = void 0):boolean {
if (e) {
e.stopPropagation();
Expand Down
14 changes: 14 additions & 0 deletions components/typeahead/typeahead-match.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

export class TypeaheadMatch {

public constructor(readonly item:any, readonly value:string = item, private header:boolean = false) {
}

public isHeader():boolean {
return this.header;
}

public toString():string {
return this.value;
}
}
3 changes: 2 additions & 1 deletion components/typeahead/typeahead.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TypeaheadDirective } from './typeahead.directive';
import { Observable } from 'rxjs';
import { TypeaheadMatch } from './typeahead-match.class';

const template = `
<input [(ngModel)]="selectedState" [typeahead]="states" (typeaheadOnSelect)="typeaheadOnSelect($event)">
Expand Down Expand Up @@ -100,7 +101,7 @@ describe('Directive: Typeahead', () => {
fixture.detectChanges();
tick(100);

expect(directive.matches).toEqual(['Alabama', 'Alaska']);
expect(directive.matches).toEqual([new TypeaheadMatch('Alabama'), new TypeaheadMatch('Alaska')]);
}));

it('should result in 0 matches, when input does not match', fakeAsync(() => {
Expand Down
44 changes: 26 additions & 18 deletions components/typeahead/typeahead.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/toArray';

import { ComponentsHelper } from '../utils/components-helper.service';
import { TypeaheadMatch } from './typeahead-match.class';

/* tslint:disable-next-line */
const KeyboardEvent = (global as any).KeyboardEvent as KeyboardEvent;
Expand Down Expand Up @@ -58,8 +59,7 @@ export class TypeaheadDirective implements OnInit {
public isTypeaheadOptionsListActive:boolean = false;

private keyUpEventEmitter:EventEmitter<any> = new EventEmitter();
private _matches:Array<any>;
private _groups:Array<any> = [];
private _matches:Array<TypeaheadMatch>;
private placement:string = 'bottom-left';
private popup:ComponentRef<TypeaheadContainerComponent>;

Expand Down Expand Up @@ -203,7 +203,7 @@ export class TypeaheadDirective implements OnInit {
this.popup.instance.position(this.viewContainerRef.element);
this.container = this.popup.instance;
this.container.parent = this;
// This improves the speedas it won't have to be done for each list item
// This improves the speed as it won't have to be done for each list item
let normalizedQuery = (this.typeaheadLatinize
? TypeaheadUtils.latinize(this.ngControl.control.value)
: this.ngControl.control.value).toString()
Expand All @@ -212,9 +212,6 @@ export class TypeaheadDirective implements OnInit {
? TypeaheadUtils.tokenize(normalizedQuery, this.typeaheadWordDelimiters, this.typeaheadPhraseDelimiters)
: normalizedQuery;
this.container.matches = this._matches;
this.container.groups = this._groups;
this.container.field = this.typeaheadOptionField;
this.container.groupField = this.typeaheadGroupField;
this.element.nativeElement.focus();
}

Expand All @@ -230,7 +227,7 @@ export class TypeaheadDirective implements OnInit {
.debounceTime(this.typeaheadWaitMs)
.mergeMap(() => this.typeahead)
.subscribe(
(matches:string[]) => {
(matches:any[]) => {
this.finalizeAsyncCall(matches);
},
(err:any) => {
Expand All @@ -252,7 +249,7 @@ export class TypeaheadDirective implements OnInit {
.toArray();
})
.subscribe(
(matches:string[]) => {
(matches:any[]) => {
this.finalizeAsyncCall(matches);
},
(err:any) => {
Expand Down Expand Up @@ -298,8 +295,7 @@ export class TypeaheadDirective implements OnInit {
}

private finalizeAsyncCall(matches:any[]):void {
this.limitMatches(matches);
this.updateGroups();
this.prepareMatches(matches);

this.typeaheadLoading.emit(false);
this.typeaheadNoResults.emit(!this.hasMatches());
Expand All @@ -318,24 +314,36 @@ export class TypeaheadDirective implements OnInit {
this.container.query = this.typeaheadSingleWords
? TypeaheadUtils.tokenize(normalizedQuery, this.typeaheadWordDelimiters, this.typeaheadPhraseDelimiters)
: normalizedQuery;
this.container.groups = this._groups;
this.container.matches = this._matches;
} else {
this.show();
}
}

private limitMatches(matches:any[]):void {
this._matches = matches.slice(0, this.typeaheadOptionsLimit);
}
private prepareMatches(options:any[]):void {
let limited:any[] = options.slice(0, this.typeaheadOptionsLimit);

private updateGroups():void {
if (this.typeaheadGroupField) {
this._groups = this._matches
.map((match:any) => TypeaheadUtils.getValueFromObject(match, this.typeaheadGroupField))
let matches:TypeaheadMatch[] = [];

// extract all group names
let groups = limited
.map((option:any) => TypeaheadUtils.getValueFromObject(option, this.typeaheadGroupField))
.filter((v:string, i:number, a:Array<any>) => a.indexOf(v) === i);

groups.forEach((group:string) => {
// add group header to array of matches
matches.push(new TypeaheadMatch(group, group, true));

// add each item of group to array of matches
matches = matches.concat(limited
.filter((option:any) => TypeaheadUtils.getValueFromObject(option, this.typeaheadGroupField) === group)
.map((option:any) => new TypeaheadMatch(option, TypeaheadUtils.getValueFromObject(option, this.typeaheadOptionField))));
});

this._matches = matches;
} else {
this._groups = [];
this._matches = limited.map((option:any) => new TypeaheadMatch(option, TypeaheadUtils.getValueFromObject(option, this.typeaheadOptionField)));
}
}

Expand Down

0 comments on commit 80fccab

Please sign in to comment.