src/app/core/entity/entity-type-select/entity-type-select.component.ts

Extends

BasicAutocompleteComponent< EntityConstructor, string >

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Inputs

placeholder
Type : any
Default value : $localize`:EntityTypeSelect placeholder:Select Record Type`
hiddenTypes
Type : string[]
Default value : []

ENTITY_TYPE keys to omit from the dropdown, e.g. types that are already selected elsewhere and should not be offered again.

optionToString
Type : (option: EntityConstructor) => string
Default value : (option) => option.label ?? option.ENTITY_TYPE
showInternalTypes
Type : boolean
Default value : false

whether to include record types without a human-readable label (usually only used internally for technical purposes)

valueMapper
Type : (option: EntityConstructor) => string
Default value : (option) => option.ENTITY_TYPE
createOption
Type : (input: string) => Promise<O>
createOptions
Type : CreateOptionConfig<O>[]
Default value : []
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)

displayFullLengthOptionLabel
Type : boolean
Default value : false

Whether dropdown option labels should be shown in full length. Set to false to truncate labels with ellipsis.

hideOption
Type : (option: O) => boolean
Default value : () => false
maxOptionsToDisplay
Type : number
Default value : 100

Maximum number of options to display in the dropdown. If more options match the current filter, a hint is shown to type to narrow results. Set to 0 for no limit. Defaults to 100.

multi
Type : boolean

Whether the user should be able to select multiple values.

options
Type : O[]
Default value : []

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.

reorder
Type : boolean

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

aria-describedby
Type : string
disabled
Type : boolean
ngControl
Type : any
Default value : inject(NgControl, { optional: true, self: true })
required
Type : boolean
value
Type : T

Outputs

autocompleteFilterChange
Type : (o: O) => boolean
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 createFromConfig
createFromConfig(marker: CreateOptionMarker<O>)
Parameters :
Name Type Optional
marker CreateOptionMarker<O> No
Returns : any
Protected createOptionAriaLabel
createOptionAriaLabel(option: CreateOptionConfig<O>, input: string)
Parameters :
Name Type Optional
option CreateOptionConfig<O> No
input string No
Returns : string
Protected createOptionDisplay
createOptionDisplay(input: string)

