src/app/core/common-components/basic-autocomplete/basic-autocomplete.component.ts
Custom MatFormFieldControl for any select / dropdown field.
OnChanges
OnInit
AfterViewInit
| providers |
BasicAutocompleteComponent
|
| selector | app-basic-autocomplete |
| imports |
BASIC_AUTOCOMPLETE_COMPONENT_IMPORTS
|
| styleUrls | ./basic-autocomplete.component.scss |
| templateUrl | basic-autocomplete.component.html |
| 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. |
|
| 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
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:47
|
|
| disabled | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:79
|
|
| ngControl | |
Type : any
|
|
Default value : inject(NgControl, { optional: true, self: true })
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:40
|
|
| placeholder | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:48
|
|
| required | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:51
|
|
| value | |
Type : T
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:90
|
|
| autocompleteFilterChange | |
Type : EventEmitter
|
|
| valueChange | |
Type : EventEmitter
|
|
|
Inherited from
CustomFormControlDirective
|
|
|
Defined in
CustomFormControlDirective:100
|
|
| compareEnumValues |
compareEnumValues(a: any, b: any)
|
|
Compare two enum values by id if present, otherwise by reference.
Returns :
boolean
|
| Async createNewOption | ||||||
createNewOption(option: string)
|
||||||
|
Parameters :
Returns :
any
|
| drop | ||||||
drop(event: CdkDragDrop
|
||||||
|
Parameters :
Returns :
void
|
| onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Defined in
CustomFormControlDirective:466
|
||||||
|
Parameters :
Returns :
void
|
| onFocusOut | ||||||
onFocusOut(event: FocusEvent)
|
||||||
|
Parameters :
Returns :
void
|
| select | ||||||
select(selected: string | SelectableOption<O | V>)
|
||||||
|
Parameters :
Returns :
void
|
| showAutocomplete | ||||||
showAutocomplete(valueToRevertTo?: string)
|
||||||
|
Parameters :
Returns :
void
|
| unselect | ||||||
unselect(option: SelectableOption<O | V>)
|
||||||
|
Parameters :
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)
|
||||||||||||
|
Inherited from
CustomFormControlDirective
|
||||||||||||
|
Defined in
CustomFormControlDirective:475
|
||||||||||||
|
Parameters :
Returns :
void
|
| blur |
blur()
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:125
|
|
Returns :
void
|
| focus |
focus()
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:120
|
|
Returns :
void
|
| registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Defined in
CustomFormControlDirective:158
|
||||||
|
Parameters :
Returns :
void
|
| registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Defined in
CustomFormControlDirective:162
|
||||||
|
Parameters :
Returns :
void
|
| setDescribedByIds | ||||||
setDescribedByIds(ids: string[])
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Defined in
CustomFormControlDirective:132
|
||||||
|
Parameters :
Returns :
void
|
| setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Defined in
CustomFormControlDirective:166
|
||||||
|
Parameters :
Returns :
void
|
| _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 : []
|
| 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
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:88
|
| _value |
Type : T
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:98
|
| controlType |
Type : string
|
Default value : "custom-control"
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:66
|
| elementRef |
Type : unknown
|
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:38
|
| errorState |
Type : unknown
|
Default value : false
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:65
|
| errorStateMatcher |
Type : unknown
|
Default value : inject(ErrorStateMatcher)
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:39
|
| focused |
Type : unknown
|
Default value : false
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:63
|
| id |
Type : unknown
|
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:45
|
| Static nextId |
Type : number
|
Default value : 0
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:44
|
| onChange |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:67
|
| onTouched |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:68
|
| parentForm |
Type : unknown
|
Default value : inject(NgForm, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:41
|
| parentFormGroup |
Type : unknown
|
Default value : inject(FormGroupDirective, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:42
|
| stateChanges |
Type : unknown
|
Default value : new Subject<void>()
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:62
|
| touched |
Type : unknown
|
Default value : false
|
|
Inherited from
CustomFormControlDirective
|
|
Defined in
CustomFormControlDirective:64
|
| displayText |
getdisplayText()
|
| disabled | ||||||
getdisabled()
|
||||||
setdisabled(value: boolean)
|
||||||
|
Parameters :
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
Parameters :
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;
}