File

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

Description

Multi-select dropdown component with advanced features including:

  • Single or multiple selection
  • Search/filter functionality
  • Hierarchical options (nested children)
  • Custom templates
  • Virtual scrolling for large lists
Example :
<sq-select-multi
  [options]="cities"
  [(value)]="selectedCities"
  [maxSelects]="3"
  [width]="'300px'"
  [minWidth]="'250px'"
  [dropdownWidth]="'350px'"
></sq-select-multi>

Implements

OnChanges

Metadata

Index

Properties
Methods
Inputs
Outputs

Constructor

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

Constructor for the SqSelectMultiComponent. Initializes the component with default values and dependencies.

Parameters :
Name Type Optional Description
element ElementRef No
  • ElementRef for accessing native DOM element
translate TranslateService No
  • Optional TranslateService for internationalization
changeDetector ChangeDetectorRef No
  • ChangeDetectorRef for manual change detection

Inputs

backgroundColor
Type : string
Default value : ''

Background color for the select input.

borderColor
Type : string
Default value : ''

Border color for the select input.

customClass
Type : string
Default value : ''

Custom CSS class to apply to the component container.

disabled
Type : boolean
Default value : false

Disables the select input when true.

dropdownWidth
Type : string
Default value : '100%'

Width of the dropdown panel. Can be different from the trigger width. Default: '100%'

errorSpan
Type : boolean
Default value : true

Shows error message container when true.

externalError
Type : string
Default value : ''

External error message to display.

externalIcon
Type : string
Default value : ''

External icon to display.

hideSearch
Type : boolean
Default value : false

Hides the search input when true.

id
Type : string

ID attribute for the select element.

label
Type : string
Default value : ''

Label text displayed above the select input.

labelColor
Type : string
Default value : ''

Color for the label text.

loading
Type : boolean
Default value : false

Shows loading spinner when true.

maxSelects
Type : number

Maximum number of items that can be selected.

minCharactersToSearch
Type : number
Default value : 0

Minimum characters required to trigger search.

minSelects
Type : number

Minimum number of items required to be selected.

minWidth
Type : string
Default value : '200px'

Minimum width of the component container. Default: '200px'

name
Type : string
Default value : `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`

Name attribute for the select element. Defaults to a random unique name if not provided.

options
Type : Array<OptionMulti>
Default value : []

Array of available options for selection.

placeholder
Type : string
Default value : ''

Placeholder text shown when no options are selected.

placeholderSearch
Type : string
Default value : ''

Placeholder text for the search input.

readonly
Type : boolean
Default value : false

Makes the select input read-only when true.

required
Type : boolean
Default value : false

Marks the field as required when true.

showInside
Type : boolean
Default value : true

Shows selected items inside the input when true.

timeToChange
Type : number
Default value : 800

Debounce time (in ms) for search input.

tooltipColor
Type : string
Default value : 'inherit'

Color scheme for the tooltip.

tooltipIcon
Type : string
Default value : ''

Icon to display with the tooltip.

tooltipMessage
Type : string
Default value : ''

Tooltip message content.

tooltipPlacement
Type : "center top" | "center bottom" | "left center" | "right center"
Default value : 'right center'

Position of the tooltip relative to the label.

useFormErrors
Type : boolean
Default value : true

Enables form error messages when true.

value
Type : OptionMulti[]
Default value : []

Currently selected options (two-way bindable).

width
Type : string
Default value : '100%'

Width of the component container. Can be any valid CSS width value (px, %, etc). Default: '100%'

Outputs

closeChange
Type : EventEmitter<boolean>

Emitted when the dropdown closes. Contains boolean indicating if selection changed.

searchChange
Type : EventEmitter<string>

Emitted when the search text changes (with debounce).

valid
Type : EventEmitter<boolean>

Emitted when validation status changes. True when valid, false when invalid.

valueChange
Type : EventEmitter<Array<OptionMulti>>

Emitted when the selected values change.

Methods

closeDropdown
closeDropdown()

