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

[Select, Multiselect]: New input properties 'autocompleteFilter' and 'autocompleteNoResultsText' with Storybook examples #470

Merged
merged 29 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
461ff50
DS-344: refactor select showcases to example components
videoeero Oct 18, 2024
efa2586
DS-344: add autocompleteFIltering prop
videoeero Oct 18, 2024
fd47510
DS-344: remove options from parents onDestroy
videoeero Oct 18, 2024
3a9a0c2
DS-344: cleaning
videoeero Oct 18, 2024
928f280
DS-344: cleaning
videoeero Oct 18, 2024
d7a481b
DS-344: add filtering check to option base
videoeero Oct 18, 2024
81e4bbe
DS-344: new mock data for Select
videoeero Oct 21, 2024
9d5ee86
DS-344: documenting
videoeero Oct 21, 2024
f89baba
DS-344: storybook example
videoeero Oct 21, 2024
752d00f
DS-344: storybook story adjusts
videoeero Oct 21, 2024
c18d1fb
DS-344: add autocompleteNoResultsText input
videoeero Oct 21, 2024
cbacc5d
DS-344: unit test update
videoeero Oct 22, 2024
0e4010e
Merge branch 'main' into DS-344-select-search-impr
videoeero Oct 22, 2024
63e31d7
DS-344: cleaning
videoeero Oct 22, 2024
b2edb57
DS-344: cleaning
videoeero Oct 22, 2024
31d3aac
DS-344: adjust flaky tests
videoeero Oct 22, 2024
b9aa39d
DS-344: adjust Select e2e tests
videoeero Oct 23, 2024
e905aec
DS-344: adjust e2e flaky tests
videoeero Oct 23, 2024
29b9779
Merge branch 'main' into DS-344-select-search-impr
videoeero Oct 23, 2024
4dc75d3
DS-344: adjust select e2e
videoeero Oct 23, 2024
c985d0a
Merge branch 'main' into DS-344-select-search-impr
videoeero Oct 23, 2024
76e9289
DS-344: adjust DL vr tests
videoeero Oct 23, 2024
ecbfa4e
DS-344: CSS and DOM order tweaking
videoeero Oct 23, 2024
62842b8
DS-344: select vr test fixes
videoeero Oct 24, 2024
3aaa56b
DS-344: select autocompletefilter false e2e tests
videoeero Oct 24, 2024
8f9f58b
DS-344: cleaning
videoeero Oct 24, 2024
18b129c
DS-344: Fix double .ts suffix, fix required validator text
RiinaKuu Oct 25, 2024
18f6e58
DS-344: Fix && operator to show correct selected option
RiinaKuu Oct 28, 2024
ac5af56
fix: Update misleading documentation for Link title
RiinaKuu Oct 28, 2024
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
5,008 changes: 5,008 additions & 0 deletions ngx-fudis/projects/ngx-fudis/src/lib/components/form/select/common/mock_data.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { SelectIconsComponent } from '../select-icons/select-icons.component';
</fudis-multiselect>
<fudis-multiselect
#multiSelectAuto
[autocompleteFilter]="autocompleteFilter"
[variant]="'autocompleteDropdown'"
[label]="'MultiAutoSelect Label'"
[autocompleteHelpText]="'This is autocomplete help text'"
Expand All @@ -71,6 +72,7 @@ class MockSelectComponent {
clearButton: boolean = true;
size = 'md';
variant: FudisSelectVariant = 'dropdown';
autocompleteFilter = true;

@ViewChild('multiSelect') multiSelect: MultiselectComponent;
@ViewChild('multiSelectAuto') multiSelectAuto: MultiselectComponent;
Expand Down Expand Up @@ -194,6 +196,59 @@ describe('SelectBaseDirective', () => {
});
});

it('should set autocompleteNoResultsText', () => {
const customText = 'This is custom no results text';

component.multiSelectAuto.autocompleteNoResultsText = customText;

fixture.detectChanges();

const autocompleteInput = getElement(fixture, '#fudis-multiselect-2') as HTMLInputElement;
autocompleteInput.focus();

fixture.detectChanges();

component.multiSelectAuto.autocompleteRef.updateInputValue('hello');

fixture.detectChanges();

const noResultsElement = getElement(fixture, '.fudis-select-dropdown__help-text__last');

expect(noResultsElement.textContent).toEqual(customText);
});

it('autocompleteFilter false should not filter results', () => {
const autocompleteInput = getElement(fixture, '#fudis-multiselect-2') as HTMLInputElement;
autocompleteInput.focus();
fixture.detectChanges();

component.multiSelectAuto.autocompleteRef.updateInputValue('salmon');

fixture.detectChanges();

const allOptionsBefore = getAllElements(
fixture,
'#fudis-multiselect-2-main-wrapper .fudis-multiselect-option--visible',
);

expect(allOptionsBefore.length).toEqual(1);

component.autocompleteFilter = false;

fixture.detectChanges();

component.multiSelectAuto.autocompleteRef.updateInputValue('salmo');

fixture.detectChanges();

const allOptionsAfter = getAllElements(
fixture,
'#fudis-multiselect-2-main-wrapper .fudis-multiselect-option--visible',
);

expect(allOptionsAfter.length).toEqual(9);
});

