Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typeahead): add feature to do multiple search w/ specified delimiter #5784

Merged
merged 2 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions cypress/full/typeahead_page_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,41 @@ describe('Typeahead demo page test suite', () => {
});
});

describe('Multiple Search', () => {
beforeEach(() => typeahead.scrollToMenu('Multiple search'));

const multipleSearch = typeahead.exampleDemosArr.multipleSearch;
const formTemplate = 'Model: ';
const stateForCheck = 'New York';
const textToInput = 'New Mexico,New York';
const textWithDelimiters = 'New Mexico,';

it('example contains typeahead input and typeahead card with "Model:"', () => {
typeahead.isElementVisible(multipleSearch, typeahead.inputSelector);
typeahead.isPreviewExist(multipleSearch, formTemplate);
});

it('When user uses "," and types "New Mexico," then drop-down with 20 states should be shown',
() => {
typeahead.clearInputAndSendKeys(multipleSearch, textWithDelimiters);
typeahead.isElementVisible(multipleSearch, typeahead.activeDropdown);
typeahead.isDropdownHasNItems(typeahead.dropdownBtn, 20);
});

it('when user starts to type "New Mexico,New York" then a drop-down with the second match is shown', () => {
typeahead.clearInputAndSendKeys(multipleSearch, textToInput);
typeahead.isElementVisible(multipleSearch, typeahead.activeDropdown);
});

it('when user clicks on any item in typeahead drop-down, then typeahead container appends with a selected State',
() => {
typeahead.clearInputAndSendKeys(multipleSearch, textToInput);
typeahead.clickByText(typeahead.activeDropdown, stateForCheck);
typeahead.isPreviewExist(multipleSearch, stateForCheck);
typeahead.isInputValueEqual(multipleSearch, textToInput);
});
});

describe('Dropup', () => {
beforeEach(() => typeahead.scrollToMenu('Dropup'));

Expand Down
1 change: 1 addition & 0 deletions cypress/support/typeahead.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class TypeaheadPo extends BaseComponent {
groupingResults: 'demo-typeahead-grouping',
ignoreSpaceAndOrder: 'demo-typeahead-single-world',
delimiters: 'demo-typeahead-phrase-delimiters',
multipleSearch: 'demo-typeahead-multiple-search',
dropUp: 'demo-typeahead-dropup',
onBlur: 'demo-typeahead-on-blur',
appendToBody: 'demo-typeahead-container',
Expand Down
4 changes: 3 additions & 1 deletion demo/src/app/components/+typeahead/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DemoTypeaheadShowOnBlurComponent } from './show-on-blur/show-on-blur';
import { DemoTypeaheadSingleWorldComponent } from './single-world/single-world';
import { DemoTypeaheadAsyncHttpRequestComponent } from './async-http-request/async-http-request';
import { DemoTypeaheadOrderingComponent } from './ordering/ordering';
import { DemoTypeaheadMultipleSearchComponent } from './multiple-search/multiple-search';

export const DEMO_COMPONENTS = [
DemoTypeaheadAdaptivePositionComponent,
Expand Down Expand Up @@ -57,5 +58,6 @@ export const DEMO_COMPONENTS = [
DemoTypeaheadShowOnBlurComponent,
DemoTypeaheadSingleWorldComponent,
DemoTypeaheadAsyncHttpRequestComponent,
DemoTypeaheadOrderingComponent
DemoTypeaheadOrderingComponent,
DemoTypeaheadMultipleSearchComponent
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pre class="card card-block card-header">Model: {{selected | json}}</pre>
<input [(ngModel)]="selected"
[typeahead]="states"
[typeaheadMultipleSearch]="true"
typeaheadMultipleSearchDelimiters=",|"
class="form-control">
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Component } from '@angular/core';

@Component({
selector: 'demo-typeahead-multiple-search',
templateUrl: './multiple-search.html'
})
export class DemoTypeaheadMultipleSearchComponent {
selected: string;
states = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Dakota',
'North Carolina',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming'
];
}
17 changes: 17 additions & 0 deletions demo/src/app/components/+typeahead/typeahead-section.list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { NgApiDocComponent, NgApiDocConfigComponent } from '../../docs/api-docs'
import { DemoTypeaheadFirstItemActiveComponent } from './demos/first-item-active/first-item-active';
import { DemoTypeaheadAsyncHttpRequestComponent } from './demos/async-http-request/async-http-request';
import { DemoTypeaheadOrderingComponent } from './demos/ordering/ordering';
import { DemoTypeaheadMultipleSearchComponent } from './demos/multiple-search/multiple-search';

export const demoComponentContent: ContentSection[] = [
{
Expand Down Expand Up @@ -307,6 +308,22 @@ export const demoComponentContent: ContentSection[] = [
component: require('!!raw-loader!./demos/ordering/ordering.ts'),
html: require('!!raw-loader!./demos/ordering/ordering.html'),
outlet: DemoTypeaheadOrderingComponent
},
{
title: 'Multiple search',
anchor: 'multiple-search',
component: require('!!raw-loader!./demos/multiple-search/multiple-search.ts'),
html: require('!!raw-loader!./demos/multiple-search/multiple-search.html'),
description: `
<p>Set <code>typeaheadMultipleSearch</code> input property to <code>true</code>
and provide the multiple search delimiter by <code>typeaheadMultipleSearchDelimiters</code>
to be able to search typeahead again after using one of the provided delimiters. Default delimiter
is "<code>,</code>" if <code>typeaheadMultipleSearchDelimiters</code> is not used.
After picking a first value from typeahead
dropdown, type "<code>,</code>" or "<code>|</code>" and then next value can be searched.
This is demo with delimeters "<code>,</code>" and "<code>|</code>"</p>
`,
outlet: DemoTypeaheadMultipleSearchComponent
}
]
},
Expand Down
11 changes: 11 additions & 0 deletions demo/src/ng-api-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4166,6 +4166,17 @@ export const ngdoc: any = {
"type": "number",
"description": "<p>minimal no of characters that needs to be entered before\ntypeahead kicks-in. When set to 0, typeahead shows on focus with full\nlist of options (limited as normal by typeaheadOptionsLimit)</p>\n"
},
{
"name": "typeaheadMultipleSearch",
"type": "boolean",
"description": "<p>Can be used to conduct a search of multiple items and have suggestion not for the\nwhole value of the input but for the value that comes after a delimiter provided via\ntypeaheadMultipleSearchDelimiters attribute. This option can only be used together with\ntypeaheadSingleWords option if typeaheadWordDelimiters and typeaheadPhraseDelimiters\nare different from typeaheadMultipleSearchDelimiters to avoid conflict in determining\nwhen to delimit multiple searches and when a single word.</p>\n"
},
{
"name": "typeaheadMultipleSearchDelimiters",
"defaultValue": ",",
"type": "string",
"description": "<p>should be used only in case typeaheadMultipleSearch attribute is true.\nSets the multiple search delimiter to know when to start a new search. Defaults to comma.\nIf space needs to be used, then explicitly set typeaheadWordDelimiters to something else than space\nbecause space is used by default OR set typeaheadSingleWords attribute to false if you don&#39;t need\nto use it together with multiple search.</p>\n"
},
{
"name": "typeaheadOptionField",
"type": "string",
Expand Down
116 changes: 116 additions & 0 deletions src/spec/typeahead.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,4 +695,120 @@ describe('Directive: Typeahead', () => {
});
});
});