Closes the dropdown and resets search state.

Returns : void
Async doDropDownAction
doDropDownAction()

Toggles the dropdown open/closed state. Handles animations and rendering of options list.

Returns : any
emit
emit(object: OptionMulti, checked: boolean)

Adds or removes an item from the selection.

Parameters :
Name Type Optional Description
object OptionMulti No
  • The option to select/deselect
checked boolean No
  • True to select, false to deselect
Returns : void
handleCollapse
handleCollapse(item: OptionMulti)

Handles expanding/collapsing of hierarchical options.

Parameters :
Name Type Optional Description
item OptionMulti No
  • The option item to toggle
Returns : void
Async modelChange
modelChange(event: any)

Handles search input changes with debounce.

Parameters :
Name Type Optional Description
event any No
  • The new search text
Returns : any
Async ngOnChanges
ngOnChanges(changes: SimpleChanges)

Angular lifecycle hook called when input properties change.

Parameters :
Name Type Optional Description
changes SimpleChanges No
  • Object containing changed properties.
Returns : any
Async setError
setError(key: string, interpolateParams: Object)

Sets an error message using translation.

Parameters :
Name Type Optional Default value Description
key string No
  • Translation key for the error
interpolateParams Object No {}
  • Optional interpolation parameters
Returns : any
validate
validate()

Validates the current selection state. Updates error messages and validity status.

Returns : void
verifyNewOptions
verifyNewOptions()

Updates options list and calculates virtual scroll viewport height. Adjusts based on number of options for optimal performance.

Returns : void

Properties

_options
Type : Array<OptionMulti>
Default value : []

Internal list of options after filters are applied

cdkItemSize
Type : string | null
Default value : '32'

Size of the items in the virtual scroll (in pixels)

cdkVirtualScrollViewportHeight
Type : string
Default value : '305px'

Dynamic height of the virtual scroll viewport Automatically calculated based on the number of options

Public element
Type : ElementRef
- ElementRef for accessing native DOM element
error
Type : boolean | string
Default value : ''

Current error message or error boolean state

findItemInValue
Default value : useMemo((item: OptionMulti, value?: Array<OptionMulti>) => { return !!value?.find(value => value.value === item.value); })

Determines if an item exists in the selected values.

Parameters :
Name Description
item
  • The item to check
value
  • Optional array to check against (defaults to current value)
ismaxSelects
Default value : false

Flag indicating whether the maximum number of selections has been reached

labelTemplate
Type : TemplateRef<HTMLElement> | null
Default value : null
Decorators :
@ContentChild('labelTemplate')

Custom template for the label content.

nativeElement
Type : ElementRef

Reference to the native element of the component

open
Default value : false

State that controls whether the dropdown is open or closed

renderOptionsList
Default value : false

Flag indicating whether the rendering of the options list is active

searchText
Type : string
Default value : ''

Current text used for search/filter

selectEmptyTemplate
Type : TemplateRef<HTMLElement> | null
Default value : null
Decorators :
@ContentChild('selectEmptyTemplate')

Custom template to display when no options are available.

timeouted
Default value : false

Temporary flag for timeout control

timeoutInput
Type : ReturnType<>

Reference to the search timeout for debounce

trackByOptValue
Type : TrackByFunction<any>
Default value : useMemo((index, opt) => opt.value)

TrackBy function for ngFor optimizations.

Parameters :
Name Description
index
  • Item index
opt
  • Option item
valueChanged
Default value : false

Flag indicating whether the value has changed since the last opening

verifyIfHasChildrenInValue
Default value : useMemo((item: OptionMulti, value?: Array<OptionMulti>) => { if (item.children?.length) { const hasAllChildren = item.children.every(child => this.findItemInValue(child, value)); if (hasAllChildren && !this.findItemInValue(item, value) && !this.timeouted) { this.timeouted = true; setTimeout(() => { this.emit(item, true); this.timeouted = false; }, 0); } return !!item.children.find(child => this.findItemInValue(child, value)); } return false; })