it('selectionClearButton', () => {
const buttonsFirst = getAllElements(fixture, 'fudis-button .fudis-icon__close');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,21 @@ export class SelectBaseDirective
*/
@Input() selectionClearButton: boolean = true;

/**
* By default Autocomplete filters options loaded to the DOM based on user input. When this is set to 'false', filtering is disabled and all options available in DOM are displayed regardless of user's input. Disabling can be useful, if application wants to implement their own filtering logic. E. g. get user's input, run a backend search and create list of options for the Select.
*/
@Input() autocompleteFilter: boolean = true;

/**
* For Autocomplete variants optional helper text displayed as first item in opened dropdown list. By default uses internal Fudis translation, which can be disabled by setting this property to boolean 'false'
*/
@Input() autocompleteHelpText: string | false;

/**
* By default, Autocomplete variant will display "No results found" text when there are 0 options matching. When combined with 'autocompleteFilter' false, application can set their own 'Fetching options...' etc. text while their own filtering is in progress.
*/
@Input() autocompleteNoResultsText: string | null = null;

/**
* Value output event on selection change
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ Both Select and Multiselect have three variants:
- User must type 3 letters before any selectable results are shown
- Initially a search icon is visible and after typing 3 letters, it changes to a button which can be used to clear the typed text

### Default Filtering Logic and Manually Filtering Options e.g. from Backend

By default, Select with Autocomplete assumes, that all available options are always loaded to the DOM and from user input it will hide or show matching options.

In real world applications there are cases, that for performance, it is unfeasible to always load all options to the DOM. Select and Multiselect has a property `autocompleteFilter`, when set to `false`, it disables default filtering logic.

This combined with Output property `filterTextUpdate` application can listen to user's input, do their own filtering logic and then load options to the DOM.

Example Stories for manual autocomplete filtering:

- [Select](/story/components-form-select-select--backend-simulation)
- [Multiselect](/story/components-form-select-multiselect--backend-simulation)

### Selection Clear Button Boolean

By default `selectionClearButton` is set to boolean `true`. Whenever user has made a valid selection from the options, the Clear button can be clicked to clear the selection. This will set the component's `FormControl` to `null`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
padding: spacing.$spacing-xs;
}

&__first {
order: -1;
}

&--hidden.fudis-body-text-host {
display: none;
visibility: hidden;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,43 @@
(blur)="_dropdownBlur($event)"
role="listbox"
>
<ng-content></ng-content>
<ng-container *ngIf="selectVariant !== 'dropdown' && open && filterText">
<fudis-body-text
*ngIf="autocompleteHelpText !== false"
[class.fudis-select-dropdown__help-text--hidden]="!results"
class="fudis-select-dropdown__help-text fudis-select-dropdown__help-text__first"
[variant]="'md-light'"
><ng-container *ngIf="autocompleteHelpText">{{ autocompleteHelpText }}</ng-container
><ng-container *ngIf="!autocompleteHelpText"
>{{ _translationShowing | async }} {{ results }}
{{ _translationResults | async }}</ng-container
></fudis-body-text
>
><ng-container *ngTemplateOutlet="helpText"
/></fudis-body-text>
<fudis-body-text
class="fudis-select-dropdown__help-text fudis-select-dropdown__help-text__last"
[class.fudis-select-dropdown__help-text--hidden]="results"
[variant]="'md-light'"
>{{ _translationNoResultsFound | async }}</fudis-body-text
>
><ng-container *ngTemplateOutlet="noResults" />
</fudis-body-text>
<span
*ngIf="autocompleteHelpText !== false && _displayStatus && results && filterText"
class="fudis-visually-hidden"
role="alert"
>
<ng-container *ngIf="autocompleteHelpText">
{{ autocompleteHelpText }}
</ng-container>
<ng-container *ngIf="!autocompleteHelpText">
{{ _translationShowing | async }} {{ results }} {{ _translationResults | async }}
</ng-container>
</span>
<span *ngIf="!results && _displayStatus" class="fudis-visually-hidden" role="alert">
{{ _translationNoResultsFound | async }}
</span>
><ng-container *ngTemplateOutlet="helpText"
/></span>
<span *ngIf="!results && _displayStatus" class="fudis-visually-hidden" role="alert"
><ng-container *ngTemplateOutlet="noResults"
/></span>
</ng-container>
<ng-content></ng-content>
</div>

<ng-template #helpText>
<ng-container *ngIf="autocompleteHelpText">{{ autocompleteHelpText }}</ng-container
><ng-container *ngIf="!autocompleteHelpText"
>{{ _translationShowing | async }} {{ results }} {{ _translationResults | async }}</ng-container
>
</ng-template>

<ng-template #noResults>
<ng-container *ngIf="autocompleteNoResultsText">{{ autocompleteNoResultsText }}</ng-container>
<ng-container *ngIf="!autocompleteNoResultsText">{{
_translationNoResultsFound | async
}}</ng-container>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export class SelectDropdownComponent extends DropdownBaseDirective implements On
*/
@Input() autocompleteHelpText: string | false;

/**
* By default, Autocomplete variant will display "No results found" text when there are 0 options matching. When combined with 'autocompleteFilter' false, application can set their own 'Fetching options...' etc. text while their own filtering is in progress.
*/
@Input() autocompleteNoResultsText: string | null = null;

/**
* Current filter text from Autocomplete parents
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class SelectOptionBaseDirective extends DropdownItemBaseDirective {
effect(() => {
const filterText = this._parent.getAutocompleteFilterText()();

if (this._parent.variant !== 'dropdown') {
if (this._parent.variant !== 'dropdown' && this._parent.autocompleteFilter) {
this._isOptionVisible(filterText);
} else {
this._isOptionVisible('');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Directive } from '@angular/core';
import { FudisSelectOption } from '../../../../types/forms';
import { selectMovieMockData } from '../common/mock_data';
import { BehaviorSubject, debounceTime, Subject } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl } from '@angular/forms';

@Directive({
standalone: true,
selector: 'example-select-backend-simulation-directive',
})
export class StorybookExampleBackendSimulationBaseDirective {
constructor() {
this.searchTextUpdateSubject.pipe(takeUntilDestroyed()).subscribe((value) => {
this.databaseCounter = 0;
if (value?.trim()) {
this.filterStatus = 'In progress...';
this.autocompleteNoResultsText = 'Fetching results...';
}
this.searchResults?.next([]);
});

this.searchTextUpdateSubject
.pipe(debounceTime(300), takeUntilDestroyed())
.subscribe((value) => {
if (value?.trim()) {
setTimeout(() => {
let counter = 0;

const counterLimit = 10;

const results: FudisSelectOption<object>[] = [];

for (const option of selectMovieMockData) {
if (counter >= counterLimit) {
break;
}

if (this.control.value && value === this.control.value?.label) {
results.push(option);
counter = counterLimit;
} else if (
(option.label.toLowerCase().includes(value.toLowerCase()) ||
option.subLabel?.toLowerCase().includes(value.toLowerCase()) ||
value === '&&&') &&
counter < counterLimit
) {
results.push(option);
counter = counter + 1;
}

this.databaseCounter = this.databaseCounter + 1;
}

this.searchResults?.next(results);
this.filterStatus = 'Finished';
setTimeout(() => {
this.autocompleteNoResultsText = null;
}, 100);
}, 500);
}
});
}

protected filterStatus: 'Finished' | 'In progress...' = 'Finished';

protected searchTextUpdateSubject = new Subject<string | null>();

protected databaseCounter = 0;

protected searchResults = new BehaviorSubject<FudisSelectOption<object>[]>([]);

protected autocompleteNoResultsText: null | string = null;

protected label = 'Select a movie';

protected helpText =
'There are 1000 options to choose from. You can also search by genre, e. g. action.';

protected placeholder = 'Select a movie';

protected control: FormControl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { NgxFudisModule } from '../../../../ngx-fudis.module';
import { CommonModule } from '@angular/common';
import { StorybookExampleBackendSimulationBaseDirective } from './backend-simulation-base.directive';
import { FudisSelectOption } from '../../../../types/forms';
import { FormControl } from '@angular/forms';
import { FudisValidators } from '../../../../utilities/form/validators';

@Component({
standalone: true,
imports: [NgxFudisModule, CommonModule],
selector: 'example-multiselect-backend-simulation',
template: `
<fudis-body-text
>This example Multiselect component has property <code>autocompleteFilter</code> set false and
filtering is done outside of Select with a delay to simulate fetching options from the
backend.</fudis-body-text
>
<fudis-body-text>It will limit its search for only 10 results. </fudis-body-text>
<fudis-body-text
>Number of options from 'database' checked: {{ databaseCounter }}</fudis-body-text
>
<fudis-body-text>Filtering status: {{ filterStatus }}</fudis-body-text>
<fudis-body-text
>Number of options loaded to DOM: {{ (searchResults | async)?.length }}</fudis-body-text
>
<fudis-multiselect
class="fudis-mt-md"
[size]="'lg'"
[variant]="'autocompleteType'"
[control]="control"
[placeholder]="placeholder"
[control]="control"
[label]="label"
[helpText]="helpText"
[autocompleteFilter]="false"
[autocompleteNoResultsText]="autocompleteNoResultsText"
[selectionClearButton]="true"
(selectionUpdate)="selectionUpdate.emit($event)"
(filterTextUpdate)="searchTextUpdateSubject.next($event)"
>
<ng-template fudisContent type="select-options">
@if (searchResults | async; as options) {
<fudis-multiselect-option
*ngFor="let option of options"
[data]="option"
></fudis-multiselect-option>
}</ng-template
></fudis-multiselect>
`,
})
export class StorybookExampleMultiselectBackendSimulationComponent extends StorybookExampleBackendSimulationBaseDirective {
@Output() selectionUpdate = new EventEmitter<FudisSelectOption<object>[] | null>();

override control: FormControl = new FormControl<FudisSelectOption<object>[] | null>(null, [
FudisValidators.required('You must choose a movie!'),
]);
}
Loading
Loading