Used in template to display the "Add new" option label. Delegates to optionToString so callers with a custom optionToString (e.g. showing an example date) get a preview in the "Add new" option too. Falls back to the raw input when optionToString throws or returns null/undefined (e.g. when O is an object type whose properties don't exist on a plain string).

Parameters :
Name Type Optional
input string No
Returns : string
Protected createOptionLabel
createOptionLabel(option: CreateOptionConfig<O>, input: string)
Parameters :
Name Type Optional
option CreateOptionConfig<O> No
input string No
Returns : string
dropChips
dropChips(event: CdkDragDrop)
Parameters :
Name Type Optional
event CdkDragDrop<any[]> No
Returns : void
Protected isSelected
isSelected(option: SelectableOption<O | V>)

Whether the given option is part of the current selection. Reads the _selectedOptions signal, so that options rendered in the open dropdown are updated as soon as the selection changes.

Parameters :
Name Type Optional
option SelectableOption<O | V> No
Returns : boolean
onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
Protected onDropdownClosed
onDropdownClosed()

The dropdown closed, which ends the selection unless the field still has the focus (e.g. after closing the dropdown with the Escape key).

Returns : void
onFocusOut
onFocusOut(event: FocusEvent)
Parameters :
Name Type Optional
event FocusEvent No
Returns : void
Protected onOptionSelectionChange
onOptionSelectionChange(event: MatOptionSelectionChange)

Apply a selection the user made while the dropdown is kept open for a multi-select field.

Material only emits its own optionSelected output when the panel closes, which is suppressed for multi-select (see KeepPanelOpenOnSelectDirective).

Parameters :
Name Type Optional
event MatOptionSelectionChange No
Returns : void
select
select(selected: string | SelectableOption | CreateOptionMarker)
Parameters :
Name Type Optional
selected string | SelectableOption<O | V> | CreateOptionMarker<O> No
Returns : void
showAutocomplete
showAutocomplete(valueToRevertTo?: string)
Parameters :
Name Type Optional
valueToRevertTo string Yes
Returns : void
Protected toCreateOptionValue
toCreateOptionValue(option: CreateOptionConfig<O>, input: string)
Parameters :
Name Type Optional
option CreateOptionConfig<O> No
input string No
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

Protected optionsSource
Type : unknown
Default value : computed(() => this.availableEntityTypes(), )
_selectedOptions
Type : unknown
Default value : signal<SelectableOption<O, V>[]>([])
autocomplete
Type : unknown
Default value : viewChild(MatAutocompleteTrigger)
autocompleteFilterFunction
Type : function
autocompleteForm
Type : unknown
Default value : new FormControl("")
autocompleteOptions
Type : Signal<SelectableOption[]>
Default value : toSignal( this.autocompleteSuggestedOptions, { initialValue: [] as SelectableOption<O, V>[] }, )
autocompleteSuggestedOptions
Type : unknown
Default value : this.autocompleteForm.valueChanges.pipe( filter((val) => typeof val === "string"), map((val) => this.updateAutocomplete(val)), startWith([] as SelectableOption<O, V>[]), )
Protected availableCreateOptions
Type : unknown
Default value : computed<CreateOptionConfig<O>[]>(() => { if (this.createOptions().length > 0) { return this.createOptions(); } if (!this.createOption()) { return []; } return [{ label: "", create: this.createOption() }]; })
effectiveDisplay
Type : unknown
Default value : computed<"text" | "chips" | "none">(() => this.multi() && this.reorder() && this.display() === "text" ? "chips" : this.display(), )

Derived display mode. Reorderable multi-select uses chips without mutating the public input.

hasMoreOptions
Type : unknown
Default value : signal(false)
inputElement
Type : unknown
Default value : viewChild("inputElement", { read: MatInput }) as Signal< (MatInput & { _elementRef: ElementRef<HTMLElement> }) | undefined >
isInSearchMode
Type : WritableSignal<boolean>
Default value : signal(false)

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

panelWidth
Type : unknown
Default value : signal("200px")

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 : signal(false)

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

templateRef
Type : unknown
Default value : contentChild(TemplateRef)
trackByOptionValueFn
Type : TrackByFunction<SelectableOption<O, V>> | undefined
Default value : () => {...}
viewportHeight
Type : unknown
Default value : computed(() => { const contentHeight = (this.autocompleteOptions()?.length ?? 0) * 48; const availableHeight = BasicAutocompleteComponent.PANEL_MAX_HEIGHT - BasicAutocompleteComponent.FOOTER_RESERVE; return Math.min(contentHeight, availableHeight); })
virtualScrollViewport
Type : unknown
Default value : viewChild(CdkVirtualScrollViewport)
controlType
Type : string
Default value : "custom-control"
elementRef
Type : unknown
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
Readonly enabled
Type : Signal<boolean>
Default value : computed(() => !this._disabled())

Whether the control is currently enabled, as a signal (tracks disabled).

errorStateMatcher
Type : unknown
Default value : inject(ErrorStateMatcher)
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>()
Readonly valueSignal
Type : Signal<T>
Default value : computed(() => this._value())

The current value of the control as a signal. Authoritative in both modes: it reflects the bound FormControl (synced in ngDoCheck) as well as [(value)] / writeValue updates.

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  Input,
  input,
} from "@angular/core";
import {
  BASIC_AUTOCOMPLETE_COMPONENT_IMPORTS,
  BasicAutocompleteComponent,
} from "../../common-components/basic-autocomplete/basic-autocomplete.component";
import { EntityConstructor } from "../model/entity";
import { EntityRegistry } from "../database-entity.decorator";
import { MatFormFieldControl } from "@angular/material/form-field";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-entity-type-select",
  templateUrl:
    "../../common-components/basic-autocomplete/basic-autocomplete.component.html",
  imports: BASIC_AUTOCOMPLETE_COMPONENT_IMPORTS,
  providers: [
    { provide: MatFormFieldControl, useExisting: EntityTypeSelectComponent },
  ],
})
export class EntityTypeSelectComponent extends BasicAutocompleteComponent<
  EntityConstructor,
  string