Checks if an item has any selected children.

Parameters :
Name Description
item
  • The parent item to check
value
  • Optional array to check against (defaults to current value)
verifyIfOptionsHasChildren
Default value : useMemo((options: OptionMulti[]) => { return options.some(item => item.children?.length); })

Checks if any options have children (hierarchy).

Parameters :
Name Description
options
  • Array of options to check
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 { OptionMulti } from '../../interfaces/option.interface';
import { SqLoaderComponent } from '../sq-loader/sq-loader.component';
import { SqTooltipComponent } from '../sq-tooltip/sq-tooltip.component';
import { SqSelectorComponent } from '../sq-selector/sq-selector.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';
import { SearchValidValuesPipe } from '../../pipes/search-valid-values/search-valid-values.pipe';

/**
 * Multi-select dropdown component with advanced features including:
 * - Single or multiple selection
 * - Search/filter functionality
 * - Hierarchical options (nested children)
 * - Custom templates
 * - Virtual scrolling for large lists
 *
 * @example
 * <sq-select-multi
 *   [options]="cities"
 *   [(value)]="selectedCities"
 *   [maxSelects]="3"
 *   [width]="'300px'"
 *   [minWidth]="'250px'"
 *   [dropdownWidth]="'350px'"
 * ></sq-select-multi>
 */
@Component({
  selector: 'sq-select-multi',
  templateUrl: './sq-select-multi.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['./sq-select-multi.component.scss'],
  standalone: true,
  imports: [
    NgClass,
    NgStyle,
    NgTemplateOutlet,
    AsyncPipe,
    FormsModule,
    ScrollingModule,
    SqLoaderComponent,
    SqTooltipComponent,
    SqSelectorComponent,
    UniversalSafePipe,
    TranslateInternalPipe,
    SearchPipe,
    SearchValidValuesPipe,
    SqClickOutsideDirective,
  ],
  providers: [],
})
export class SqSelectMultiComponent implements OnChanges {
  // #region Input Properties

  /**
   * Name attribute for the select element.
   * Defaults to a random unique name if not provided.
   */
  @Input() name = `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`;

  /**
   * Currently selected options (two-way bindable).
   */
  @Input() value: OptionMulti[] = [];

  /**
   * ID attribute for the select element.
   */
  @Input() id?: string;

  /**
   * Label text displayed above the select input.
   */
  @Input() label = '';

  /**
   * Custom CSS class to apply to the component container.
   */
  @Input() customClass = '';

  /**
   * Placeholder text shown when no options are selected.
   */
  @Input() placeholder = '';

  /**
   * External error message to display.
   */
  @Input() externalError = '';

  /**
   * External icon to display.
   */
  @Input() externalIcon = '';

  /**
   * Placeholder text for the search input.
   */
  @Input() placeholderSearch = '';

  /**
   * Disables the select input when true.
   */
  @Input() disabled = false;

  /**
   * Makes the select input read-only when true.
   */
  @Input() readonly = false;

  /**
   * Marks the field as required when true.
   */
  @Input() required = false;

  /**
   * Shows loading spinner when true.
   */
  @Input() loading = false;

  /**
   * Enables form error messages when true.
   */
  @Input() useFormErrors = true;

  /**
   * Shows error message container when true.
   */
  @Input() errorSpan = true;

  /**
   * Background color for the select input.
   */
  @Input() backgroundColor = '';

  /**
   * Border color for the select input.
   */
  @Input() borderColor = '';

  /**
   * Color for the label text.
   */
  @Input() labelColor = '';

  /**
   * Minimum characters required to trigger search.
   */
  @Input() minCharactersToSearch = 0;

  /**
   * Debounce time (in ms) for search input.
   */
  @Input() timeToChange = 800;

  /**
   * Array of available options for selection.
   */
  @Input() options: Array<OptionMulti> = [];

  /**
   * Maximum number of items that can be selected.
   */
  @Input() maxSelects?: number;

