File

src/app/core/common-components/basic-autocomplete/basic-autocomplete.component.ts

Description

Custom MatFormFieldControl for any select / dropdown field.

Extends

CustomFormControlDirective

Implements

OnChanges OnInit AfterViewInit

Metadata

Index

Properties
Methods
Inputs
Outputs
Accessors

Inputs

createOption
Type : function
display
Type : "text" | "chips" | "none"
Default value : "text"

Display the selected items as simple text, as chips or not at all (if used in combination with another component)

hideOption
Type : function
Default value : () => false
multi
Type : boolean

Whether the user should be able to select multiple values.

options
Type : O[]

The options to display in the autocomplete dropdown. If you pass complex objects here, you can customize what value is displayed and what value is output/stored by overriding the valueMapper and optionToString methods via inputs. By default, the "_id" property is used as the value and the "_label" property or toString() method as the display value.

optionToString
Type : (option: O) => any
Default value : (option: O) => option?.["_label"] ?? option?.toString()
reorder
Type : boolean

Whether the user can manually drag & drop to reorder the selected items

valueMapper
Type : (option: O) => any
Default value : (option: O) => option?.["_id"] ?? (option as unknown as V)
aria-describedby
Type : string
disabled
Type : boolean
ngControl
Type : any
Default value : inject(NgControl, { optional: true, self: true })
placeholder
Type : string
required
Type : boolean
value
Type : T

Outputs

autocompleteFilterChange
Type : EventEmitter
valueChange
Type : EventEmitter

Methods

compareEnumValues
compareEnumValues(a: any, b: any)

Compare two enum values by id if present, otherwise by reference.

Parameters :
Name Type Optional
a any No
b any No
Returns : boolean
Async createNewOption
createNewOption(option: string)
Parameters :
Name Type Optional
option string No
Returns : any
drop
drop(event: CdkDragDrop)
Parameters :
Name Type Optional
event CdkDragDrop<any[]> No
Returns : void
onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
onFocusOut
onFocusOut(event: FocusEvent)
Parameters :
Name Type Optional
event FocusEvent No
Returns : void
select
select(selected: string | SelectableOption<O | V>)
Parameters :
Name Type Optional
selected string | SelectableOption<O | V> No
Returns : void
showAutocomplete
showAutocomplete(valueToRevertTo?: string)
Parameters :
Name Type Optional
valueToRevertTo string Yes
Returns : void
unselect
unselect(option: SelectableOption<O | V>)
Parameters :
Name Type Optional
option SelectableOption<O | V> No
Returns : void
Public updatePanelWidth
updatePanelWidth()

Set the width of the dropdown panel programmatically to match the parent form field. (this is not possible with pure CSS)

Note: If the field is close to the viewport edge, Angular Material's overlay system may shift the dropdown horizontally to keep it visible, causing minor misalignment. This is expected and ensures accessibility.

Returns : void
writeValue
writeValue(val: V[] | V, notifyFormControl: unknown)
Parameters :
Name Type Optional Default value
val V[] | V No
notifyFormControl unknown No false
Returns : void
blur
blur()
Returns : void
focus
focus()
Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
setDescribedByIds
setDescribedByIds(ids: string[])
Parameters :
Name Type Optional
ids string[] No
Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void

Properties

_selectedOptions
Type : SelectableOption<O, V>[]
Default value : []
autocomplete
Type : MatAutocompleteTrigger
Decorators :
@ViewChild(MatAutocompleteTrigger)
autocompleteFilterFunction
Type : function
autocompleteForm
Type : unknown
Default value : new FormControl("")
autocompleteOptions
Type : SelectableOption<O, V>[]
Default value : []
autocompleteSuggestedOptions
Type : unknown
Default value : this.autocompleteForm.valueChanges.pipe( filter((val) => typeof val === "string"), map((val) => this.updateAutocomplete(val)), startWith([] as SelectableOption<O, V>[]), )
inputElement
Type : unknown
Decorators :
@ViewChild(MatInput, {static: true})
isInSearchMode
Type : WritableSignal<boolean>
Default value : signal(false)

display the search input rather than the selected elements only (when the form field gets focused).

maxPanelHeight
Type : number

maximum height of the autocomplete panel. We need a calculation to avoid multiple scrollbars, couldn't get this working just with css.

panelWidth
Type : string

Dynamic width of the autocomplete dropdown panel. Set to match the full width of the Material form field container (including icons/padding).