> {
  // `placeholder` is part of the `MatFormFieldControl` contract (Angular Material
  // reads it as a plain property), so it stays a decorator `@Input()` rather than a
  // signal input; here we only override the default value.
  @Input() override placeholder =
    $localize`:EntityTypeSelect placeholder:Select Record Type`;

  /**
   * whether to include record types without a human-readable label
   * (usually only used internally for technical purposes)
   */
  showInternalTypes = input(false);

  /**
   * ENTITY_TYPE keys to omit from the dropdown, e.g. types that are already
   * selected elsewhere and should not be offered again.
   */
  hiddenTypes = input<string[]>([]);

  private readonly entityRegistry = inject(EntityRegistry);
  private readonly availableEntityTypes = computed(() =>
    this.entityRegistry
      .getEntityTypes(!this.showInternalTypes())
      .map(({ value }) => value)
      .filter((type) => !this.hiddenTypes().includes(type.ENTITY_TYPE)),
  );

  protected override optionsSource = computed(() =>
    this.availableEntityTypes(),
  );

  override optionToString = input<(option: EntityConstructor) => string>(
    (option) => option.label ?? option.ENTITY_TYPE,
  );
  override valueMapper = input<(option: EntityConstructor) => string>(
    (option) => option.ENTITY_TYPE,
  );
}
<div matAutocompleteOrigin #autocompleteOrigin="matAutocompleteOrigin">
  <!--Display-->
  @if (effectiveDisplay() === "text" || effectiveDisplay() === "none") {
    <input
      [id]="id"
      [hidden]="isInSearchMode() || effectiveDisplay() === 'none'"
      [disabled]="!enabled()"
      matInput
      style="text-overflow: ellipsis; width: calc(100% - 50px)"
      (focusin)="showAutocomplete()"
      (focusout)="showAutocomplete()"
      [value]="displayText"
      [placeholder]="placeholder"
    />
  } @else {
    <input
      [hidden]="true"
      [disabled]="!enabled()"
      matInput
      (focusin)="showAutocomplete()"
      (focusout)="showAutocomplete()"
      [matChipInputFor]="chipList"
    />
    <mat-chip-grid
      #chipList
      cdkDropList
      cdkDropListOrientation="mixed"
      [cdkDropListDisabled]="!reorder()"
      (cdkDropListDropped)="dropChips($event)"
      class="chip-drop-list"
    >
      @for (item of _selectedOptions(); track item.asValue) {
        <mat-chip-row
          cdkDrag
          [cdkDragDisabled]="!reorder()"
          [editable]="enabled()"
          class="chip"
          [style.background-color]="item.asValue?.['color']"
        >
          <ng-template
            [ngTemplateOutlet]="chipContent"
            [ngTemplateOutletContext]="{ $implicit: item }"
          ></ng-template>

          @if (enabled()) {
            <button matChipRemove (click)="unselect(item)">
              <app-fa-dynamic-icon
                i18n-matTooltip="
                  tooltip for remove icon on chips of dropdown item
                "
                matTooltip="Remove"
                icon="xmark"
              ></app-fa-dynamic-icon>
            </button>
          }

          <ng-template cdkDragPreview>
            <mat-chip-row
              [editable]="false"
              class="chip chip-preview"
              [style.background-color]="item.asValue?.['color']"
            >
              <ng-template
                [ngTemplateOutlet]="chipContent"
                [ngTemplateOutletContext]="{ $implicit: item }"
              ></ng-template>
            </mat-chip-row>
          </ng-template>
        </mat-chip-row>
      }
    </mat-chip-grid>
  }

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

