Skip to content

Commit

Permalink
(feat): Add template support. #20
Browse files Browse the repository at this point in the history
  • Loading branch information
meeroslav committed Dec 21, 2017
1 parent 7771aca commit 1a56a69
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 44 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ export class AppModule { }
## API
### DOM element properties
`Type-ahead` supports following properties:
- `suggestions: TypeaheadSuggestions` - List or observable list of elements which represent set of possible suggestions. For more information on type check [TypeaheadSuggestions](#typeaheadsuggestions)
- `suggestions: TypeaheadSuggestions` - List or observable list of elements which represent set of possible suggestions. For more information on type check [TypeaheadSuggestions](#typeaheadsuggestions).
Default value is `[]`.
- `custom: boolean` - Flag indicating whether custom values are allowed
- `itemTemplate: TemplateRef` - Custom template template for items in suggestions list and badges in multi select scenario. Exposed properties are `item` and `index`.
- `custom: boolean` - Flag indicating whether custom values are allowed.
Default value is `true`.
- `multi: boolean` - Flag indicating whether control accepts multiple values/array of values.
Default value is `false`.
- `complex: boolean` - Flag indicating whether suggestion represents an Object instead of simple string.
- `complex: boolean` - Flag indicating whether suggestion represents an Object instead of simple string.
Default value is `false`.
- `idField: string` - Only for `complex` suggestions. Object's indicator property name used as a value for form component. Can be just in combination with `multi`, but automatically cancels `custom`.
- `idField: string` - Only for `complex` suggestions. Object's indicator property name used as a value for form component. Can be just in combination with `multi`, but automatically cancels `custom`.
Default value is `id`.
- `nameField: string` - Only for `complex` suggestions. Object's name property. This value will be shown in dropdown and in the input, but `idField` will be saved to form.
Default value is `name`.
Expand Down
10 changes: 8 additions & 2 deletions config/webpack.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {
devtool: 'inline-source-map',

resolve: {
extensions: ['.ts', '.js'],
extensions: ['.ts', '.js', '.html'],
modules: [helpers.root('src'), 'node_modules']
},

Expand Down Expand Up @@ -45,7 +45,13 @@ module.exports = {
module: "commonjs",
removeComments: true
},
exclude: [/\.e2e\.ts$/]
}, {
test: /\.ts$/,
loader: 'angular2-template-loader',
}, {
test: /\.html$/,
loader: 'raw-loader',
include: helpers.root('src')
}, {
enforce: 'post',
test: /\.(js|ts)$/,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-type-ahead",
"version": "1.0.4",
"version": "1.0.5",
"description": "Typeahead multi-select dropdown component for angular",
"scripts": {
"test": "karma start",
Expand Down
24 changes: 17 additions & 7 deletions src/typeahead.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
Component, forwardRef, Input, OnDestroy, ElementRef, Output, OnChanges,
EventEmitter, AfterViewInit, Inject, OnInit, Renderer2, HostListener, HostBinding, SimpleChanges
EventEmitter, AfterViewInit, Inject, OnInit, Renderer2, HostListener, HostBinding, SimpleChanges, TemplateRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
Expand Down Expand Up @@ -55,7 +55,7 @@ const sanitizeString = (text: string) =>
:host[disabled] input {
background-color: inherit;
}
:host .typeahead-badge {
:host .type-ahead-badge {
white-space: nowrap;
cursor: pointer;
}
Expand All @@ -73,8 +73,14 @@ const sanitizeString = (text: string) =>
}
`],
template: `
<span [ngClass]="settings.tagClass" class="typeahead-badge" *ngFor="let value of values">
<!-- default options item template -->
<ng-template #taItemTemplate let-value="item">
{{ complex ? value[nameField] : value }}
</ng-template>
<span [ngClass]="settings.tagClass" class="type-ahead-badge" *ngFor="let value of values; let i = index">
<ng-template [ngTemplateOutlet]="itemTemplate || taItemTemplate"
[ngTemplateOutletContext]="{ item: value, index: i, complex: complex, nameField: nameField }"></ng-template>
<span *ngIf="!isDisabled" aria-hidden="true" (click)="removeValue(value)"
[ngClass]="settings.tagRemoveIconClass">×</span>
</span>
Expand All @@ -87,12 +93,13 @@ const sanitizeString = (text: string) =>
<i *ngIf="!isDisabled" (click)="toggleDropdown()" tabindex="-1"
[ngClass]="settings.dropdownToggleClass"></i>
<div role="menu" [attr.class]="dropDownClass" *ngIf="matches.length || !custom">
<button *ngFor="let match of matches" type="button" role="menuitem" tabindex="-1"
<button *ngFor="let match of matches; let i = index" type="button" role="menuitem" tabindex="-1"
[ngClass]="settings.dropdownMenuItemClass"
(mouseup)="handleButton($event, match)"
(keydown)="handleButton($event, match)"
(keyup)="handleButton($event, match)">
{{ complex ? match[nameField] : match }}
<ng-template [ngTemplateOutlet]="itemTemplate || taItemTemplate"
[ngTemplateOutletContext]="{ item: match, index: i, complex: complex, nameField: nameField }"></ng-template>
</button>
<div role="menuitem" *ngIf="!matches.length && !custom" tabindex="-1" aria-disabled="true" disabled="true"
[ngClass]="settings.dropdownMenuItemClass">
Expand All @@ -105,8 +112,11 @@ const sanitizeString = (text: string) =>
export class TypeaheadComponent implements ControlValueAccessor, AfterViewInit, OnDestroy, OnInit, OnChanges {
/** suggestions list - array of strings, objects or Observable */
@Input() suggestions: TypeaheadSuggestions = [];
/** template for items in drop down */
// @Input() public suggestionTemplate: TemplateRef<any>;
/**
* template for items in drop down
* properties exposed are item and index
**/
@Input() itemTemplate: TemplateRef<any>;
/** field to use from objects as name */
@Input() nameField = 'name';
/** field to use from objects as id */
Expand Down
50 changes: 21 additions & 29 deletions tests/typeahead.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
import { ReactiveFormsModule } from '@angular/forms';
import { TypeaheadComponent } from '../src/typeahead.component';
import { Observable } from 'rxjs';
import { TypeaheadSuggestions } from '../src/typeahead.interface';
import { asNativeElements } from '@angular/core';
import { By } from '@angular/platform-browser';

const KEY_UP = 'keyup';
const KEY_DOWN = 'keydown';
Expand All @@ -14,17 +15,19 @@ describe('TypeaheadComponent', () => {
fixture: ComponentFixture<TypeaheadComponent>,
component: TypeaheadComponent;

beforeEach(() => {
beforeEach((done) => {
jasmine.clock().uninstall();
jasmine.clock().install();

const module = TestBed.configureTestingModule({
TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [TypeaheadComponent]
});

fixture = module.createComponent(TypeaheadComponent);
component = fixture.componentInstance;
TestBed.compileComponents().then(() => {
fixture = TestBed.createComponent(TypeaheadComponent);
component = fixture.componentInstance;
done();
});
});

it('should initialize component', () => {
Expand All @@ -43,24 +46,21 @@ describe('TypeaheadComponent', () => {

it('should copy observable suggestions to allmatches', fakeAsync(() => {
const suggestions: string[] = ['ABC', 'DEF', 'GHI'];
const suggestions$: TypeaheadSuggestions = Observable.of(suggestions);
component.suggestions = suggestions$;
component.suggestions = Observable.of(suggestions);
fixture.detectChanges();
tick();
expect(component.allMatches).toEqual(suggestions);
}));

it('should set simple value', fakeAsync(() => {
const suggestions = ['ABC', 'DEF', 'GHI'];
component.suggestions = suggestions;
component.suggestions = ['ABC', 'DEF', 'GHI'];
component.value = 'ABC';
fixture.detectChanges();
expect((<any> component)._input.value).toEqual('ABC');
}));

it('should set multiple values', fakeAsync(() => {
const suggestions = ['ABC', 'DEF', 'GHI'];
component.suggestions = suggestions;
component.suggestions = ['ABC', 'DEF', 'GHI'];
component.multi = true;
component.value = ['ABC', 'DEF'];
fixture.detectChanges();
Expand All @@ -78,22 +78,18 @@ describe('TypeaheadComponent', () => {
}));

it('should set multiple complex values', fakeAsync(() => {
const suggestions = [{ name: 'ABC', id: 'A' }, { name: 'DEF', id: 'D' }, { name: 'GHI', id: 'G' }];
component.suggestions = suggestions;
component.suggestions = [{ name: 'ABC', id: 'A' }, { name: 'DEF', id: 'D' }, { name: 'GHI', id: 'G' }];
component.complex = true;
component.multi = true;
fixture.detectChanges();
tick();

component.value = ['A', 'D'];
fixture.detectChanges();

expect(component.values).toEqual([{ name: 'ABC', id: 'A' }, { name: 'DEF', id: 'D' }]);
expect((<any> component)._input.value).toEqual('');
}));

it('should show dropdown on input', fakeAsync(() => {
const suggestions = ['ABC', 'DEF', 'GHI'];
component.suggestions = suggestions;
component.suggestions = ['ABC', 'DEF', 'GHI'];
fixture.detectChanges();
tick();

Expand All @@ -105,8 +101,7 @@ describe('TypeaheadComponent', () => {
}));

it('should hide dropdown on escape', fakeAsync(() => {
const suggestions = ['ABC', 'DEF', 'GHI'];
component.suggestions = suggestions;
component.suggestions = ['ABC', 'DEF', 'GHI'];
fixture.detectChanges();
tick();

Expand All @@ -120,8 +115,7 @@ describe('TypeaheadComponent', () => {
}));

it('should limit the number of suggestions shown', fakeAsync(() => {
const suggestions = ['batman', 'flash', 'aquaman', 'orin', 'robin', 'spectre'];
component.suggestions = suggestions;
component.suggestions = ['batman', 'flash', 'aquaman', 'orin', 'robin', 'spectre'];
component.settings.suggestionsLimit = 2;
fixture.detectChanges();

Expand All @@ -139,8 +133,7 @@ describe('TypeaheadComponent', () => {
}));

it('multi - should be able to enter new items with Enter key', fakeAsync(() => {
const suggestions = ['batman', 'flash', 'aquaman', 'orin', 'robin', 'spectre'];
component.suggestions = suggestions;
component.suggestions = ['batman', 'flash', 'aquaman', 'orin', 'robin', 'spectre'];
component.multi = true;
fixture.detectChanges();
const customValue1 = 'hulk';
Expand All @@ -160,14 +153,13 @@ describe('TypeaheadComponent', () => {
jasmine.clock().tick(50);
fixture.detectChanges();

const customItems = fixture.nativeElement.querySelectorAll('.typeahead-badge');
const customItems = asNativeElements(fixture.debugElement.queryAll(By.css('.type-ahead-badge')));
expect(customItems[0].innerText).toContain(customValue1);
expect(customItems[1].innerText).toContain(customValue2);
}));

it('multi - should delete item with Backspace key', fakeAsync(() => {
const suggestions = ['batman', 'flash', 'aquaman', 'orin', 'robin', 'spectre'];
component.suggestions = suggestions;
component.suggestions = ['batman', 'flash', 'aquaman', 'orin', 'robin', 'spectre'];
component.multi = true;
fixture.detectChanges();
const customValue1 = 'hulk';
Expand All @@ -186,7 +178,7 @@ describe('TypeaheadComponent', () => {
jasmine.clock().tick(50);
fixture.detectChanges();

const customItems = fixture.nativeElement.querySelectorAll('.typeahead-badge');
const customItems = asNativeElements(fixture.debugElement.queryAll(By.css('.type-ahead-badge')));
expect(customItems.length).toBe(0);
}));

Expand Down
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"declaration": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"stripInternal": true,
"allowSyntheticDefaultImports": true,
"noUnusedParameters": false,
"types": [
"hammerjs",
"jasmine",
Expand Down
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
devtool: '#source-map',

resolve: {
extensions: ['.ts', '.js', '.css', '.scss']
extensions: ['.ts', '.js']
},

entry: helpers.root('index.ts'),
Expand Down

0 comments on commit 1a56a69

Please sign in to comment.