retainSearchValue
Type : string

Keep the search value to help users quickly multi-select multiple related options without having to type filter text again

showAddOption
Type : unknown
Default value : false

whether the "add new" option is logically allowed in the current context (e.g. not creating a duplicate)

templateRef
Type : TemplateRef<any>
Decorators :
@ContentChild(TemplateRef)
trackByOptionValueFn
Type : TrackByFunction<SelectableOption<O, V>> | undefined
Default value : () => {...}
virtualScrollViewport
Type : CdkVirtualScrollViewport
Decorators :
@ViewChild(CdkVirtualScrollViewport)
_disabled
Type : unknown
Default value : false
_value
Type : T
controlType
Type : string
Default value : "custom-control"
elementRef
Type : unknown
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
errorState
Type : unknown
Default value : false
errorStateMatcher
Type : unknown
Default value : inject(ErrorStateMatcher)
focused
Type : unknown
Default value : false
id
Type : unknown
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
Static nextId
Type : number
Default value : 0
onChange
Type : unknown
Default value : () => {...}
onTouched
Type : unknown
Default value : () => {...}
parentForm
Type : unknown
Default value : inject(NgForm, { optional: true })
parentFormGroup
Type : unknown
Default value : inject(FormGroupDirective, { optional: true })
stateChanges
Type : unknown
Default value : new Subject<void>()
touched
Type : unknown
Default value : false

Accessors

displayText
getdisplayText()
disabled
getdisabled()
setdisabled(value: boolean)
Parameters :
Name Type Optional
value boolean No
Returns : void
options
setoptions(options: O[])

The options to display in the autocomplete dropdown. If you pass complex objects here, you can customize what value is displayed and what value is output/stored by overriding the valueMapper and optionToString methods via inputs. By default, the "_id" property is used as the value and the "_label" property or toString() method as the display value.

Parameters :
Name Type Optional Description
options O[] No

Array of available options (can be filtered further by the hideOption function)

Returns : void
import {
  AfterViewInit,
  Component,
  ContentChild,
  ElementRef,
  EventEmitter,
  Input,
  OnChanges,
  OnInit,
  Output,
  signal,
  TemplateRef,
  TrackByFunction,
  ViewChild,
  WritableSignal,
} from "@angular/core";
import { NgClass, NgForOf, NgIf, NgTemplateOutlet } from "@angular/common";
import { MatFormFieldControl } from "@angular/material/form-field";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { MatInput, MatInputModule } from "@angular/material/input";
import {
  MatAutocompleteModule,
  MatAutocompleteTrigger,
} from "@angular/material/autocomplete";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { filter, map, startWith } from "rxjs/operators";
import { CustomFormControlDirective } from "./custom-form-control.directive";
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
  MatChipGrid,
  MatChipInput,
  MatChipRemove,
  MatChipRow,
} from "@angular/material/chips";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { MatTooltip } from "@angular/material/tooltip";
import {
  CdkDragDrop,
  DragDropModule,
  moveItemInArray,
} from "@angular/cdk/drag-drop";
import {
  CdkFixedSizeVirtualScroll,
  CdkVirtualForOf,
  CdkVirtualScrollViewport,
} from "@angular/cdk/scrolling";

interface SelectableOption<O, V> {
  initial: O;
  asString: string;
  asValue: V;
  selected: boolean;
  isHidden: boolean;
  isInvalid?: boolean;
  isEmpty?: boolean;
}

export const BASIC_AUTOCOMPLETE_COMPONENT_IMPORTS = [
  ReactiveFormsModule,
  MatInputModule,
  MatAutocompleteModule,
  NgForOf,
  MatCheckboxModule,
  NgIf,
  NgTemplateOutlet,
  MatChipInput,
  MatChipGrid,
  MatChipRow,
  FaIconComponent,
  MatTooltip,
  MatChipRemove,
  DragDropModule,
  CdkVirtualScrollViewport,
  CdkVirtualForOf,
  CdkFixedSizeVirtualScroll,
  NgClass,
];

/**
 * Custom `MatFormFieldControl` for any select / dropdown field.
 */
