src/components/sq-select-search/sq-select-search.component.ts
Represents a search-based select component.
Example :<sq-select-search
[name]="'search'"
[value]="selectedOption"
[options]="searchOptions"
(valueChange)="handleOptionSelection($event)"
(searchChange)="handleSearch($event)"
>
</sq-select-search>
OnChanges
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | sq-select-search |
| standalone | true |
| imports |
NgClass
NgStyle
NgTemplateOutlet
AsyncPipe
FormsModule
ScrollingModule
SqLoaderComponent
SqTooltipComponent
UniversalSafePipe
TranslateInternalPipe
SearchPipe
SqClickOutsideDirective
SqDataTestDirective
|
| styleUrls | ./sq-select-search.component.scss |
| templateUrl | ./sq-select-search.component.html |
constructor(element: ElementRef, translate: TranslateService, changeDetector: ChangeDetectorRef)
|
||||||||||||||||
|
Constructs a new SqSelectSearchComponent.
Parameters :
|
| backgroundColor | |
Type : string
|
|
Default value : ''
|
|
|
Background color for the search-based select input. |
|
| borderColor | |
Type : string
|
|
Default value : ''
|
|
|
Border color for the search-based select input. |
|
| customClass | |
Type : string
|
|
Default value : ''
|
|
|
Custom CSS class for styling the component. |
|
| disabled | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the search-based select input is disabled. |
|
| errorSpan | |
Type : boolean
|
|
Default value : true
|
|
|
Indicates whether to display an error span. |
|
| externalError | |
Type : string
|
|
Default value : ''
|
|
|
External error message for the search-based select input. |
|
| externalIcon | |
Type : string
|
|
Default value : ''
|
|
|
External icon for the search-based select input. |
|
| id | |
Type : string
|
|
|
The id attribute for the search-based select input. |
|
| label | |
Type : string
|
|
Default value : ''
|
|
|
The label for the search-based select input. |
|
| labelColor | |
Type : string
|
|
Default value : ''
|
|
|
Color for the label of the search-based select input. |
|
| loading | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the search-based select input is in a loading state. |
|
| minCharactersToSearch | |
Type : number
|
|
Default value : 0
|
|
|
Minimum number of characters to perform the searchChange. |
|
| name | |
Type : string
|
|
Default value : `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`
|
|
|
The name attribute for the search-based select input. |
|
| options | |
Type : Array<Option>
|
|
Default value : []
|
|
|
Options available for selection. |
|
| placeholder | |
Type : string
|
|
Default value : ''
|
|
|
Placeholder text for the input field. |
|
| placeholderSearch | |
Type : string
|
|
Default value : ''
|
|
|
Placeholder text for the search input field. |
|
| readonly | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the search-based select input is readonly. |
|
| required | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the search-based select input is required. |
|
| selectHandleDataTest | |
Type : string
|
|
Default value : 'select-search'
|
|
|
The data-test attribute value for the select search element. |
|
| selectInputDataTest | |
Type : string
|
|
Default value : 'input-select-search'
|
|
|
The data-test attribute value for the select input element. |
|
| SelectOptionDataTest | |
Type : string
|
|
Default value : 'option-select-search'
|
|
|
The data-test attribute value for the select option elements. |
|
| timeToChange | |
Type : number
|
|
Default value : 800
|
|
|
The time interval for input timeout in ms. |
|
| tooltipColor | |
Type : string
|
|
Default value : 'inherit'
|
|
|
Tooltip color for the search-based select input. |
|
| tooltipIcon | |
Type : string
|
|
Default value : ''
|
|
|
Tooltip icon for the search-based select input. |
|
| tooltipMessage | |
Type : string
|
|
Default value : ''
|
|
|
Tooltip message for the search-based select input. |
|
| tooltipPlacement | |
Type : "center top" | "center bottom" | "left center" | "right center"
|
|
Default value : 'right center'
|
|
|
Tooltip placement for the search-based select input. |
|
| useFormErrors | |
Type : boolean
|
|
Default value : true
|
|
|
Indicates whether to use form errors for validation. |
|
| value | |
Type : Option
|
|
|
The selected value for the search-based select input. |
|
| searchChange | |
Type : EventEmitter<string>
|
|
|
Event emitted when the search input value changes. |
|
| valid | |
Type : EventEmitter<boolean>
|
|
|
Event emitted when the search-based select input becomes valid or invalid. |
|
| valueChange | |
Type : EventEmitter<Option>
|
|
|
Event emitted when the selected value changes. |
|
| closeDropdown |
closeDropdown()
|
|
Closes the dropdown and resets the search text.
Returns :
void
|
| Async doDropDownAction |
doDropDownAction()
|
|
Do action to open or close thw dropdown list
Returns :
any
|
| emit | ||||||||
emit(event: any)
|
||||||||
|
Emits the selected value and closes the dropdown.
Parameters :
Returns :
void
|
| Async ngOnChanges | ||||||||
ngOnChanges(changes: SimpleChanges)
|
||||||||
|
Lifecycle hook called when any input properties change.
Parameters :
Returns :
any
|
| Async onTipSearchChange | ||||||||
onTipSearchChange(event: string)
|
||||||||
|
Handles changes to the search input value.
Parameters :
Returns :
any
|
| Async setError | ||||||||
setError(key: string)
|
||||||||
|
Sets an error message.
Parameters :
Returns :
any
|
| validate |
validate()
|
|
Validates the search-based select input and sets the error state.
Returns :
void
|
| verifyNewOptions |
verifyNewOptions()
|
|
Verify new options and set the cdkVirtualScrollViewportHeight
Returns :
void
|
| _options |
Type : Array<Option>
|
Default value : []
|
|
Control options to render |
| cdkVirtualScrollViewportHeight |
Type : string
|
Default value : '305px'
|
|
The height for the cdk-virtual-scroll-viewport (default 305px). |
| Public element |
Type : ElementRef
|
|
- The element reference.
|
| error |
Type : boolean | string
|
Default value : ''
|
|
Error message associated with the search-based select input. |
| labelTemplate |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('labelTemplate')
|
|
The label template for the search-based select input. |
| nativeElement |
Type : ElementRef
|
|
Native element reference. |
| open |
Default value : false
|
|
Indicates whether the dropdown is open. |
| renderOptionsList |
Default value : false
|
|
Indicates when is the time to render the multi-tag select dropdown. |
| searchText |
Type : string
|
Default value : ''
|
|
Text entered in the search input field. |
| selectEmptyTemplate |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('selectEmptyTemplate')
|
|
The select empty template for the search-based select input. |
| timeoutInput |
Type : ReturnType<>
|
|
Timeout for input changes. |
| trackByOptValue |
Type : TrackByFunction<any>
|
Default value : useMemo((index, opt) => opt.value)
|
|
Return trackBy for ngFor |
import { SqDataTestDirective } from './../../directives/sq-data-test/sq-data-test.directive';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
Input,
OnChanges,
Optional,
Output,
SimpleChanges,
TemplateRef,
TrackByFunction,
} from '@angular/core';
import { NgClass, NgStyle, NgTemplateOutlet, AsyncPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { TranslateService } from '@ngx-translate/core';
import { useMemo } from '../../helpers/memo.helper';
import { Option } from '../../interfaces/option.interface';
import { SqLoaderComponent } from '../sq-loader/sq-loader.component';
import { SqTooltipComponent } from '../sq-tooltip/sq-tooltip.component';
import { UniversalSafePipe } from '../../pipes/universal-safe/universal-safe.pipe';
import { TranslateInternalPipe } from '../../pipes/translate-internal/translate-internal.pipe';
import { SearchPipe } from '../../pipes/search/search.pipe';
import { SqClickOutsideDirective } from '../../directives/sq-click-outside/sq-click-outside.directive';
/**
* Represents a search-based select component.
*
* @example
* <sq-select-search
* [name]="'search'"
* [value]="selectedOption"
* [options]="searchOptions"
* (valueChange)="handleOptionSelection($event)"
* (searchChange)="handleSearch($event)"
* >
* </sq-select-search>
*/
@Component({
selector: 'sq-select-search',
templateUrl: './sq-select-search.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./sq-select-search.component.scss'],
standalone: true,
imports: [
NgClass,
NgStyle,
NgTemplateOutlet,
AsyncPipe,
FormsModule,
ScrollingModule,
SqLoaderComponent,
SqTooltipComponent,
UniversalSafePipe,
TranslateInternalPipe,
SearchPipe,
SqClickOutsideDirective,
SqDataTestDirective,
],
providers: [],
})
export class SqSelectSearchComponent implements OnChanges {
/**
* The name attribute for the search-based select input.
*/
@Input() name = `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`;
/**
* The selected value for the search-based select input.
*/
@Input() value?: Option;
/**
* The id attribute for the search-based select input.
*/
@Input() id?: string;
/**
* The label for the search-based select input.
*/
@Input() label = '';
/**
* Custom CSS class for styling the component.
*/
@Input() customClass = '';
/**
* Placeholder text for the input field.
*/
@Input() placeholder = '';
/**
* External error message for the search-based select input.
*/
@Input() externalError = '';
/**
* External icon for the search-based select input.
*/
@Input() externalIcon = '';
/**
* Placeholder text for the search input field.
*/
@Input() placeholderSearch = '';
/**
* Indicates whether the search-based select input is disabled.
*/
@Input() disabled = false;
/**
* Indicates whether the search-based select input is readonly.
*/
@Input() readonly = false;
/**
* Indicates whether the search-based select input is required.
*/
@Input() required = false;
/**
* Indicates whether the search-based select input is in a loading state.
*/
@Input() loading = false;
/**
* Indicates whether to use form errors for validation.
*/
@Input() useFormErrors = true;
/**
* Indicates whether to display an error span.
*/
@Input() errorSpan = true;
/**
* Minimum number of characters to perform the searchChange.
*/
@Input() minCharactersToSearch = 0;
/**
* The time interval for input timeout in ms.
*/
@Input() timeToChange = 800;
/**
* Options available for selection.
*/
@Input() options: Array<Option> = [];
/**
* Background color for the search-based select input.
*/
@Input() backgroundColor = '';
/**
* Border color for the search-based select input.
*/
@Input() borderColor = '';
/**
* Color for the label of the search-based select input.
*/
@Input() labelColor = '';
/**
* Tooltip message for the search-based select input.
*/
@Input() tooltipMessage = '';
/**
* Tooltip placement for the search-based select input.
*/
@Input() tooltipPlacement: 'center top' | 'center bottom' | 'left center' | 'right center' = 'right center';
/**
* Tooltip color for the search-based select input.
*/
@Input() tooltipColor = 'inherit';
/**
* Tooltip icon for the search-based select input.
*/
@Input() tooltipIcon = '';
/**
* The data-test attribute value for the select search element.
*
* @default 'select-search'
*/
@Input() selectHandleDataTest = 'select-search';
/**
* The data-test attribute value for the select input element.
*
* @default 'input-select-search'
*/
@Input() selectInputDataTest = 'input-select-search';
/**
* The data-test attribute value for the select option elements.
*
* @default 'option-select-search'
*/
@Input() SelectOptionDataTest = 'option-select-search';
/**
* Event emitted when the selected value changes.
*/
@Output() valueChange: EventEmitter<Option> = new EventEmitter();
/**
* Event emitted when the search input value changes.
*/
@Output() searchChange: EventEmitter<string> = new EventEmitter();
/**
* Event emitted when the search-based select input becomes valid or invalid.
*/
@Output() valid: EventEmitter<boolean> = new EventEmitter();
/**
* The label template for the search-based select input.
*/
@ContentChild('labelTemplate')
labelTemplate: TemplateRef<HTMLElement> | null = null;
/**
* The select empty template for the search-based select input.
*/
@ContentChild('selectEmptyTemplate')
selectEmptyTemplate: TemplateRef<HTMLElement> | null = null;
/**
* Error message associated with the search-based select input.
*/
error: boolean | string = '';
/**
* Native element reference.
*/
nativeElement: ElementRef;
/**
* Text entered in the search input field.
*/
searchText = '';
/**
* Indicates when is the time to render the multi-tag select dropdown.
*/
renderOptionsList = false;
/**
* Indicates whether the dropdown is open.
*/
open = false;
/**
* Control options to render
*/
_options: Array<Option> = [];
/**
* Timeout for input changes.
*/
timeoutInput!: ReturnType<typeof setTimeout>;
/**
* The height for the cdk-virtual-scroll-viewport (default 305px).
*/
cdkVirtualScrollViewportHeight = '305px';
/**
* Constructs a new SqSelectSearchComponent.
*
* @param {ElementRef} element - The element reference.
* @param {TranslateService} translate - The optional TranslateService for internationalization.
* @param {ChangeDetectorRef} changeDetector - Base class that provides change detection functionality.
*/
constructor(
public element: ElementRef,
@Optional() private translate: TranslateService,
private changeDetector: ChangeDetectorRef
) {
this.nativeElement = element.nativeElement;
}
/**
* Lifecycle hook called when any input properties change.
*
* @param changes - The changes detected in the component's input properties.
*/
async ngOnChanges(changes: SimpleChanges) {
if (this.open && changes.hasOwnProperty('options')) {
this.verifyNewOptions();
}
}
/**
* Emits the selected value and closes the dropdown.
*
* @param {any} event - The event containing the selected value.
*/
emit(event: any) {
this.value = event;
this.valueChange.emit(this.value);
this.validate();
this.closeDropdown();
}
/**
* Validates the search-based select input and sets the error state.
*/
validate() {
if (this.externalError) {
this.error = false;
} else if (this.required && !this.value) {
this.setError('forms.required');
this.valid.emit(false);
} else {
this.valid.emit(true);
this.error = '';
}
}
/**
* Do action to open or close thw dropdown list
*/
async doDropDownAction() {
if (this.open) {
this.closeDropdown();
this.renderOptionsList = await new Promise<boolean>(resolve =>
setTimeout(() => {
resolve(false);
}, 300)
);
this.changeDetector.detectChanges();
} else {
this.verifyNewOptions();
this.renderOptionsList = true;
this.open = await new Promise<boolean>(resolve =>
setTimeout(() => {
resolve(true);
}, 100)
);
this.changeDetector.detectChanges();
}
}
/**
* Closes the dropdown and resets the search text.
*/
closeDropdown() {
this.open = false;
this._options = [];
this.searchText = '';
}
/**
* Return trackBy for ngFor
*/
trackByOptValue: TrackByFunction<any> = useMemo((index, opt) => opt.value);
/**
* Handles changes to the search input value.
*
* @param {string} event - The search input value.
*/
async onTipSearchChange(event: string) {
if (!this.minCharactersToSearch || !event.length || event.length >= this.minCharactersToSearch) {
clearTimeout(this.timeoutInput);
this.searchText =
(await new Promise<string>(
resolve =>
(this.timeoutInput = setTimeout(() => {
resolve(event);
}, this.timeToChange))
)) || '';
this.searchChange.emit(event);
this.changeDetector.detectChanges();
}
}
/**
* Sets an error message.
*
* @param {string} key - The translation key for the error message.
*/
async setError(key: string) {
if (this.useFormErrors && this.translate) {
this.error = await this.translate.instant(key);
}
}
/**
* Verify new options and set the cdkVirtualScrollViewportHeight
*/
verifyNewOptions() {
this._options = this.options;
if (!this._options.length) {
this.cdkVirtualScrollViewportHeight = '12px';
} else if (this._options.length < 15) {
this.cdkVirtualScrollViewportHeight = this._options.length * 32 + 'px';
} else {
this.cdkVirtualScrollViewportHeight = '305px';
}
}
}
<div
class="wrapper-all-inside-input wrapper-select-search {{ customClass }}"
[ngClass]="{
error: (externalError && externalError !== '') || (error && error !== ''),
readonly: readonly,
loading: loading,
}"
>
@if (label?.length || labelTemplate || tooltipMessage) {
<label
class="display-flex align-items-center"
[ngClass]="{
readonly: readonly,
}"
[for]="id"
>
@if (label && !labelTemplate) {
<div [ngStyle]="{ color: labelColor }" [innerHtml]="label | universalSafe"></div>
}
@if (labelTemplate) {
<span>
<ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
</span>
}
@if (tooltipMessage) {
<sq-tooltip
class="ml-1"
[message]="tooltipMessage"
[placement]="tooltipPlacement"
[color]="tooltipColor"
[icon]="tooltipIcon"
></sq-tooltip>
}
</label>
}
<div
[dataTest]="selectHandleDataTest"
[class]="'input-fake col border-' + borderColor"
style="min-height: auto"
[ngStyle]="{ 'border-color': borderColor }"
[ngClass]="{
'no-label': !label.length,
'has-icon': error || externalError,
disabled: disabled,
readonly: readonly,
}"
[ngStyle]="{
'background-color': backgroundColor,
'border-color': borderColor,
}"
(clickOutside)="closeDropdown()"
[clickOutsideEnabled]="open"
>
<div
class="input-fake-content"
[ngClass]="{
disabled: disabled,
readonly: readonly,
}"
(click)="doDropDownAction()"
>
@if (loading) {
<div class="loading-wrapper">
<sq-loader></sq-loader>
</div>
}
@if (!value) {
<span>{{ placeholder }}</span>
}
@if (value) {
<div class="input-fake-content-text">
<span class="text">
{{ value!.label }}
</span>
</div>
}
@if (!loading) {
<i class="icon-down fas fa-chevron-down"></i>
}
</div>
@if (!disabled && !loading && renderOptionsList && !readonly) {
<div
class="input-window scrollbar"
id="sq-select-search-scroll"
[ngClass]="{
open: !disabled && !loading && renderOptionsList && open,
}"
>
<div class="input-search">
<div class="wrapper-all-inside-input">
<div class="p-0 wrapper-input wrapper-input-squid text-ellipsisarea">
<input
[dataTest]="selectInputDataTest"
[name]="name"
[id]="id"
[placeholder]="placeholderSearch || ('forms.search' | translateInternal | async) || ''"
class="col input"
[ngModel]="searchText"
(ngModelChange)="onTipSearchChange($event)"
/>
</div>
<span class="icon icon-external textarea-icon"><i class="fas fa-search"></i></span>
</div>
</div>
<cdk-virtual-scroll-viewport
itemSize="22"
[ngStyle]="{ height: cdkVirtualScrollViewportHeight }"
class="list scrollbar"
>
<ng-container *cdkVirtualFor="let opt of _options | search: searchText; i as index; trackBy: trackByOptValue">
<ng-template *ngTemplateOutlet="option; context: { opt: opt, i: index }"></ng-template>
</ng-container>
</cdk-virtual-scroll-viewport>
@if (!_options?.length) {
@if (!selectEmptyTemplate) {
<p class="mb-0 mt-3">
{{ 'forms.searchSelectEmpty' | translateInternal | async }}
</p>
}
@if (selectEmptyTemplate) {
<span>
<ng-container *ngTemplateOutlet="selectEmptyTemplate"></ng-container>
</span>
}
}
</div>
}
</div>
@if (errorSpan) {
<div
class="box-validation box-invalid show"
[ngClass]="{
'visibility-hidden-force':
((!externalError || externalError === '') && (!error || error === '')) || disabled || readonly,
}"
>
<i
[ngClass]="{
'visibility-hidden-force': !error && !externalError,
}"
class="fa-solid fa-triangle-exclamation"
></i>
{{ externalError ? externalError : '' }}
{{ error && !externalError ? error : '' }}
</div>
}
</div>
<ng-template #option let-opt="opt" let-i="i">
<li
[dataTest]="SelectOptionDataTest + '-' + (i || opt.value)"
(click)="!opt.disabled ? emit(opt) : ''"
[ngClass]="{ disabled: opt.disabled }"
>
<span class="text m-0 display-inline-block">{{ opt?.label }}</span>
</li>
</ng-template>
./sq-select-search.component.scss
.wrapper-select-search {
margin: 0;
padding: 0;
text-align: left;
display: flex;
flex-direction: column;
position: relative;
&.loading {
cursor: wait;
}
.input-fake-content-text {
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
gap: 5px;
max-width: calc(100% - 40px);
.text {
margin: 0;
display: flex;
width: 100%;
height: 100%;
font-weight: 700;
align-items: center;
justify-content: flex-start;
}
}
.input-fake-content {
min-height: 44px;
border: 1px solid var(--border_color);
background: var(--background);
padding: 0.75rem 1rem;
color: var(--text_color);
border-radius: 5px;
.icon-down {
right: 5px;
top: 18px;
position: absolute;
font-size: 0.86rem;
}
&.readonly {
cursor: not-allowed;
pointer-events: none;
background-color: var(--color_border_input);
border-color: var(--color_border_input_disabled);
}
}
.loading-wrapper {
position: absolute;
right: 5px;
bottom: 10px;
}
.input-search {
display: inline-block;
position: sticky;
top: 0;
z-index: 1;
width: 100%;
padding-top: 1.1rem;
margin-bottom: 0.7rem;
background-color: var(--background_secondary);
.wrapper-all-inside-input {
.icon {
margin: 0;
font-size: 1rem;
font-weight: 700;
position: absolute;
right: 11px;
top: 27px;
}
.icon-external {
color: inherit !important;
}
}
}
.input-window {
width: 100%;
position: absolute;
top: 100%;
max-height: 0;
height: 0;
transition: all 0.3s ease;
overflow: hidden;
z-index: 5;
&.open {
height: auto;
max-height: 400px;
background: var(--background_secondary);
padding: 0 0.7rem 1.1rem;
overflow-y: hidden;
box-shadow: var(--box_shadow);
}
}
.list {
padding: 0;
list-style: none;
margin: 0;
width: 100%;
margin: 0.7rem 0 0;
li {
margin: 0;
cursor: pointer;
min-height: 30px;
flex-flow: row wrap;
display: flex;
align-items: center;
justify-content: flex-start;
transition: var(--transition);
padding: 0.35rem;
border-radius: 5px;
&:hover {
background-color: var(--border_color);
}
}
}
}