  /**
   * Minimum number of items required to be selected.
   */
  @Input() minSelects?: number;

  /**
   * Shows selected items inside the input when true.
   */
  @Input() showInside = true;

  /**
   * Hides the search input when true.
   */
  @Input() hideSearch = false;

  /**
   * Tooltip message content.
   */
  @Input() tooltipMessage = '';

  /**
   * Position of the tooltip relative to the label.
   */
  @Input() tooltipPlacement: 'center top' | 'center bottom' | 'left center' | 'right center' = 'right center';

  /**
   * Color scheme for the tooltip.
   */
  @Input() tooltipColor = 'inherit';

  /**
   * Icon to display with the tooltip.
   */
  @Input() tooltipIcon = '';

  /**
   * Width of the component container.
   * Can be any valid CSS width value (px, %, etc).
   * Default: '100%'
   */
  @Input() width = '100%';

  /**
   * Minimum width of the component container.
   * Default: '200px'
   */
  @Input() minWidth = '200px';

  /**
   * Width of the dropdown panel.
   * Can be different from the trigger width.
   * Default: '100%'
   */
  @Input() dropdownWidth = '100%';

  // #endregion

  // #region Output Events

  /**
   * Emitted when the selected values change.
   */
  @Output() valueChange: EventEmitter<Array<OptionMulti>> = new EventEmitter();

  /**
   * Emitted when the search text changes (with debounce).
   */
  @Output() searchChange: EventEmitter<string> = new EventEmitter();

  /**
   * Emitted when the dropdown closes.
   * Contains boolean indicating if selection changed.
   */
  @Output() closeChange: EventEmitter<boolean> = new EventEmitter();

  /**
   * Emitted when validation status changes.
   * True when valid, false when invalid.
   */
  @Output() valid: EventEmitter<boolean> = new EventEmitter();

  // #endregion

  // #region Content Projection

  /**
   * Custom template for the label content.
   */
  @ContentChild('labelTemplate') labelTemplate: TemplateRef<HTMLElement> | null = null;

  /**
   * Custom template to display when no options are available.
   */
  @ContentChild('selectEmptyTemplate') selectEmptyTemplate: TemplateRef<HTMLElement> | null = null;

  // #endregion

  // #region Internal State

  /**
   * Flag indicating whether the rendering of the options list is active
   * @internal
   */
  renderOptionsList = false;

  /**
   * State that controls whether the dropdown is open or closed
   * @internal
   */
  open = false;

  /**
   * Current text used for search/filter
   * @internal
   */
  searchText = '';

  /**
   * Flag indicating whether the value has changed since the last opening
   * @internal
   */
  valueChanged = false;

  /**
   * Temporary flag for timeout control
   * @internal
   */
  timeouted = false;

  /**
   * Current error message or error boolean state
   * @internal
   */
  error: boolean | string = '';

  /**
   * Reference to the native element of the component
   * @internal
   */
  nativeElement: ElementRef;

  /**
   * Internal list of options after filters are applied
   * @internal
   */
  _options: Array<OptionMulti> = [];

  /**
   * Flag indicating whether the maximum number of selections has been reached
   * @internal
   */
  ismaxSelects = false;

  /**
   * Reference to the search timeout for debounce
   * @internal
   */
  timeoutInput!: ReturnType<typeof setTimeout>;

  /**
   * Dynamic height of the virtual scroll viewport
   * Automatically calculated based on the number of options
   * @internal
   */
  cdkVirtualScrollViewportHeight = '305px';

  /**
   * Size of the items in the virtual scroll (in pixels)
   * @internal
   */
  cdkItemSize: string | null = '32';

  // #endregion

  /**
   * Constructor for the SqSelectMultiComponent.
   * Initializes the component with default values and dependencies.
   * @param element - ElementRef for accessing native DOM element
   * @param translate - Optional TranslateService for internationalization
   * @param changeDetector - ChangeDetectorRef for manual change detection
   */
  // #region Constructor
  constructor(
    public element: ElementRef,
    @Optional() private translate: TranslateService,
    private changeDetector: ChangeDetectorRef
  ) {
    this.nativeElement = element.nativeElement;
  }
  // #endregion

