Skip to content

Commit

Permalink
feat(autocomplete): added new forceFilter() method to allow for dyn…
Browse files Browse the repository at this point in the history
…amically updating the options of an autocomplete (#391)
  • Loading branch information
DRiFTy17 authored Sep 27, 2023
1 parent deca1a3 commit 8c4edaf
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/lib/autocomplete/autocomplete-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ export interface IAutocompletePopupConfiguration {
export interface IAutocompleteSelectEventData<T = any> {
value: T;
}

export interface IAutocompleteForceFilterOptions {
preserveValue: boolean;
}
20 changes: 19 additions & 1 deletion src/lib/autocomplete/autocomplete-foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IListItemComponent } from '../list';
import { IListDropdownConfig, IListDropdownOption, ListDropdownAsyncStyle, ListDropdownFooterBuilder, ListDropdownHeaderBuilder, ListDropdownOptionBuilder } from '../list-dropdown';
import { IListDropdownAwareFoundation, ListDropdownAwareFoundation } from '../list-dropdown/list-dropdown-aware-foundation';
import { IAutocompleteAdapter } from './autocomplete-adapter';
import { AutocompleteFilterCallback, AutocompleteMode, AutocompleteOptionBuilder, AutocompleteSelectedTextBuilder, AUTOCOMPLETE_CONSTANTS, IAutocompleteOption, IAutocompleteOptionGroup, IAutocompleteSelectEventData } from './autocomplete-constants';
import { AutocompleteFilterCallback, AutocompleteMode, AutocompleteOptionBuilder, AutocompleteSelectedTextBuilder, AUTOCOMPLETE_CONSTANTS, IAutocompleteForceFilterOptions, IAutocompleteOption, IAutocompleteOptionGroup, IAutocompleteSelectEventData } from './autocomplete-constants';
import { getSelectedOption, isOptionType, optionEqualPredicate, OptionType } from './autocomplete-utils';

export interface IAutocompleteFoundation extends IListDropdownAwareFoundation {
Expand All @@ -25,6 +25,7 @@ export interface IAutocompleteFoundation extends IListDropdownAwareFoundation {
matchKey: string | null | undefined;
appendOptions(options: IAutocompleteOption[] | IAutocompleteOptionGroup[]): void;
beforeValueChange: (value: any) => boolean | Promise<boolean>;
forceFilter(opts: IAutocompleteForceFilterOptions): void;
}

/**
Expand Down Expand Up @@ -117,6 +118,23 @@ export class AutocompleteFoundation extends ListDropdownAwareFoundation implemen
}
}

public async forceFilter({ preserveValue }: IAutocompleteForceFilterOptions): Promise<void> {
// Clear any existing options since they are expected to no longer be valid
this._options = [];

// Execute the filter callback to fetch new options if the consumer has provided any.
// This allows us to update the current value(s) with new label(s) if there are any matches
await this._executeFilter(true, true);

// Edge case, but if the consumer has a need to preserve the existing selection if it doesn't exist in the new options, this will support that
if (preserveValue) {
this._options.push(...this._selectedOptions as IAutocompleteOption<any>[] & IAutocompleteOptionGroup[]);
}

// This will update our current state, but it's expected that consumers will manage their own values so it's likely that this will be called again soon
this._applyValue(this._values);
}

private _attachListeners(): void {
this._adapter.addInputListener('click', this._clickListener);
this._adapter.addInputListener('focus', this._focusListener);
Expand Down
11 changes: 10 additions & 1 deletion src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { PopupComponent } from '../popup';
import { SkeletonComponent } from '../skeleton';
import { TextFieldComponent } from '../text-field';
import { AutocompleteAdapter } from './autocomplete-adapter';
import { AutocompleteFilterCallback, AutocompleteMode, AutocompleteOptionBuilder, AutocompleteSelectedTextBuilder, AUTOCOMPLETE_CONSTANTS, IAutocompleteOption, IAutocompleteOptionGroup, IAutocompleteSelectEventData } from './autocomplete-constants';
import { AutocompleteFilterCallback, AutocompleteMode, AutocompleteOptionBuilder, AutocompleteSelectedTextBuilder, AUTOCOMPLETE_CONSTANTS, IAutocompleteForceFilterOptions, IAutocompleteOption, IAutocompleteOptionGroup, IAutocompleteSelectEventData } from './autocomplete-constants';
import { AutocompleteFoundation } from './autocomplete-foundation';

import template from './autocomplete.html';
Expand All @@ -36,6 +36,7 @@ export interface IAutocompleteComponent extends IListDropdownAware {
appendOptions(options: IAutocompleteOption[] | IAutocompleteOptionGroup[]): void;
openDropdown(): void;
closeDropdown(): void;
forceFilter(opts?: IAutocompleteForceFilterOptions): void;
}

declare global {
Expand Down Expand Up @@ -232,4 +233,12 @@ export class AutocompleteComponent extends ListDropdownAware implements IAutocom
public closeDropdown(): void {
this.open = false;
}

/**
* Forces the filter callback to be executed to update the current selection state with new options.
* @param opts
*/
public forceFilter(opts: IAutocompleteForceFilterOptions = { preserveValue: false }): void {
this._foundation.forceFilter(opts);
}
}
16 changes: 16 additions & 0 deletions src/stories/src/components/autocomplete/autocomplete.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ Closes the dropdown.

</MethodDef>

<MethodDef name="forceFilter(opts: IAutocompleteForceFilterOptions): void;">

Forces the filter callback to be executed to update the current selection state with new options.

Call this when you need to update the options of an autocomplete dynamically by forcing the filter callback to execute and clear any existing options.

</MethodDef>

</PageSection>

---
Expand Down Expand Up @@ -331,4 +339,12 @@ interface IAutocompletePopupConfiguration {
}
```

### IAutocompleteForceFilterOptions

```ts
interface IAutocompleteForceFilterOptions {
preserveValue: boolean;
}
```

</PageSection>
75 changes: 74 additions & 1 deletion src/test/spec/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,7 @@ describe('AutocompleteComponent', function(this: ITestContext) {
expect(this.context.component.value).toBe(listItems[2].value);
});

it('should pass in to-be new value in beforeValueChange', async function(this: ITestContext) {
it('should pass in to-be new value in beforeValueChange', async function(this: ITestContext) {
this.context = setupTestContext(true);
let newValue = '';
this.context.component.filter = () => DEFAULT_FILTER_OPTIONS;
Expand All @@ -1546,6 +1546,79 @@ describe('AutocompleteComponent', function(this: ITestContext) {

expect(newValue).toBe(listItems[2].value);
});

it('should force filter callback to execute and update selected value', async function(this: ITestContext) {
this.context = setupTestContext(true);
this.context.component.filter = () => DEFAULT_FILTER_OPTIONS;

_triggerDropdownClick(this.context.input);

await tick();
const listItems = _getListItems(this.context.component.popupElement);
_clickListItem(2, this.context.component.popupElement);
await tick();

expect(this.context.component.value).toBe(listItems[2].value);

this.context.component.filter = () => [
{ label: `${DEFAULT_FILTER_OPTIONS[2].label} UPDATED`, value: DEFAULT_FILTER_OPTIONS[2].value},
{ label: 'New', value: 'new' }
];
this.context.component.forceFilter();

await tick();

expect(this.context.input.value).toBe(`${DEFAULT_FILTER_OPTIONS[2].label} UPDATED`);
expect(this.context.component.value).toBe(DEFAULT_FILTER_OPTIONS[2].value);
});

it('should force filter callback to execute and remove selected value', async function(this: ITestContext) {
this.context = setupTestContext(true);
this.context.component.filter = () => DEFAULT_FILTER_OPTIONS;

_triggerDropdownClick(this.context.input);

await tick();
const listItems = _getListItems(this.context.component.popupElement);
_clickListItem(2, this.context.component.popupElement);
await tick();

expect(this.context.component.value).toBe(listItems[2].value);

this.context.component.filter = () => [
{ label: 'New', value: 'new' }
];
this.context.component.forceFilter();

await tick();

expect(this.context.input.value).toBe('');
expect(this.context.component.value).toBe(DEFAULT_FILTER_OPTIONS[2].value);
});

it('should force filter callback to execute and preserve selected value if not present in new filtered options', async function(this: ITestContext) {
this.context = setupTestContext(true);
this.context.component.filter = () => DEFAULT_FILTER_OPTIONS;

_triggerDropdownClick(this.context.input);

await tick();
const listItems = _getListItems(this.context.component.popupElement);
_clickListItem(2, this.context.component.popupElement);
await tick();

expect(this.context.component.value).toBe(listItems[2].value);

this.context.component.filter = () => [
{ label: 'New', value: 'new' }
];
this.context.component.forceFilter({ preserveValue: true });

await tick();

expect(this.context.input.value).toBe(DEFAULT_FILTER_OPTIONS[2].label);
expect(this.context.component.value).toBe(DEFAULT_FILTER_OPTIONS[2].value);
});
});
});

Expand Down

0 comments on commit 8c4edaf

Please sign in to comment.