@Component({
  selector: "app-basic-autocomplete",
  templateUrl: "basic-autocomplete.component.html",
  styleUrls: ["./basic-autocomplete.component.scss"],
  providers: [
    { provide: MatFormFieldControl, useExisting: BasicAutocompleteComponent },
  ],
  imports: BASIC_AUTOCOMPLETE_COMPONENT_IMPORTS,
})
export class BasicAutocompleteComponent<O, V = O>
  extends CustomFormControlDirective<V | V[]>
  implements OnChanges, OnInit, AfterViewInit
{
  @ContentChild(TemplateRef) templateRef: TemplateRef<any>;
  // `_elementRef` is protected in `MapInput`
  @ViewChild(MatInput, { static: true }) inputElement: MatInput & {
    _elementRef: ElementRef<HTMLElement>;
  };
  @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
  @ViewChild(CdkVirtualScrollViewport)
  virtualScrollViewport: CdkVirtualScrollViewport;

  @Input() valueMapper = (option: O) =>
    option?.["_id"] ?? (option as unknown as V);
  @Input() optionToString = (option: O) =>
    option?.["_label"] ?? option?.toString();
  @Input() createOption: (input: string) => Promise<O>;
  @Input() hideOption: (option: O) => boolean = () => false;

  /**
   * Whether the user should be able to select multiple values.
   */
  @Input() multi?: boolean;

  /**
   * Whether the user can manually drag & drop to reorder the selected items
   */
  @Input() reorder?: boolean;

  autocompleteOptions: SelectableOption<O, V>[] = [];
  autocompleteForm = new FormControl("");
  autocompleteSuggestedOptions = this.autocompleteForm.valueChanges.pipe(
    filter((val) => typeof val === "string"),
    map((val) => this.updateAutocomplete(val)),
    startWith([] as SelectableOption<O, V>[]),
  );
  autocompleteFilterFunction: (option: O) => boolean;
  @Output() autocompleteFilterChange = new EventEmitter<(o: O) => boolean>();

  /** whether the "add new" option is logically allowed in the current context (e.g. not creating a duplicate) */
  showAddOption = false;

  /**
   * maximum height of the autocomplete panel.
   * We need a calculation to avoid multiple scrollbars, couldn't get this working just with css.
   */
  maxPanelHeight: number;
  /**
   * Dynamic width of the autocomplete dropdown panel.
   * Set to match the full width of the Material form field container (including icons/padding).
   */
  panelWidth: string;

  get displayText() {
    const values: V[] = Array.isArray(this.value) ? this.value : [this.value];

    return values
      .map(
        (v) =>
          this._options.find((o) => this.compareEnumValues(o.asValue, v))
            ?.asString,
      )
      .join(", ");
  }

  override get disabled(): boolean {
    return this._disabled;
  }

  override set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
    this._disabled
      ? this.autocompleteForm.disable()
      : this.autocompleteForm.enable();
    this.stateChanges.next();
  }

  /**
   * The options to display in the autocomplete dropdown.
   * If you pass complex objects here, you can customize what value is displayed and what value is output/stored
   * by overriding the `valueMapper` and `optionToString` methods via inputs.
   * By default, the "_id" property is used as the value and the "_label" property or `toString()` method as the display value.
   *
   * @param options Array of available options (can be filtered further by the `hideOption` function)
   */
  @Input() set options(options: O[]) {
    this._options = options.map((o) => this.toSelectableOption(o));
  }

  private _options: SelectableOption<O, V>[] = [];

  _selectedOptions: SelectableOption<O, V>[] = [];

  /**
   * Keep the search value to help users quickly multi-select multiple related options without having to type filter text again
   */
  retainSearchValue: string;

  /**
   * Display the selected items as simple text, as chips or not at all (if used in combination with another component)
   */
  @Input() display: "text" | "chips" | "none" = "text";

  /**
   * display the search input rather than the selected elements only
   * (when the form field gets focused).
   */
  isInSearchMode: WritableSignal<boolean> = signal(false);
  trackByOptionValueFn: TrackByFunction<SelectableOption<O, V>> | undefined = (
    i,
    o,
  ) => o?.asValue;

  ngOnInit() {
    this.autocompleteSuggestedOptions.subscribe((options) => {
      this.autocompleteOptions = options;
      setTimeout(() => {
        this.virtualScrollViewport.checkViewportSize();
      });
    });
    // Subscribe to the valueChanges observable to print the input value
    this.autocompleteForm.valueChanges.subscribe((value) => {
      if (typeof value === "string") {
        this.retainSearchValue = value;
      }
    });
  }

  ngOnChanges(changes: { [key in keyof this]?: any }) {
    if (changes.valueMapper) {
      this._options.forEach(
        (opt) => (opt.asValue = this.valueMapper(opt.initial)),
      );
    }
    if (changes.optionToString) {
      this._options.forEach(
        (opt) => (opt.asString = this.optionToString(opt.initial)),
      );
    }
    if (changes.value || changes.options) {
      this.setInitialInputValue();

      if (this.autocomplete?.panelOpen) {
        // if new options have been added, make sure to update the visible autocomplete options
        this.showAutocomplete(this.autocompleteForm.value);
      }
    }
  }

  ngAfterViewInit() {
    window.addEventListener("focus", () => {
      if (this.autocomplete?.panelOpen) {
        this.showAutocomplete();
      }
    });

    this.calculateVisibleItemsForHeight();
  }

  private calculateVisibleItemsForHeight() {
    const screenHeight = window.innerHeight;
    const inputBottom =
      this.inputElement._elementRef.nativeElement.getBoundingClientRect()
        .bottom;

    const availableSpaceBelow = screenHeight - inputBottom;

    // workaround for ExpressionChangedAfterItHasBeenCheckedError problems
    setTimeout(() => {
      const maxVisibleItems = Math.max(3, Math.floor(availableSpaceBelow / 48));

      this.maxPanelHeight = Math.min(maxVisibleItems * 48, availableSpaceBelow);
      this.virtualScrollViewport.checkViewportSize();
    }, 0);
  }

  /**
   * Set the width of the dropdown panel programmatically to match the parent form field.
   * (this is not possible with pure CSS)
   *
   * Note: If the field is close to the viewport edge, Angular Material's overlay system may shift the dropdown horizontally
   * to keep it visible, causing minor misalignment. This is expected and ensures accessibility.
   */
  public updatePanelWidth() {
    // Use closest .mat-mdc-form-field or .mat-form-field from input element
    const fieldEl = this.inputElement?._elementRef?.nativeElement.closest(
      ".mat-mdc-form-field, .mat-form-field",
    ) as HTMLElement;
    const fieldWidth = fieldEl ? fieldEl.getBoundingClientRect().width : 200;
    this.panelWidth = `${fieldWidth}px`;
  }

  drop(event: CdkDragDrop<any[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(
        this.autocompleteOptions,
        event.previousIndex,
        event.currentIndex,
      );
    }
    this._selectedOptions = this.autocompleteOptions.filter((o) => o.selected);
    if (this.multi) {
      this.value = this._selectedOptions.map((o) => o.asValue);
    } else {
      this.value = undefined;
    }
    this.setInitialInputValue();
    this.onChange(this.value);
    this.showAutocomplete(this.autocompleteForm.value);
  }

  showAutocomplete(valueToRevertTo?: string) {
    if (this.multi && this.retainSearchValue) {
      // reset the search value to previously entered text to help user selecting multiple similar options without retyping filter text
      this.autocompleteForm.setValue(this.retainSearchValue);
    } else {
      // reset the search value to show all available options again
      this.autocompleteForm.setValue("");
    }
    if (!this.multi) {
      // cannot setValue to "" here because the current selection would be lost
      this.autocompleteForm.setValue(this.displayText, { emitEvent: false });
    }

    // Update panel width when autocomplete is actually shown (when form field is rendered)
    this.updatePanelWidth();

    setTimeout(() => {
      this.inputElement.focus();

      // select all text for easy overwriting when typing to search for options
      (
        this.inputElement._elementRef.nativeElement as HTMLInputElement
      ).select();
      if (valueToRevertTo) {
        this.autocompleteForm.setValue(valueToRevertTo);
      }
    });

    this.isInSearchMode.set(true);

    // update virtual scroll as the container remains empty until the user scrolls initially
    setTimeout(() => this.virtualScrollViewport.checkViewportSize());
  }

  private updateAutocomplete(inputText: string): SelectableOption<O, V>[] {
    let filteredOptions = this._options.filter(
      (o) => !this.hideOption(o.initial) && !o.isHidden,
    );
    if (inputText) {
      this.autocompleteFilterFunction = (option) =>
        this.optionToString(option)
          ?.toLowerCase()
          ?.includes(inputText.toLowerCase());
      this.autocompleteFilterChange.emit(this.autocompleteFilterFunction);

      filteredOptions = filteredOptions.filter((o) =>
        this.autocompleteFilterFunction(o.initial),
      );

      // do not allow users to create a new entry "identical" to an existing one:
      this.showAddOption = !this._options.some(
        (o) => o?.asString?.toLowerCase() === inputText?.toLowerCase(),
      );
    }
    return filteredOptions;
  }

  /**
   * Compare two enum values by id if present, otherwise by reference.
   */
  compareEnumValues(a: any, b: any): boolean {
    if (a === b) return true;
    if (!a || !b) return false;
    if (a.id !== undefined && b.id !== undefined) {
      return a.id === b.id;
    }
    if (a.value !== undefined && b.value !== undefined) {
      return a.value === b.value;
    }
    return false;
  }

  private setInitialInputValue() {
    this._options.forEach(
      (o) =>
        (o.selected = Array.isArray(this.value)
          ? this.value?.some((v) => this.compareEnumValues(v, o.asValue))
          : this.compareEnumValues(this.value, o.asValue)),
    );
    this._selectedOptions = this._options.filter(
      (o) => o.selected && !o.isHidden,
    );
  }

  select(selected: string | SelectableOption<O, V>) {
    if (typeof selected === "string") {
      this.createNewOption(selected);
      return;
    }

    if (selected) {
      this.selectOption(selected);
    } else {
      this.autocompleteForm.setValue("");
      this._selectedOptions = [];
      this.value = undefined;
    }
    this.onChange(this.value);
  }

  unselect(option: SelectableOption<O, V>) {
    option.selected = false;
    this._selectedOptions = this._options.filter((o) => o.selected);

    if (this.multi) {
      this.value = this._selectedOptions.map((o) => o.asValue);
    } else {
      this.value = undefined;
    }
    this.onChange(this.value);
  }

  async createNewOption(option: string) {
    const createdOption = await this.createOption(option);
    if (createdOption) {
      const newOption = this.toSelectableOption(createdOption);
      this._options.push(newOption);
      this.select(newOption);
    } else {
      // continue editing
      this.showAutocomplete();
      this.autocompleteForm.setValue(option);
    }
  }

  private selectOption(option: SelectableOption<O, V>) {
    if (this.multi) {
      option.selected = !option.selected;
      this._selectedOptions = this._options.filter((o) => o.selected);
      this.value = this._selectedOptions.map((o) => o.asValue);
      // re-open autocomplete to select next option
      setTimeout(() => this.showAutocomplete());
    } else {
      this._selectedOptions = [option];
      this.value = option.asValue;
      this.isInSearchMode.set(false);
    }
  }

  private toSelectableOption(opt: O): SelectableOption<O, V> {
    return {
      initial: opt,
      asValue: this.valueMapper(opt),
      asString: this.optionToString(opt),
      selected: false,
      isHidden: (opt as SelectableOption<O, V>)?.isHidden ?? false,
      isInvalid: (opt as SelectableOption<O, V>)?.isInvalid ?? false,
      isEmpty: (opt as SelectableOption<O, V>)?.isEmpty ?? false,
    };
  }

  onFocusOut(event: FocusEvent) {
    if (
      !this.elementRef.nativeElement.contains(event.relatedTarget as Element)
    ) {
      if (!this.multi && this.autocompleteForm.value === "") {
        this.select(undefined);
      }
      this.isInSearchMode.set(false);
      this.retainSearchValue = "";
    }
  }

  override onContainerClick(event: MouseEvent) {
    if (
      !this._disabled &&
      (event.target as Element).tagName.toLowerCase() != "input"
    ) {
      this.showAutocomplete();
    }
  }

  override writeValue(val: V[] | V, notifyFormControl = false): void {
    super.writeValue(val, notifyFormControl);
    this.setInitialInputValue();
  }
}
<!--Display-->
@if (display === "text" || display === "none") {
  <input
    [id]="id"
    [hidden]="isInSearchMode() || display === 'none'"
    [disabled]="_disabled"
    matInput
    style="text-overflow: ellipsis; width: calc(100% - 50px)"
    (focusin)="showAutocomplete()"
    (focusout)="showAutocomplete()"
    [value]="displayText"
    [placeholder]="placeholder"
  />
} @else {
  <input
    [hidden]="true"
    [disabled]="_disabled"
    matInput
    (focusin)="showAutocomplete()"
    (focusout)="showAutocomplete()"
    [matChipInputFor]="chipList"
  />
  <mat-chip-grid #chipList>
    <ng-container>
      @for (item of _selectedOptions; track item) {
        <mat-chip-row
          [editable]="!_disabled"
          class="chip"
          [style.background-color]="item.asValue?.['color']"
        >
          @if (!templateRef) {
            {{ item.asString }}
          } @else {
            <ng-template
              [ngTemplateOutlet]="templateRef"
              [ngTemplateOutletContext]="{ $implicit: item.initial }"
            ></ng-template>
          }
          @if (!_disabled) {
            <button matChipRemove (click)="unselect(item)">
              <fa-icon
                i18n-matTooltip="
                  tooltip for remove icon on chips of dropdown item
                "
                matTooltip="remove"
                icon="xmark"
              ></fa-icon>
            </button>
          }
        </mat-chip-row>
      }
    </ng-container>
  </mat-chip-grid>
}