  // #region Lifecycle Methods

  /**
   * Angular lifecycle hook called when input properties change.
   * @param changes - Object containing changed properties.
   */
  async ngOnChanges(changes: SimpleChanges) {
    if (this.open && changes.hasOwnProperty('options')) {
      this.verifyNewOptions();
    }
    if (
      changes.hasOwnProperty('value') ||
      changes.hasOwnProperty('minSelects') ||
      changes.hasOwnProperty('maxSelects')
    ) {
      this.validate();
    }
  }

  // #endregion

  // #region Public Methods

  /**
   * Toggles the dropdown open/closed state.
   * Handles animations and rendering of options 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 search state.
   */
  closeDropdown() {
    this.open = false;
    this._options = [];
    this.searchText = '';
    this.closeChange.emit(this.valueChanged);
    this.valueChanged = false;
  }

  /**
   * Handles expanding/collapsing of hierarchical options.
   * @param item - The option item to toggle
   */
  handleCollapse(item: OptionMulti) {
    item.open = !item.open;
    if (item.children) {
      if (this.options.find(option => option.open) || item.open) {
        this.cdkItemSize = null;
      } else {
        this.cdkItemSize = '32';
      }
    }
  }

  // #endregion

  // #region Selection Methods

  /**
   * Determines if an item exists in the selected values.
   * @param item - The item to check
   * @param value - Optional array to check against (defaults to current value)
   * @returns True if the item is selected
   */
  findItemInValue = useMemo((item: OptionMulti, value?: Array<OptionMulti>) => {
    return !!value?.find(value => value.value === item.value);
  });

  /**
   * Checks if any options have children (hierarchy).
   * @param options - Array of options to check
   * @returns True if any option has children
   */
  verifyIfOptionsHasChildren = useMemo((options: OptionMulti[]) => {
    return options.some(item => item.children?.length);
  });

  /**
   * Checks if an item has any selected children.
   * @param item - The parent item to check
   * @param value - Optional array to check against (defaults to current value)
   * @returns True if any child is selected
   */
  verifyIfHasChildrenInValue = useMemo((item: OptionMulti, value?: Array<OptionMulti>) => {
    if (item.children?.length) {
      const hasAllChildren = item.children.every(child => this.findItemInValue(child, value));
      if (hasAllChildren && !this.findItemInValue(item, value) && !this.timeouted) {
        this.timeouted = true;
        setTimeout(() => {
          this.emit(item, true);
          this.timeouted = false;
        }, 0);
      }
      return !!item.children.find(child => this.findItemInValue(child, value));
    }
    return false;
  });

  /**
   * Adds or removes an item from the selection.
   * @param object - The option to select/deselect
   * @param checked - True to select, false to deselect
   */
  emit(object: OptionMulti, checked: boolean) {
    if (checked) {
      this.value?.push(object);
    } else {
      this.value = this.value?.filter(item => item.value !== object.value);
      if (object.children?.length) {
        object.children.forEach(item => {
          this.value = this.value?.filter(child => child.value !== item.value);
        });
      }
    }
    this.valueChanged = true;
    this.valueChange.emit(this.value);
    this.validate();
  }

  // #endregion

  // #region Validation

  /**
   * Validates the current selection state.
   * Updates error messages and validity status.
   */
  validate() {
    if (this.externalError) {
      this.error = false;
    } else if (this.required && !this.value?.length) {
      this.setError('forms.required');
      this.valid.emit(false);
    } else if (this.minSelects && this.value && this.value?.length < this.minSelects) {
      this.setError('forms.minimumRequired', { minSelects: this.minSelects });
      this.valid.emit(false);
    } else if (this.maxSelects && this.value && this.value?.length === this.maxSelects) {
      this.renderOptionsList = false;
      this.ismaxSelects = true;
      this.error = '';
      this.valid.emit(true);
    } else {
      this.ismaxSelects = false;
      this.error = '';
      this.valid.emit(true);
    }
  }

