File

src/components/sq-select-search/sq-select-search.component.ts

Description

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>

Implements

OnChanges

Metadata

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(element: ElementRef, translate: TranslateService, changeDetector: ChangeDetectorRef)

Constructs a new SqSelectSearchComponent.

Parameters :
Name Type Optional Description
element ElementRef No
  • The element reference.
translate TranslateService No
  • The optional TranslateService for internationalization.
changeDetector ChangeDetectorRef No
  • Base class that provides change detection functionality.

Inputs

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.

Outputs

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.

Methods

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 :
Name Type Optional Description
event any No
  • The event containing the selected value.
Returns : void
Async ngOnChanges
ngOnChanges(changes: SimpleChanges)

Lifecycle hook called when any input properties change.

Parameters :
Name Type Optional Description
changes SimpleChanges No
  • The changes detected in the component's input properties.
Returns : any
Async onTipSearchChange
onTipSearchChange(event: string)

Handles changes to the search input value.

Parameters :
Name Type Optional Description
event string No
  • The search input value.
Returns : any
Async setError
setError(key: string)

Sets an error message.

Parameters :
Name Type Optional Description
key string No
  • The translation key for the error message.
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

Properties

_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);
      }
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""