describe('if typeaheadMultipleSearch is true', () => {
beforeEach(
fakeAsync(() => {
directive.typeahead = component.statesString;
directive.typeaheadMultipleSearch = true;
fixture.detectChanges();
tick(100);
})
);

it('and comma entered after one value is picked from typeahead dropdown, should show all available matches',
fakeAsync(() => {
inputElement.value = 'Alabama';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(1);
expect(directive.matches[0].item).toBe('Alabama');

inputElement.value = 'Alabama,';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(component.statesString.length);
}));

it(`and \'Ala\' is entered after \',\' or \'|\' when these used for typeaheadMultipleSearchDelimiters,
should give matches for Alaska and Alabama`,
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ',|';
inputElement.value = 'Alabama';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(1);
expect(directive.matches[0].item).toBe('Alabama');

inputElement.value = 'Alabama,Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');

inputElement.value = 'Alabama|Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');
}));

it('and use, should give matches for Alaska and Alabama',
fakeAsync(() => {
inputElement.value = 'Alabama';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(1);
expect(directive.matches[0].item).toBe('Alabama');

inputElement.value = 'Alabama,Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');
}));

it('and use comma for typeaheadWordDelimiters, should throw error',
fakeAsync(() => {
directive.typeaheadWordDelimiters = ',';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).toThrowError();
}));

it('and use comma for typeaheadPhraseDelimiters, should throw error',
fakeAsync(() => {
directive.typeaheadPhraseDelimiters = ',';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).toThrowError();
}));

it('and use space for typeaheadMultipleSearchDelimiters, should throw error',
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ' ';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).toThrowError();
}));

it('use space for typeaheadMultipleSearchDelimiters and \',\' for typeaheadWordDelimiters, should not throw error',
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ' ';
directive.typeaheadWordDelimiters = ',';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).not.toThrowError();
}));

it('and use space for typeaheadMultipleSearchDelimiters and typeaheadSingleWords is false, should not throw error',
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ' ';
directive.typeaheadSingleWords = false;
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).not.toThrowError();
}));

});
});
29 changes: 25 additions & 4 deletions src/typeahead/typeahead-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,34 @@ export function escapeRegexp(queryToEscape: string): string {

/* tslint:disable */
export function tokenize(str: string,
wordRegexDelimiters = ' ',
phraseRegexDelimiters = ''): Array<string> {
wordRegexDelimiters = ' ',
phraseRegexDelimiters = '', delimitersForMultipleSearch?: string): Array<string> {

let result: string[] = [];
if (!delimitersForMultipleSearch) {
result = tokenizeWordsAndPhrases(str, wordRegexDelimiters, phraseRegexDelimiters);
} else {
const multipleSearchRegexStr = `([${delimitersForMultipleSearch}]+)`;
const delimitedTokens = str.split(new RegExp(multipleSearchRegexStr, 'g'));
const lastToken = delimitedTokens[delimitedTokens.length - 1];
if (lastToken > '') {
if (wordRegexDelimiters && phraseRegexDelimiters) {
result = tokenizeWordsAndPhrases(lastToken, wordRegexDelimiters, phraseRegexDelimiters);
} else {
result.push(lastToken);
}
}
}

return result;
}

function tokenizeWordsAndPhrases(str: string, wordRegexDelimiters: string, phraseRegexDelimiters: string): Array<string> {
const result: string[] = [];
/* tslint:enable */
const regexStr = `(?:[${phraseRegexDelimiters}])([^${phraseRegexDelimiters}]+)` +
`(?:[${phraseRegexDelimiters}])|([^${wordRegexDelimiters}]+)`;
`(?:[${phraseRegexDelimiters}])|([^${wordRegexDelimiters}]+)`;
const preTokenized: string[] = str.split(new RegExp(regexStr, 'g'));
const result: string[] = [];
const preTokenizedLength: number = preTokenized.length;
let token: string;
const replacePhraseDelimiters = new RegExp(`[${phraseRegexDelimiters}]+`, 'g');
Expand Down
Loading