<!--
Autocomplete
-->
<mat-autocomplete
  [disableRipple]="true"
  #autoSuggestions="matAutocomplete"
  (optionSelected)="select($event.option.value)"
  (closed)="onDropdownClosed()"
  autoActiveFirstOption
  [hideSingleSelectionIndicator]="true"
  [panelWidth]="panelWidth()"
>
  <!-- Select All and Clear Buttons on top of options via content projection-->
  <div>
    <ng-content select="[autocompleteHeader]"></ng-content>
  </div>
  <div>
    @if (!displayFullLengthOptionLabel()) {
      <cdk-virtual-scroll-viewport
        [style.height.px]="viewportHeight()"
        [itemSize]="48"
        minBufferPx="200"
        maxBufferPx="500"
      >
        <mat-option
          [value]="item"
          (onSelectionChange)="onOptionSelectionChange($event)"
          *cdkVirtualFor="
            let item of autocompleteOptions();
            trackBy: trackByOptionValueFn
          "
        >
          <ng-container
            [ngTemplateOutlet]="optionContent"
            [ngTemplateOutletContext]="{ $implicit: item, truncate: true }"
          ></ng-container>
        </mat-option>
      </cdk-virtual-scroll-viewport>
    } @else {
      @for (item of autocompleteOptions(); track item.asValue) {
        <mat-option
          [value]="item"
          class="option-with-wrap"
          (onSelectionChange)="onOptionSelectionChange($event)"
        >
          <ng-container
            [ngTemplateOutlet]="optionContent"
            [ngTemplateOutletContext]="{ $implicit: item, truncate: false }"
          ></ng-container>
        </mat-option>
      }
    }
  </div>

  <ng-template #chipContent let-item>
    @if (reorder()) {
      <span class="drag-handle color-accent" cdkDragHandle>
        <app-fa-dynamic-icon icon="grip-vertical"></app-fa-dynamic-icon>
      </span>
    }

    @if (!templateRef()) {
      {{ item.asString }}
    } @else {
      <ng-template
        [ngTemplateOutlet]="templateRef()"
        [ngTemplateOutletContext]="{ $implicit: item.initial }"
      ></ng-template>
    }
  </ng-template>

  <ng-template #optionContent let-item let-truncate="truncate">
    <div
      class="option-content flex-row disable-autocomplete-active-color align-center"
    >
      @if (multi()) {
        <mat-checkbox [checked]="isSelected(item)"></mat-checkbox>
      }
      @if (!templateRef()) {
        <span
          class="option-label"
          [class.text-truncate]="truncate"
          [class.text-wrap]="!truncate"
          [matTooltip]="item.asString"
          matTooltipPosition="above"
          [class.not-defined-label]="item.isEmpty"
          [class.invalid-label]="item.isInvalid"
        >
          {{ item.asString }}
        </span>
      } @else {
        <div class="option-label item-option">
          <ng-template
            [ngTemplateOutlet]="templateRef()"
            [ngTemplateOutletContext]="{ $implicit: item.initial }"
          ></ng-template>
        </div>
      }
    </div>
  </ng-template>

  <!-- Create new option -->
  @if (showAddOption() && inputElement.value) {
    @for (option of availableCreateOptions(); track $index) {
      <mat-option
        [value]="toCreateOptionValue(option, inputElement.value)"
        [attr.aria-label]="createOptionAriaLabel(option, inputElement.value)"
        (onSelectionChange)="onOptionSelectionChange($event)"
      >
        <em
          i18n="
            Label for adding an option in a dropdown|e.g. Add new My new Option
          "
          >Add new</em
        >
        {{ createOptionLabel(option, 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">
    @if (hasMoreOptions()) {
      <em
        class="more-options-hint"
        i18n="Hint shown when dropdown has more options than displayed"
        >Type to filter more results ...</em
      >
    }
    <ng-content select="[autocompleteFooter]"></ng-content>
  </div>
</mat-autocomplete>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""