<!--Search-->
<input
  [hidden]="!isInSearchMode()"
  #inputElement
  [formControl]="autocompleteForm"
  matInput
  style="text-overflow: ellipsis"
  [matAutocomplete]="autoSuggestions"
  (focusout)="onFocusOut($event)"
  [placeholder]="placeholder"
/>

<!--
Autocomplete
-->
<mat-autocomplete
  [disableRipple]="true"
  #autoSuggestions="matAutocomplete"
  (optionSelected)="select($event.option.value)"
  autoActiveFirstOption
  [hideSingleSelectionIndicator]="true"
  [class.custom-panel]="true"
  [style.max-height.px]="maxPanelHeight"
  [panelWidth]="panelWidth"
>
  <!-- Select All and Clear Buttons on top of options via content projection-->
  <div>
    <ng-content select="[autocompleteHeader]"></ng-content>
  </div>
  <div
    cdkDropList
    (cdkDropListDropped)="drop($event)"
    cdkDropListGroup
    [cdkDropListDisabled]="!reorder"
  >
    <cdk-virtual-scroll-viewport
      [style.height.px]="(autocompleteOptions?.length ?? 0) * 48"
      [itemSize]="48"
      minBufferPx="200"
    >
      <mat-option
        [value]="item"
        cdkDrag
        *cdkVirtualFor="
          let item of autocompleteOptions;
          trackBy: trackByOptionValueFn
        "
      >
        <div class="flex-row disable-autocomplete-active-color align-center">
          @if (reorder) {
            <div>
              <fa-icon
                icon="grip-vertical"
                size="sm"
                class="drag-handle"
              ></fa-icon>
            </div>
          }
          @if (multi) {
            <mat-checkbox [checked]="item.selected"></mat-checkbox>
          }
          @if (!templateRef) {
            <span
              class="text-truncate"
              [matTooltip]="item.asString"
              matTooltipPosition="above"
              [ngClass]="{
                'not-defined-label': item.isEmpty,
                'invalid-label': item.isInvalid,
              }"
            >
              {{ item.asString }}
            </span>
          } @else {
            <ng-template
              class="item-option"
              [ngTemplateOutlet]="templateRef"
              [ngTemplateOutletContext]="{ $implicit: item.initial }"
            ></ng-template>
          }
        </div>
      </mat-option>
    </cdk-virtual-scroll-viewport>
  </div>

  <!-- Create new option -->
  @if (createOption && showAddOption && inputElement.value) {
    <mat-option [value]="inputElement.value">
      <em
        i18n="
          Label for adding an option in a dropdown|e.g. Add new My new Option
        "
        >Add new</em
      >
      {{ inputElement.value }}
    </mat-option>
  }

  <mat-option style="display: none">
    <!-- This mat-option is never displayed ("display: none") but has to be there,
      because the footer below will only be displayed with at least one mat-option -->
  </mat-option>
  <div class="autocomplete-footer">
    <ng-content select="[autocompleteFooter]"></ng-content>
  </div>
</mat-autocomplete>

./basic-autocomplete.component.scss

@use "variables/colors";
@use "variables/sizes";

@use "../special-option-labels.scss";

:host {
  overflow: hidden;
}

em {
  color: colors.$primary;
}

.disable-autocomplete-active-color {
  color: black;
}

.autocomplete-footer {
  margin: sizes.$small sizes.$regular;
}

.mat-autocomplete-panel.custom-panel {
  overflow-y: hidden !important;
  max-height: none !important;
}

::ng-deep {
  .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
    width: 100%;
  }
}

.text-truncate {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip {
  border-radius: 4px;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""