  /**
   * Sets an error message using translation.
   * @param key - Translation key for the error
   * @param interpolateParams - Optional interpolation parameters
   */
  async setError(key: string, interpolateParams: Object = {}) {
    if (this.useFormErrors && this.translate) {
      this.error = await this.translate.instant(key, interpolateParams);
    }
  }

  // #endregion

  // #region Search & Filtering

  /**
   * Handles search input changes with debounce.
   * @param event - The new search text
   */
  async modelChange(event: any) {
    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();
    }
  }

  // #endregion

  // #region Virtual Scroll Helpers

  /**
   * Updates options list and calculates virtual scroll viewport height.
   * Adjusts based on number of options for optimal performance.
   */
  verifyNewOptions() {
    this._options = this.options;
    if (!this._options.length) {
      this.cdkVirtualScrollViewportHeight = '16px';
    } else if (this._options.length < 15) {
      this.cdkVirtualScrollViewportHeight = this._options.length * 32 + 'px';
    } else {
      this.cdkVirtualScrollViewportHeight = '305px';
    }
  }

  /**
   * TrackBy function for ngFor optimizations.
   * @param index - Item index
   * @param opt - Option item
   * @returns Unique identifier for the item
   */
  trackByOptValue: TrackByFunction<any> = useMemo((index, opt) => opt.value);

  // #endregion
}
<div class="wrapper-all-inside-input {{ customClass }}" [style.width]="width" [style.min-width]="minWidth">
  @if (label || labelTemplate || tooltipMessage) {
    <label class="display-flex align-items-center" [ngClass]="{ readonly: readonly || ismaxSelects }" [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
    class="wrapper-select-multi"
    [ngClass]="{
      error: (externalError && externalError !== '') || (error && error !== ''),
      disabled: disabled,
      readonly: readonly || ismaxSelects,
      loading: loading,
    }"
  >
    <div
      [class]="'input-fake col  border-' + borderColor"
      style="min-height: auto"
      [ngStyle]="{ 'border-color': borderColor }"
      [ngClass]="{
        'no-label': !(label && label.length > 0),
        'has-icon': error || externalError,
        disabled: disabled,
        readonly: readonly || ismaxSelects,
      }"
      [ngStyle]="{
        'background-color': backgroundColor,
        'border-color': borderColor,
      }"
      [clickOutsideEnabled]="open"
      (clickOutside)="closeDropdown()"
    >
      <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?.length) {
          <span>{{ placeholder }}</span>

          <div class="input-fake-content-text">
            <span class="selected-value">
              {{ value[0]?.label }}
              @if (value.length > 1) {
                <span> {{ 'forms.more' | translateInternal | async }} {{ value.length - 1 }} </span>
              }
            </span>
          </div>
        }

        @if (!loading) {
          <i class="icon-down fas fa-chevron-down"> </i>
        }
      </div>

      @if (!loading && !disabled && !readonly && !ismaxSelects && renderOptionsList) {
        <div
          id="sq-select-multi-tags-scroll"
          class="input-window scrollbar"
          [ngClass]="{
            open: !loading && !disabled && !ismaxSelects && renderOptionsList && open,
          }"
        >
          <div class="input-search">
            <div class="wrapper-all-inside-input">
              <div class="p-0 wrapper-input wrapper-input-squid text-ellipsisarea">
                @if (!hideSearch) {
                  <input
                    [name]="name"
                    [id]="id"
                    [placeholder]="placeholderSearch || ('forms.search' | translateInternal | async) || ''"
                    class="col input"
                    [ngModel]="searchText"
                    (ngModelChange)="modelChange($event)"
                  />
                }
              </div>
              <span class="icon icon-external textarea-icon">
                <i class="fas fa-search"></i>
              </span>
            </div>
          </div>

          <cdk-virtual-scroll-viewport
            [itemSize]="cdkItemSize"
            [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>
            } @else {
              <span>
                <ng-container *ngTemplateOutlet="selectEmptyTemplate"> </ng-container>
              </span>
            }
          }
        </div>
      }
    </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>
  }

  <ng-template #option let-opt="opt" let-i="i">
    <li>
      <div class="label">
        @if (verifyIfOptionsHasChildren(options)) {
          <i
            class="icon-collapse fas fa-chevron-down"
            [ngClass]="{ 'fa-rotate-by': !opt.open }"
            [ngStyle]="{ color: !opt?.children?.length || opt?.disabled ? 'transparent' : '' }"
            (click)="handleCollapse(opt)"
            style="--fa-rotate-angle: -90deg"
          ></i>
        }

        <sq-selector
          [style.minWidth]="'auto'"
          [id]="(id || name) + '-checkbox-' + opt?.value + '-' + i"
          [name]="name + '-checkbox'"
          [value]="opt?.value"
          [disabled]="opt?.disabled"
          [checked]="findItemInValue(opt, value)"
          [indeterminate]="verifyIfHasChildrenInValue(opt, value)"
          (valueChange)="emit(opt, $event.checked)"
        >
        </sq-selector>
        <span
          class="text m-0 display-inline-block"
          [ngClass]="{ 'cursor-pointer': opt?.children?.length }"
          (click)="handleCollapse(opt)"
        >
          {{ opt?.label }}
        </span>
      </div>
      @if (opt?.children?.length) {
        <ul class="children" [ngClass]="{ open: !opt?.disabled && opt?.open }">
          @for (
            child of opt?.children | searchValidValues: searchText;
            track trackByOptValue(j, child);
            let j = $index
          ) {
            <ng-template *ngTemplateOutlet="option; context: { opt: child, i: j }"></ng-template>
          }
        </ul>
      }
    </li>
  </ng-template>
