Skip to content

Commit

Permalink
feat: search input now uses multi input filter logic (#672)
Browse files Browse the repository at this point in the history
* search input should use multi input filter logic

* start working on displayFn

* formatting
  • Loading branch information
mikerodonnell89 committed Apr 5, 2019
1 parent d4b3457 commit aa0ea24
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ <h1>&lt;fd-combobox-input&gt;</h1>
{ name: 'placeholder', description: 'Placeholder for the input.'},
{ name: 'fillOnSelect', description: 'Boolean, whether to automatically fill the input with the selected value. Defaults to true.' },
{ name: 'closeOnSelect', description: 'Boolean, whether to automatically close the popover when a value is selected. Defaults to true.' },
{ name: 'compact', description: 'Boolean, set to \'true\' for the compact combobox input.'}
{ name: 'compact', description: 'Boolean, set to \'true\' for the compact combobox input.'},
{ name: 'filterFn', description: 'Function. Filter function to apply to the list.' }
]
}"></properties>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!--fd-search-input [dropdownValues]="values"
[placeholder]="'Search here...'"
[(ngModel)]="searchTerm"
[displayFn]="displayFunc"></fd-search-input-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component } from '@angular/core';

@Component({
selector: 'fd-search-input-displaywith-example',
templateUrl: './search-input-displaywith-example.component.html'
})
export class SearchInputDisplaywithExampleComponent {
values = [
{text: 'Apple'},
{text: 'Banana'},
{text: 'Pineapple'},
{text: 'Tomato'}
];

searchTerm = '';

displayFunc(obj: any): string {
if (obj) {
return obj.toLocaleUpperCase();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<fd-search-input [ngModel]="searchTerm"
(ngModelChange)="modelChange($event)"
[dropdownValues]="dropdownSubject"
<fd-search-input [(ngModel)]="searchTerm"
[dropdownValues]="dropdownValues"
[placeholder]="'Type to filter the list...'"
(itemClicked)="selectItem($event)"
[usingCustomFilter]="true"> <!-- This flag is important! -->
[filterFn]="customFilter">
</fd-search-input>
<br />
<span>Search Term: {{searchTerm}}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ export class SearchInputDynamicExampleComponent {
{ text: 'Pineapple', callback: () => {alert('Pineapple Clicked!')} }
];

dropdownSubject = this.dropdownValues;

modelChange(event: any): void {
this.searchTerm = event;
this.dropdownSubject = this.dropdownValues.filter(value =>
value.text.toLocaleLowerCase().includes(this.searchTerm.toLocaleLowerCase()));
}

selectItem(event: any) {
this.selected = event.text;
}

customFilter(content: any[], searchTerm: string): any[] {
const search = searchTerm.toLocaleLowerCase();
return content.filter(item =>
item.text.toLocaleLowerCase().startsWith(search)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ <h1>&lt;fd-search-input&gt;</h1>
{ name: 'compact', description: 'Boolean, set to \'true\' for the compact search input.'},
{ name: 'glyph', description: 'String, use to select what icon appears in the button. Defaults to search.' },
{ name: 'highlight', description: 'Boolean, whether to highlight the search term in the results, if it is included. Defaults to true.' },
{ name: 'filterFn', description: 'Function. Filter function to apply to the list. See example below.' },
{ name: 'fillOnSelect', description: 'Boolean, whether to automatically fill the input with the selected value. Defaults to true.' },
{ name: 'closeOnSelect', description: 'Boolean, whether to automatically close the popover when a value is selected. Defaults to true.' },
{ name: 'usingCustomFilter', description: 'Boolean, whether you are handling the filtration by yourself, or allowing the built-in pipe to do it. Defaults to false.' }
{ name: 'closeOnSelect', description: 'Boolean, whether to automatically close the popover when a value is selected. Defaults to true.' }
],
outputs: [
{ name: 'itemClicked', description: 'Fired when an item in the menu is clicked, or the enter key is pressed while the item is focused. Returns the term.'}
Expand All @@ -40,7 +40,7 @@ <h2>Standard Search Input</h2>

<separator></separator>
<h2>Custom Filter</h2>
<description>The search input can be used with a custom filter that is decided outside the component.<br /><br />To accomplish this, you will need to set the <code>[usingCustomFilter]</code> flag to true, like shown below. To display nothing when the user first clicks inside, simply set the starting Subject value to an empty array.</description>
<description>The input supports custom filters through the <code>filterFn</code> input function. Simply pass a function which accepts an array and a search term, and then returns the desired filtered array.</description>
<component-example [name]="'ex2'">
<fd-search-input-dynamic-example></fd-search-input-dynamic-example>
</component-example>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as searchInputDynHtml from '!raw-loader!./examples/search-input-dynamic
import * as searchInputDynTs from '!raw-loader!./examples/search-input-dynamic-example.component.ts';
import * as searchInputAsyncHtml from '!raw-loader!./examples/search-input-async-example.component.html';
import * as searchInputAsyncTs from '!raw-loader!./examples/search-input-async-example.component.ts';
import * as searchInputDisplayHtml from '!raw-loader!./examples/search-input-displaywith-example.component.html';
import * as searchInputDisplayTs from '!raw-loader!./examples/search-input-displaywith-example.component.ts';

@Component({
selector: 'app-search-input',
Expand All @@ -21,6 +23,9 @@ export class SearchInputDocsComponent {
searchInputAsyncHtml = searchInputAsyncHtml;
searchInputAsyncTs = searchInputAsyncTs;

displayHtml = searchInputDisplayHtml;
displayTs = searchInputDisplayTs;

searchInputAsyncData = JSON.stringify([
{text: 'Apple', alertMessage: 'Apple Clicked'},
{text: 'Banana', alertMessage: 'Banana Clicked'},
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/documentation/documentation.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ import { InfiniteScrollBasicExampleComponent } from './containers/infinite-scrol
import { TableCheckboxesExampleComponent } from './containers/table/examples/table-checkboxes-example.component';
import { SearchInputAsyncExampleComponent } from './containers/search-input/examples/search-input-async-example.component';
import { SearchInputDynamicExampleComponent } from './containers/search-input/examples/search-input-dynamic-example.component';
import { SearchInputDisplaywithExampleComponent } from './containers/search-input/examples/search-input-displaywith-example.component';
import { ListSingleSelectExampleComponent } from './containers/list/examples/list-single-select-example.component';
import { FileInputDocsComponent } from './containers/file-input/file-input-docs.component';
import { FileInputExampleComponent } from './containers/file-input/examples/file-input-example/file-input-example.component';
Expand Down Expand Up @@ -464,6 +465,7 @@ const ROUTES: Routes = [
SearchInputExampleComponent,
SearchInputAsyncExampleComponent,
SearchInputDynamicExampleComponent,
SearchInputDisplaywithExampleComponent,
ShellbarBasicExampleComponent,
ShellbarCollapsibleExampleComponent,
SideNavigationCollapsedExampleComponent,
Expand Down
20 changes: 14 additions & 6 deletions library/src/lib/combobox-input/combobox-input.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<fd-popover [(isOpen)]="isOpen"
[fillControl]="true"
class="fd-combobox-popover-custom"
[ngClass]="{'fd-popover-body--display-none': (dropdownValues | fdSearch:inputText).length < 1}">
[ngClass]="{'fd-popover-body--display-none': displayedValues && !displayedValues.length}">
<fd-popover-control>
<div class="fd-combobox-control">
<div class="fd-input-group fd-input-group--after" [ngClass]="{'fd-input-group--compact': compact}">
Expand All @@ -11,6 +11,7 @@
(keyup)="onInputKeyupHandler()"
[disabled]="disabled"
[(ngModel)]="inputText"
(ngModelChange)="handleSearchTermChange()"
placeholder="{{placeholder}}">
<span class="fd-input-group__addon fd-input-group__addon--after fd-input-group__addon--button">
<button class="fd-button--light sap-icon--navigation-down-arrow"
Expand All @@ -21,16 +22,22 @@
</div>
</div>
</fd-popover-control>
<fd-popover-body *ngIf="(dropdownValues | fdSearch:inputText).length">
<fd-popover-body>
<fd-menu>
<ul fd-menu-list>
<li fd-menu-item
*ngFor="let term of dropdownValues | fdSearch:inputText"
<li fd-menu-item *ngFor="let term of displayedValues"
(click)="onMenuClickHandler($event, term)"
(keydown)="onMenuKeydownHandler($event, term)">
<a
tabindex="0"><strong>{{inputText}}</strong>{{inputText && inputText.length ? term.text.substr(inputText.length) : term.text}}</a>
<a tabindex="0">
<div *ngIf="highlight && inputTextValue && inputTextValue.toLocaleLowerCase">
{{ term.text.substr(0, term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase())) }}<strong>{{term.text.substr(term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase()), inputText.length)}}</strong>{{ term.text.substring(term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase()) + inputText.length, term.text.length) }}
</div>
<div *ngIf="!highlight || !inputTextValue">
{{ term.text }}
</div>
</a>
</li>

<li fd-menu-item
(click)="newItemCallback($event)"
(keydown)="newItemKeydownHandler($event)">
Expand All @@ -41,3 +48,4 @@
</fd-popover-body>
</fd-popover>
</div>

Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import { PopoverModule } from '../popover/popover.module';
import { SearchInputModule } from '../search-input/search-input.module';

import { ComboboxInputComponent } from './combobox-input.component';
import { FdSearchPipe } from '../search-input/search-input.component';

describe('ComboboxInputComponent', () => {
let component: ComboboxInputComponent;
let pipe: FdSearchPipe;
let fixture: ComponentFixture<ComboboxInputComponent>;

beforeEach(async(() => {
Expand All @@ -27,7 +25,6 @@ describe('ComboboxInputComponent', () => {
{text: 'Apple', callback: () => {}}
];
component.searchFunction = () => {};
pipe = new FdSearchPipe();
fixture.detectChanges();
});

Expand Down
16 changes: 7 additions & 9 deletions library/src/lib/search-input/search-input.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<fd-popover [(isOpen)]="isOpen"
[fillControl]="true"
class="fd-search-input-popover-custom"
[ngClass]="{'fd-popover-body--display-none': (usingCustomFilter ? dropdownValues : dropdownValues | fdSearch:inputText) &&
(usingCustomFilter ? dropdownValues : dropdownValues | fdSearch:inputText).length < 1}">
[ngClass]="{'fd-popover-body--display-none': displayedValues && !displayedValues.length}">
<fd-popover-control>
<div *ngIf="!inShellbar" class="fd-combobox-control">
<div class="fd-input-group fd-input-group--after" [ngClass]="{'fd-input-group--compact': compact}">
Expand All @@ -11,6 +10,7 @@
(keyup)="onInputKeyupHandler()"
[disabled]="disabled"
[(ngModel)]="inputText"
(ngModelChange)="handleSearchTermChange()"
placeholder="{{placeholder}}">
<span class="fd-input-group__addon fd-input-group__addon--after fd-input-group__addon--button">
<button class="fd-button--light" [ngClass]="('sap-icon--' + this.glyph)"></button>
Expand All @@ -27,23 +27,21 @@
(keyup)="onInputKeyupHandler()"
[disabled]="disabled"
[(ngModel)]="inputText"
(ngModelChange)="handleSearchTermChange()"
placeholder="{{placeholder}}"
(click)="shellbarSearchInputClicked($event)">
</div>
</div>
</div>
</fd-popover-control>
<fd-popover-body *ngIf="(usingCustomFilter ? dropdownValues : dropdownValues | fdSearch:inputText) &&
(usingCustomFilter ? dropdownValues : dropdownValues | fdSearch:inputText).length">
<fd-popover-body *ngIf="displayedValues && displayedValues.length">
<fd-menu>
<ul fd-menu-list>
<li fd-menu-item
*ngFor="let term of (usingCustomFilter ? dropdownValues : dropdownValues | fdSearch:inputText)"
<li fd-menu-item *ngFor="let term of displayedValues"
(click)="onMenuClickHandler($event, term)" (keydown)="onMenuKeydownHandler($event, term)">
<a tabindex="0">
<div *ngIf="highlight && inputTextValue">
{{ term.text.substr(0, term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase())) }}
<strong>{{inputText}}</strong>{{ term.text.substring(term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase()) + inputText.length, term.text.length) }}
<div *ngIf="highlight && inputTextValue && inputTextValue.toLocaleLowerCase">
{{ term.text.substr(0, term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase())) }}<strong>{{term.text.substr(term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase()), inputText.length)}}</strong>{{ term.text.substring(term.text.toLocaleLowerCase().indexOf(inputText.toLocaleLowerCase()) + inputText.length, term.text.length) }}
</div>
<div *ngIf="!highlight || !inputTextValue">
{{ term.text }}
Expand Down
22 changes: 2 additions & 20 deletions library/src/lib/search-input/search-input.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { FormsModule } from '@angular/forms';
import { MenuModule } from '../menu/menu.module';
import { PopoverModule } from '../popover/popover.module';

import { FdSearchPipe, SearchInputComponent } from './search-input.component';
import { SearchInputComponent } from './search-input.component';

describe('SearchInputComponent', () => {
let component: SearchInputComponent;
let pipe: FdSearchPipe;
let fixture: ComponentFixture<SearchInputComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [FdSearchPipe, SearchInputComponent],
declarations: [SearchInputComponent],
imports: [FormsModule, MenuModule, PopoverModule]
}).compileComponents();
}));
Expand All @@ -25,7 +24,6 @@ describe('SearchInputComponent', () => {
{text: 'Apple', callback: () => {}}
];
component.searchFunction = () => {};
pipe = new FdSearchPipe();
fixture.detectChanges();
});

Expand Down Expand Up @@ -175,20 +173,4 @@ describe('SearchInputComponent', () => {
expect(component.onChange).toBe('function');
expect(component.onTouched).toBe('function');
});

it('should test the search pipe', () => {
const searchTerms = [
{text: 'term1'},
{text: 'term2'}
];
let result = pipe.transform(searchTerms, 't');
expect(result).toEqual([
{text: 'term1'},
{text: 'term2'}
]);
result = pipe.transform(searchTerms, 'term1');
expect(result).toEqual([
{text: 'term1'}
]);
});
});
56 changes: 37 additions & 19 deletions library/src/lib/search-input/search-input.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
Component,
EventEmitter,
forwardRef, HostBinding,
forwardRef,
HostBinding,
Input,
OnChanges,
OnInit,
Output,
Pipe,
PipeTransform,
SimpleChanges,
QueryList,
ViewChild,
ViewChildren
Expand All @@ -25,12 +27,14 @@ import { MenuItemDirective } from '../menu/menu-item.directive';
}
]
})
export class SearchInputComponent implements ControlValueAccessor {
export class SearchInputComponent implements ControlValueAccessor, OnInit, OnChanges {
@Input()
dropdownValues: any[];
dropdownValues: any[] = [];

@Input()
usingCustomFilter: boolean = false;
filterFn: Function = this.defaultFilter;

displayedValues: any[] = [];

@Input()
disabled: boolean;
Expand Down Expand Up @@ -165,28 +169,42 @@ export class SearchInputComponent implements ControlValueAccessor {
registerOnTouched(fn) {
this.onTouched = fn;
}

private handleClickActions(term): void {
if (this.closeOnSelect) {
this.isOpen = false;
}

if (this.fillOnSelect) {
this.inputText = term.text;
}
}
}

@Pipe({
name: 'fdSearch'
})
export class FdSearchPipe implements PipeTransform {
transform(value: any, input: string) {
if (input && typeof input === 'string') {
input = input.toLocaleLowerCase();
return value.filter((result: any) => {
return result.text.toLocaleLowerCase().startsWith(input);
});
ngOnInit() {
if (this.dropdownValues) {
this.displayedValues = this.dropdownValues;
}
return value;
}

ngOnChanges(changes: SimpleChanges) {
if (this.dropdownValues && (changes.dropdownValues || changes.searchTerm)) {
if (this.inputText) {
this.displayedValues = this.filterFn(this.dropdownValues, this.inputText);
} else {
this.displayedValues = this.dropdownValues;
}
}
}

handleSearchTermChange(): void {
this.displayedValues = this.filterFn(this.dropdownValues, this.inputText);
}

private defaultFilter(contentArray: any[], searchTerm: string): any[] {
const searchLower = searchTerm.toLocaleLowerCase();
return contentArray.filter(item => {
if (item) {
return item.text.toLocaleLowerCase().includes(searchLower);
}
});
}
}

0 comments on commit aa0ea24

Please sign in to comment.