</div>

./sq-select-multi.component.scss

.wrapper-select-multi {
  margin: 0;
  padding: 0;
  text-align: left;
  display: flex;
  flex-direction: column;
  position: relative;

  &.loading {
    cursor: wait;
  }

  .input-fake-content {
    min-height: 44px;
    border: 1px solid var(--border_color);
    background: var(--background);
    padding: 0.75rem 1rem;
    transition: var(--transition);
    color: var(--text_color);
    border-radius: 5px;
    display: flex;
    align-items: center;
    position: relative;

    .selected-value {
      display: inline-flex;
      align-items: center;
      gap: 5px;
    }

    .additional-count {
      background: var(--border_color);
      border-radius: 10px;
      padding: 0 6px;
      font-size: 0.85rem;
    }

    .icon-down {
      position: absolute;
      right: 15px;
      top: 50%;
      transform: translateY(-50%);
      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%;
    min-width: 100%;
    position: absolute;
    top: 100%;
    max-height: 0;
    height: 0;
    transition: var(--transition);
    overflow: hidden;
    z-index: 1;

    &.open {
      height: auto;
      max-height: 400px;
      background: var(--background_secondary);
      padding: 0 0.7rem 1.1rem;
      overflow-y: auto;
      box-shadow: var(--box_shadow);
    }
  }

  .icon-collapse {
    transition: var(--transition);
    cursor: pointer;
    padding: 0.35rem;
    position: relative;
  }

  .list,
  .children {
    padding: 0;
    list-style: none;
    margin: 0;
    width: 100%;

    li {
      display: grid;

      .label {
        margin: 0 0 0.5rem;
        display: flex;
        flex-wrap: nowrap;
        align-items: baseline;
        justify-content: flex-start;
        transition: var(--transition);
      }
    }
  }

  .children {
    max-height: 0;
    overflow: hidden;
    transition: var(--transition);
    padding-left: 1.4rem;

    &.open {
      max-height: 9999px;
      height: auto;
      max-height: 400px;
    }

    .list {
      overflow-y: auto;
      max-height: 300px;

      li {
        padding: 8px 12px;
      }
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""