igniteui-angular
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
1,412 lines (1,407 loc) • 109 kB
JavaScript
import * as i3 from 'igniteui-angular/input-group';
import { IGX_INPUT_GROUP_TYPE, IgxInputState, IgxInputGroupComponent, IgxInputDirective, IgxSuffixDirective, IgxPrefixDirective, IgxLabelDirective, IgxReadOnlyInputDirective, IgxHintDirective } from 'igniteui-angular/input-group';
import { NgTemplateOutlet, NgClass } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, inject, booleanAttribute, Input, HostBinding, Component, Directive, InjectionToken, ElementRef, ChangeDetectorRef, DOCUMENT, Injector, EventEmitter, QueryList, TemplateRef, ViewChildren, ContentChildren, ViewChild, forwardRef, ContentChild, Output, Pipe, HostListener, NgModule } from '@angular/core';
import * as i1 from '@angular/forms';
import { NgControl, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { IgxForOfDirective, IgxToggleDirective, IgxButtonDirective, IgxRippleDirective } from 'igniteui-angular/directives';
import { rem, IgxSelectionAPIService, getCurrentResourceStrings, ComboResourceStringsEN, SortingDirection, AutoPositionStrategy, AbsoluteScrollStrategy, cloneArray } from 'igniteui-angular/core';
import { IgxCheckboxComponent } from 'igniteui-angular/checkbox';
import { IgxDropDownItemComponent, Navigate, IgxDropDownComponent, DropDownActionKey, IGX_DROPDOWN_BASE, IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
import { caseSensitive } from '@igniteui/material-icons-extended';
import { Subject, noop } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { IgxIconService, IgxIconComponent } from 'igniteui-angular/icon';
import { isEqual } from 'lodash-es';
/**
* @hidden
*/
class IgxComboAPIService {
constructor() {
this.disableTransitions = false;
}
get valueKey() {
return this.combo.valueKey !== null && this.combo.valueKey !== undefined ? this.combo.valueKey : null;
}
get item_focusable() {
return false;
}
get isRemote() {
return this.combo.isRemote;
}
get comboID() {
return this.combo.id;
}
register(combo) {
this.combo = combo;
}
clear() {
this.combo = null;
}
add_custom_item() {
if (!this.combo) {
return;
}
this.combo.addItemToCollection();
}
set_selected_item(itemID, event) {
const selected = this.combo.isItemSelected(itemID);
if (itemID === undefined) {
return;
}
if (!selected) {
this.combo.select([itemID], false, event);
}
else {
this.combo.deselect([itemID], event);
}
}
is_item_selected(itemID) {
return this.combo.isItemSelected(itemID);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboAPIService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboAPIService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboAPIService, decorators: [{
type: Injectable
}] });
/** @hidden */
class IgxComboItemComponent extends IgxDropDownItemComponent {
constructor() {
super(...arguments);
this.comboAPI = inject(IgxComboAPIService);
/**
* Gets the height of a list item
*
* @hidden
*/
this.itemHeight = '';
}
/** @hidden @internal */
get _itemHeightToRem() {
if (this.itemHeight) {
return rem(this.itemHeight);
}
}
get ariaLabel() {
const valueKey = this.comboAPI.valueKey;
return (valueKey !== null && this.value != null) ? this.value[valueKey] : this.value;
}
/**
* @hidden
*/
get itemID() {
const valueKey = this.comboAPI.valueKey;
return valueKey !== null ? this.value[valueKey] : this.value;
}
/**
* @hidden
*/
get comboID() {
return this.comboAPI.comboID;
}
/**
* @hidden
* @internal
*/
get disableTransitions() {
return this.comboAPI.disableTransitions;
}
/**
* @hidden
*/
get selected() {
return this.comboAPI.is_item_selected(this.itemID);
}
set selected(value) {
if (this.isHeader) {
return;
}
this._selected = value;
}
/**
* @hidden
*/
isVisible(direction) {
const rect = this.element.nativeElement.getBoundingClientRect();
const parentDiv = this.element.nativeElement.parentElement.parentElement.getBoundingClientRect();
if (direction === Navigate.Down) {
return rect.y + rect.height <= parentDiv.y + parentDiv.height;
}
return rect.y >= parentDiv.y;
}
clicked(event) {
this.comboAPI.disableTransitions = false;
if (!this.isSelectable) {
return;
}
this.dropDown.navigateItem(this.index);
this.comboAPI.set_selected_item(this.itemID, event);
}
/**
* @hidden
* @internal
* The event that is prevented is the click on the checkbox label element.
* That is the only visible element that a user can interact with.
* The click propagates to the host and the preventDefault is to stop it from
* switching focus to the input it's base on.
* The toggle happens in an internal handler in the drop-down on the next task queue cycle.
*/
disableCheck(event) {
event.preventDefault();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboItemComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.2", type: IgxComboItemComponent, isStandalone: true, selector: "igx-combo-item", inputs: { itemHeight: "itemHeight", ariaLabel: "ariaLabel", singleMode: ["singleMode", "singleMode", booleanAttribute] }, host: { properties: { "style.height.rem": "this._itemHeightToRem", "attr.aria-label": "this.ariaLabel" } }, usesInheritance: true, ngImport: i0, template: "@if (!isHeader && !singleMode) {\n <!-- checkbox should not allow changing its state from UI click (that's why it should be readonly=true), becasue when cancelling the selectionChange event in the combo, then checkbox will still change state.-->\n <igx-checkbox [checked]=\"selected\" [readonly]=\"true\" [disableRipple]=\"true\" [disableTransitions]=\"disableTransitions\" [tabindex]=\"-1\" (click)=\"disableCheck($event)\" class=\"igx-combo__checkbox\"></igx-checkbox>\n}\n<span class=\"igx-drop-down__inner\"><ng-content></ng-content></span>\n", dependencies: [{ kind: "component", type: IgxCheckboxComponent, selector: "igx-checkbox", inputs: ["indeterminate", "checked", "disabled", "invalid", "readonly", "disableTransitions"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboItemComponent, decorators: [{
type: Component,
args: [{ selector: 'igx-combo-item', imports: [IgxCheckboxComponent], template: "@if (!isHeader && !singleMode) {\n <!-- checkbox should not allow changing its state from UI click (that's why it should be readonly=true), becasue when cancelling the selectionChange event in the combo, then checkbox will still change state.-->\n <igx-checkbox [checked]=\"selected\" [readonly]=\"true\" [disableRipple]=\"true\" [disableTransitions]=\"disableTransitions\" [tabindex]=\"-1\" (click)=\"disableCheck($event)\" class=\"igx-combo__checkbox\"></igx-checkbox>\n}\n<span class=\"igx-drop-down__inner\"><ng-content></ng-content></span>\n" }]
}], propDecorators: { itemHeight: [{
type: Input
}], _itemHeightToRem: [{
type: HostBinding,
args: ['style.height.rem']
}], ariaLabel: [{
type: HostBinding,
args: ['attr.aria-label']
}, {
type: Input
}], singleMode: [{
type: Input,
args: [{ transform: booleanAttribute }]
}] } });
/**
* Allows a custom element to be added at the beginning of the combo list.
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo>
* <ng-template igxComboHeader>
* <div class="header-class">Custom header</div>
* <img src=""/>
* </ng-template>
* </igx-combo>
*/
class IgxComboHeaderDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboHeaderDirective, isStandalone: true, selector: "[igxComboHeader]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboHeaderDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboHeader]',
standalone: true
}]
}] });
/**
* Allows a custom element to be added at the end of the combo list.
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo>
* <ng-template igxComboFooter>
* <div class="footer-class">Custom footer</div>
* <img src=""/>
* </ng-template>
* </igx-combo>
*/
class IgxComboFooterDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboFooterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboFooterDirective, isStandalone: true, selector: "[igxComboFooter]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboFooterDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboFooter]',
standalone: true
}]
}] });
/**
* Allows the combo's items to be modified with a custom template
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo>
* <ng-template igxComboItem let-display let-key="valueKey">
* <div class="item">
* <span class="state">State: {{ display[key] }}</span>
* <span class="region">Region: {{ display.region }}</span>
* </div>
* </ng-template>
* </igx-combo>
*/
class IgxComboItemDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboItemDirective, isStandalone: true, selector: "[igxComboItem]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboItem]',
standalone: true
}]
}] });
/**
* Defines the custom template that will be displayed when the combo's list is empty
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo>
* <ng-template igxComboEmpty>
* <div class="combo--empty">
* There are no items to display
* </div>
* </ng-template>
* </igx-combo>
*/
class IgxComboEmptyDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboEmptyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboEmptyDirective, isStandalone: true, selector: "[igxComboEmpty]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboEmptyDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboEmpty]',
standalone: true
}]
}] });
/**
* Defines the custom template that will be used when rendering header items for groups in the combo's list
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo>
* <ng-template igxComboHeaderItem let-item let-key="groupKey">
* <div class="custom-item--group">Group header for {{ item[key] }}</div>
* </ng-template>
* </igx-combo>
*/
class IgxComboHeaderItemDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboHeaderItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboHeaderItemDirective, isStandalone: true, selector: "[igxComboHeaderItem]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboHeaderItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboHeaderItem]',
standalone: true
}]
}] });
/**
* Defines the custom template that will be used to display the `ADD` button
*
* @remarks To show the `ADD` button, the `allowCustomValues` option must be enabled
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo #combo>
* <ng-template igxComboAddItem>
* <button type="button" class="combo__add-button">
* Click to add item
* </button>
* </ng-template>
* </igx-combo>
*/
class IgxComboAddItemDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboAddItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboAddItemDirective, isStandalone: true, selector: "[igxComboAddItem]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboAddItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboAddItem]',
standalone: true
}]
}] });
/**
* The custom template that will be used when rendering the combo's toggle button
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo #combo>
* <ng-template igxComboToggleIcon let-collapsed>
* <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
* </ng-template>
* </igx-combo>
*/
class IgxComboToggleIconDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboToggleIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboToggleIconDirective, isStandalone: true, selector: "[igxComboToggleIcon]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboToggleIconDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboToggleIcon]',
standalone: true
}]
}] });
/**
* Defines the custom template that will be used when rendering the combo's clear icon
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @example
* <igx-combo #combo>
* <ng-template igxComboClearIcon>
* <igx-icon>clear</igx-icon>
* </ng-template>
* </igx-combo>
*/
class IgxComboClearIconDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboClearIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxComboClearIconDirective, isStandalone: true, selector: "[igxComboClearIcon]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxComboClearIconDirective, decorators: [{
type: Directive,
args: [{
selector: '[igxComboClearIcon]',
standalone: true
}]
}] });
const IGX_COMBO_COMPONENT = /*@__PURE__*/ new InjectionToken('IgxComboComponentToken');
let NEXT_ID = 0;
class IgxComboBaseDirective {
constructor() {
this.elementRef = inject(ElementRef);
this.cdr = inject(ChangeDetectorRef);
this.selectionService = inject(IgxSelectionAPIService);
this.comboAPI = inject(IgxComboAPIService);
this.document = inject(DOCUMENT);
this._inputGroupType = inject(IGX_INPUT_GROUP_TYPE, { optional: true });
this._injector = inject(Injector, { optional: true });
this._iconService = inject(IgxIconService, { optional: true });
/**
* Defines whether the caseSensitive icon should be shown in the search input
*
* ```typescript
* // get
* let myComboShowSearchCaseIcon = this.combo.showSearchCaseIcon;
* ```
*
* ```html
* <!--set-->
* <igx-combo [showSearchCaseIcon]='true'></igx-combo>
* ```
*/
this.showSearchCaseIcon = false;
/**
* Set custom overlay settings that control how the combo's list of items is displayed.
* Set:
* ```html
* <igx-combo [overlaySettings]="customOverlaySettings"></igx-combo>
* ```
*
* ```typescript
* const customSettings = { positionStrategy: { settings: { target: myTarget } } };
* combo.overlaySettings = customSettings;
* ```
* Get any custom overlay settings used by the combo:
* ```typescript
* const comboOverlaySettings: OverlaySettings = myCombo.overlaySettings;
* ```
*/
this.overlaySettings = null;
/**
* Controls whether custom values can be added to the collection
*
* ```typescript
* // get
* let comboAllowsCustomValues = this.combo.allowCustomValues;
* ```
*
* ```html
* <!--set-->
* <igx-combo [allowCustomValues]='true'></igx-combo>
* ```
*/
this.allowCustomValues = false;
/**
* Determines which column in the data source is used to determine the value.
*
* ```typescript
* // get
* let myComboValueKey = this.combo.valueKey;
* ```
*
* ```html
* <!--set-->
* <igx-combo [valueKey]='myKey'></igx-combo>
* ```
*/
this.valueKey = null;
/** @hidden @internal */
this.cssClass = 'igx-combo'; // Independent of display density for the time being
/**
* Disables the combo. The default is `false`.
* ```html
* <igx-combo [disabled]="'true'">
* ```
*/
this.disabled = false;
/**
* Emitted before the dropdown is opened
*
* ```html
* <igx-combo opening='handleOpening($event)'></igx-combo>
* ```
*/
this.opening = new EventEmitter();
/**
* Emitted after the dropdown is opened
*
* ```html
* <igx-combo (opened)='handleOpened($event)'></igx-combo>
* ```
*/
this.opened = new EventEmitter();
/**
* Emitted before the dropdown is closed
*
* ```html
* <igx-combo (closing)='handleClosing($event)'></igx-combo>
* ```
*/
this.closing = new EventEmitter();
/**
* Emitted after the dropdown is closed
*
* ```html
* <igx-combo (closed)='handleClosed($event)'></igx-combo>
* ```
*/
this.closed = new EventEmitter();
/**
* Emitted when an item is being added to the data collection
*
* ```html
* <igx-combo (addition)='handleAdditionEvent($event)'></igx-combo>
* ```
*/
this.addition = new EventEmitter();
/**
* Emitted when the value of the search input changes (e.g. typing, pasting, clear, etc.)
*
* ```html
* <igx-combo (searchInputUpdate)='handleSearchInputEvent($event)'></igx-combo>
* ```
*/
this.searchInputUpdate = new EventEmitter();
/**
* Emitted when new chunk of data is loaded from the virtualization
*
* ```html
* <igx-combo (dataPreLoad)='handleDataPreloadEvent($event)'></igx-combo>
* ```
*/
this.dataPreLoad = new EventEmitter();
/**
* The custom template, if any, that should be used when rendering ITEMS in the combo list
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.itemTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboItem let-item let-key="valueKey">
* <div class="custom-item">
* <div class="custom-item__name">{{ item[key] }}</div>
* <div class="custom-item__cost">{{ item.cost }}</div>
* </div>
* </ng-template>
* </igx-combo>
* ```
*/
this.itemTemplate = null;
/**
* The custom template, if any, that should be used when rendering the HEADER for the combo items list
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.headerTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboHeader>
* <div class="combo__header">
* This is a custom header
* </div>
* </ng-template>
* </igx-combo>
* ```
*/
this.headerTemplate = null;
/**
* The custom template, if any, that should be used when rendering the FOOTER for the combo items list
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.footerTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboFooter>
* <div class="combo__footer">
* This is a custom footer
* </div>
* </ng-template>
* </igx-combo>
* ```
*/
this.footerTemplate = null;
/**
* The custom template, if any, that should be used when rendering HEADER ITEMS for groups in the combo list
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.headerItemTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboHeaderItem let-item let-key="groupKey">
* <div class="custom-item--group">Group header for {{ item[key] }}</div>
* </ng-template>
* </igx-combo>
* ```
*/
this.headerItemTemplate = null;
/**
* The custom template, if any, that should be used when rendering the ADD BUTTON in the combo drop down
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.addItemTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboAddItem>
* <button type="button" igxButton="contained" class="combo__add-button">
* Click to add item
* </button>
* </ng-template>
* </igx-combo>
* ```
*/
this.addItemTemplate = null;
/**
* The custom template, if any, that should be used when rendering the ADD BUTTON in the combo drop down
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.emptyTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboEmpty>
* <div class="combo--empty">
* There are no items to display
* </div>
* </ng-template>
* </igx-combo>
* ```
*/
this.emptyTemplate = null;
/**
* The custom template, if any, that should be used when rendering the combo TOGGLE(open/close) button
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.toggleIconTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboToggleIcon let-collapsed>
* <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
* </ng-template>
* </igx-combo>
* ```
*/
this.toggleIconTemplate = null;
/**
* The custom template, if any, that should be used when rendering the combo CLEAR button
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.combo.clearIconTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-combo #combo>
* ...
* <ng-template igxComboClearIcon>
* <igx-icon>clear</igx-icon>
* </ng-template>
* </igx-combo>
* ```
*/
this.clearIconTemplate = null;
/** @hidden @internal */
this.searchInput = null;
this.dropdownContainer = null;
/** @hidden @internal */
this.customValueFlag = true;
/** @hidden @internal */
this.filterValue = '';
/** @hidden @internal */
this.defaultFallbackGroup = 'Other';
/** @hidden @internal */
this.activeDescendant = '';
this.containerSize = undefined;
this.itemSize = undefined;
this._data = [];
this._value = [];
this._displayValue = '';
this._groupKey = '';
this._searchValue = '';
this._filteredData = [];
this._remoteSelection = {};
this._resourceStrings = getCurrentResourceStrings(ComboResourceStringsEN);
this._valid = IgxInputState.INITIAL;
this.ngControl = null;
this.destroy$ = new Subject();
this._onTouchedCallback = noop;
this._onChangeCallback = noop;
this.compareCollator = new Intl.Collator();
this._id = `igx-combo-${NEXT_ID++}`;
this._disableFiltering = false;
this._type = null;
this._dataType = '';
this._itemHeight = undefined;
this._itemsMaxHeight = null;
this._groupSortingDirection = SortingDirection.Asc;
this._defaultFilteringOptions = { caseSensitive: false };
this.itemsInContainer = 10;
this.onStatusChanged = () => {
if (this.ngControl && this.isTouchedOrDirty && !this.disabled) {
if (this.hasValidators && (!this.collapsed || this.inputGroup.isFocused)) {
this.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
}
else {
this.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
}
else {
// B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
this.valid = IgxInputState.INITIAL;
}
this.manageRequiredAsterisk();
};
this.findMatch = (element) => {
const value = this.displayKey ? element[this.displayKey] : element;
const searchValue = this.searchValue || this.comboInput?.value;
return value?.toString().trim().toLowerCase() === searchValue.trim().toLowerCase();
};
}
/**
* Enables/disables filtering in the list. The default is `false`.
*/
get disableFiltering() {
return this._disableFiltering;
}
set disableFiltering(value) {
this._disableFiltering = value;
}
/**
* Gets/gets combo id.
*
* ```typescript
* // get
* let id = this.combo.id;
* ```
*
* ```html
* <!--set-->
* <igx-combo [id]='combo1'></igx-combo>
* ```
*/
get id() {
return this._id;
}
set id(value) {
if (!value) {
return;
}
const selection = this.selectionService.get(this._id);
this.selectionService.clear(this._id);
this._id = value;
if (selection) {
this.selectionService.set(this._id, selection);
}
}
/**
* Configures the drop down list height
*
* ```typescript
* // get
* let myComboItemsMaxHeight = this.combo.itemsMaxHeight;
* ```
*
* ```html
* <!--set-->
* <igx-combo [itemsMaxHeight]='320'></igx-combo>
* ```
*/
get itemsMaxHeight() {
if (this.itemHeight && !this._itemsMaxHeight) {
return this.itemHeight * this.itemsInContainer;
}
return this._itemsMaxHeight;
}
set itemsMaxHeight(val) {
this._itemsMaxHeight = val;
}
/** @hidden */
get itemsMaxHeightInRem() {
if (this.itemsMaxHeight) {
return rem(this.itemsMaxHeight);
}
}
/**
* Configures the drop down list item height
*
* ```typescript
* // get
* let myComboItemHeight = this.combo.itemHeight;
* ```
*
* ```html
* <!--set-->
* <igx-combo [itemHeight]='32'></igx-combo>
* ```
*/
get itemHeight() {
return this._itemHeight;
}
set itemHeight(val) {
this._itemHeight = val;
}
/**
* Combo data source.
*
* ```html
* <!--set-->
* <igx-combo [data]='items'></igx-combo>
* ```
*/
get data() {
return this._data;
}
set data(val) {
// igxFor directive ignores undefined values
// if the combo uses simple data and filtering is applied
// an error will occur due to the mismatch of the length of the data
// this can occur during filtering for the igx-combo and
// during filtering & selection for the igx-simple-combo
// since the simple combo's input is both a container for the selection and a filter
this._data = (val) ? val.filter(x => x !== undefined) : [];
}
set displayKey(val) {
this._displayKey = val;
}
/**
* Determines which column in the data source is used to determine the display value.
*
* ```typescript
* // get
* let myComboDisplayKey = this.combo.displayKey;
*
* // set
* this.combo.displayKey = 'val';
*
* ```
*
* ```html
* <!--set-->
* <igx-combo [displayKey]='myDisplayKey'></igx-combo>
* ```
*/
get displayKey() {
return this._displayKey ? this._displayKey : this.valueKey;
}
/**
* The item property by which items should be grouped inside the items list. Not usable if data is not of type Object[].
*
* ```html
* <!--set-->
* <igx-combo [groupKey]='newGroupKey'></igx-combo>
* ```
*/
set groupKey(val) {
this._groupKey = val;
}
/**
* The item property by which items should be grouped inside the items list. Not usable if data is not of type Object[].
*
* ```typescript
* // get
* let currentGroupKey = this.combo.groupKey;
* ```
*/
get groupKey() {
return this._groupKey;
}
/**
* Sets groups sorting order.
*
* @example
* ```html
* <igx-combo [groupSortingDirection]="groupSortingDirection"></igx-combo>
* ```
* ```typescript
* public groupSortingDirection = SortingDirection.Asc;
* ```
*/
get groupSortingDirection() {
return this._groupSortingDirection;
}
set groupSortingDirection(val) {
this._groupSortingDirection = val;
}
/**
* Sets the visual combo type.
* The allowed values are `line`, `box`, `border` and `search`. The default is `box`.
* ```html
* <igx-combo [type]="'line'">
* ```
*/
get type() {
return this._type || this._inputGroupType || 'box';
}
set type(val) {
this._type = val;
}
/**
* Gets/Sets the resource strings.
*
* @remarks
* By default it uses EN resources.
*/
get resourceStrings() {
return this._resourceStrings;
}
set resourceStrings(value) {
this._resourceStrings = Object.assign({}, this._resourceStrings, value);
}
/** @hidden @internal */
get searchValue() {
return this._searchValue;
}
set searchValue(val) {
this.filterValue = val;
this._searchValue = val;
}
/** @hidden @internal */
get isRemote() {
return this.totalItemCount > 0 &&
this.valueKey &&
this.dataType === "complex" /* DataTypes.COMPLEX */;
}
/** @hidden @internal */
get dataType() {
if (this.displayKey) {
return "complex" /* DataTypes.COMPLEX */;
}
return "primitive" /* DataTypes.PRIMITIVE */;
}
/**
* Gets if control is valid, when used in a form
*
* ```typescript
* // get
* let valid = this.combo.valid;
* ```
*/
get valid() {
return this._valid;
}
/**
* Sets if control is valid, when used in a form
*
* ```typescript
* // set
* this.combo.valid = IgxInputState.INVALID;
* ```
*/
set valid(valid) {
this._valid = valid;
this.comboInput.valid = valid;
}
/**
* The value of the combo
*
* ```typescript
* // get
* let comboValue = this.combo.value;
* ```
*/
get value() {
return this._value;
}
/**
* The text displayed in the combo input
*
* ```typescript
* // get
* let comboDisplayValue = this.combo.displayValue;
* ```
*/
get displayValue() {
return this._displayValue;
}
/**
* Defines the current state of the virtualized data. It contains `startIndex` and `chunkSize`
*
* ```typescript
* // get
* let state = this.combo.virtualizationState;
* ```
*/
get virtualizationState() {
return this.virtDir.state;
}
/**
* Sets the current state of the virtualized data.
*
* ```typescript
* // set
* this.combo.virtualizationState(state);
* ```
*/
set virtualizationState(state) {
this.virtDir.state = state;
}
/**
* Gets drop down state.
*
* ```typescript
* let state = this.combo.collapsed;
* ```
*/
get collapsed() {
return this.dropdown.collapsed;
}
/**
* Gets total count of the virtual data items, when using remote service.
*
* ```typescript
* // get
* let count = this.combo.totalItemCount;
* ```
*/
get totalItemCount() {
return this.virtDir.totalItemCount;
}
/**
* Sets total count of the virtual data items, when using remote service.
*
* ```typescript
* // set
* this.combo.totalItemCount(remoteService.count);
* ```
*/
set totalItemCount(count) {
this.virtDir.totalItemCount = count;
}
/** @hidden @internal */
get template() {
this._dataType = this.dataType;
if (this.itemTemplate) {
return this.itemTemplate;
}
if (this._dataType === "complex" /* DataTypes.COMPLEX */) {
return this.complexTemplate;
}
return this.primitiveTemplate;
}
/**
* Configures the way combo items will be filtered.
*
* ```typescript
* // get
* let myFilteringOptions = this.combo.filteringOptions;
* ```
*
* ```html
* <!--set-->
* <igx-combo [filteringOptions]='myFilteringOptions'></igx-combo>
* ```
*/
get filteringOptions() {
return this._filteringOptions || this._defaultFilteringOptions;
}
set filteringOptions(value) {
this._filteringOptions = value;
}
ngAfterViewChecked() {
const targetElement = this.inputGroup.element.nativeElement.querySelector('.igx-input-group__bundle');
this._overlaySettings = {
target: targetElement,
scrollStrategy: new AbsoluteScrollStrategy(),
positionStrategy: new AutoPositionStrategy(),
modal: false,
closeOnOutsideClick: true,
excludeFromOutsideClick: [targetElement]
};
}
/** @hidden @internal */
ngAfterContentChecked() {
if (this.inputGroup && this.prefixes?.length > 0) {
this.inputGroup.prefixes = this.prefixes;
}
if (this.inputGroup) {
const suffixesArray = this.suffixes?.toArray() ?? [];
const internalSuffixesArray = this.internalSuffixes?.toArray() ?? [];
const mergedSuffixes = new QueryList();
mergedSuffixes.reset([
...suffixesArray,
...internalSuffixesArray
]);
this.inputGroup.suffixes = mergedSuffixes;
}
}
/** @hidden @internal */
ngOnInit() {
this.ngControl = this._injector.get(NgControl, null);
this.selectionService.set(this.id, new Set());
this._iconService?.addSvgIconFromText(caseSensitive.name, caseSensitive.value, 'imx-icons');
this.computedStyles = this.document.defaultView.getComputedStyle(this.elementRef.nativeElement);
}
/** @hidden @internal */
ngAfterViewInit() {
this.filteredData = [...this.data];
if (this.ngControl) {
this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged);
this.manageRequiredAsterisk();
this.cdr.detectChanges();
}
this.virtDir.chunkPreload.pipe(takeUntil(this.destroy$)).subscribe((e) => {
const eventArgs = Object.assign({}, e, { owner: this });
this.dataPreLoad.emit(eventArgs);
});
this.dropdown?.opening.subscribe((_args) => {
// calculate the container size and item size based on the sizes from the DOM
const dropdownContainerHeight = this.dropdownContainer.nativeElement.getBoundingClientRect().height;
if (dropdownContainerHeight) {
this.containerSize = parseFloat(dropdownContainerHeight);
}
if (this.dropdown.children?.first) {
this.itemSize = this.dropdown.children.first.element.nativeElement.getBoundingClientRect().height;
}
});
}
/** @hidden @internal */
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.comboAPI.clear();
this.selectionService.delete(this.id);
}
/**
* A method that opens/closes the combo.
*
* ```html
* <button type="button" (click)="combo.toggle()">Toggle Combo</button>
* <igx-combo #combo></igx-combo>
* ```
*/
toggle() {
if (this.collapsed && this._displayValue.length !== 0) {
this.filterValue = '';
this.cdr.detectChanges();
}
const overlaySettings = Object.assign({}, this._overlaySettings, this.overlaySettings);
this.dropdown.toggle(overlaySettings);
if (!this.collapsed) {
this.setActiveDescendant();
}
}
/**
* A method that opens the combo.
*
* ```html
* <button type="button" (click)="combo.open()">Open Combo</button>
* <igx-combo #combo></igx-combo>
* ```
*/
open() {
if (this.collapsed && this._displayValue.length !== 0) {
this.filterValue = '';
this.cdr.detectChanges();
}
const overlaySettings = Object.assign({}, this._overlaySettings, this.overlaySettings);
this.dropdown.open(overlaySettings);
this.setActiveDescendant();
}
/**
* A method that closes the combo.
*
* ```html
* <button type="button" (click)="combo.close()">Close Combo</button>
* <igx-combo #combo></igx-combo>
* ```
*/
close() {
this.dropdown.close();
}
/**
* Triggers change detection on the combo view
*/
triggerCheck() {
this.cdr.detectChanges();
}
/**
* Get current selection state
*
* @returns Array of selected items
* ```typescript
* let mySelection = this.combo.selection;
* ```
*/
get selection() {
const serviceRef = this.selectionService.get(this.id);
return serviceRef ? this.convertKeysToItems(Array.from(serviceRef)) : [];
}
/**
* Returns if the specified itemID is selected
*
* @hidden
* @internal
*/
isItemSelected(item) {
return this.selectionService.is_item_selected(this.id, item);
}
/** @hidden @internal */
get toggleIcon() {
return this.dropdown.collapsed ? 'input_expand' : 'input_collapse';
}
/** @hidden @internal */
addItemToCollection() {
if (!this.searchValue) {
return;
}
const addedItem = this.displayKey ? {
[this.valueKey]: this.searchValue,
[this.displayKey]: this.searchValue
} : this.searchValue;
if (this.groupKey) {
Object.assign(addedItem, { [this.groupKey]: this.defaultFallbackGroup });
}
// expose shallow copy instead of this.data in event args so this.data can't be mutated
const oldCollection = [...this.data];
const newCollection = [...this.data, addedItem];
const args = {
oldCollection, addedItem, newCollection, owner: this, cancel: false
};
this.addition.emit(args);
if (args.cancel) {
return;
}
this.data.push(args.addedItem);
// trigger re-render
this.data = cloneArray(this.data);
this.select(this.valueKey !== null && this.valueKey !== undefined ?
[args.addedItem[this.valueKey]] : [args.addedItem], false);
this.customValueFlag = false;
this.searchInput?.nativeElement.focus();
this.dropdown.focusedItem = null;
this.virtDir.scrollTo(0);
}
/** @hidden @internal */
isAddButtonVisible() {
// This should always return a boolean value. If this.searchValue was '', it returns '' instead of false;
return this.searchValue !== '' && this.customValueFlag;
}
/** @hidden @internal */
handleInputChange(event) {
if (event !== undefined) {
const args = {
searchText: typeof event === 'string' ? event : event.target.value,
owner: this,
cancel: false
};
this.searchInputUpdate.emit(args);
if (args.cancel) {
this.filterValue = null;
}
}
this.checkMatch();
}
/**
* Event handlers
*
* @hidden
* @internal
*/
handleOpening(e) {
const args = { owner: this, event: e.event, cancel: e.cancel };
this.opening.emit(args);
e.cancel = args.cancel;
}
/** @hidden @internal */
handleClosing(e) {
const args = { owner: this, event: e.event, cancel: e.cancel };
this.closing.emit(args);
e.cancel = args.cancel;
if (e.cancel) {
return;
}
this.searchValue = '';
if (!e.event) {
this.comboInput?.nativeElement.focus();
}
else {
this._onTouchedCallback();
this.updateValidity();
}
}
/** @hidden @internal */
handleClosed() {
this.closed.emit({ owner: this });
}
/** @hidden @internal */
handleKeyDown(event) {
if (event.key === 'ArrowUp' || event.key === 'Up') {
event.preventDefault();
event.stopPropagation();
this.close();
}
}
/** @hidden @internal */
handleToggleKeyDown(eventArgs) {
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
eventArgs.preventDefault();
this.toggle();
}
}
/** @hidden @internal */
getAriaLabel() {
return this.displayValue ? this.resourceStrings.igx_combo_aria_label_options : this.resourceStrings.igx_combo_aria_label_no_options;
}
/** @hidden @internal */
registerOnChange(fn) {
this._onChangeCallback = fn;
}
/** @hidden @internal */
registerOnTouched(fn) {
this._onTouchedCallback = fn;
}
/** @hidden @internal */
setDisabledState(isDisabled) {
this.disabled = isDisabled;
}
/** @hidden @internal */
onClick(event) {
event.stopPropagation();
event.preventDefault();
if (!this.disabled) {
this.toggle();
}
}
/** @hidden @internal */
onBlur() {
if (this.collapsed) {
this._onTouchedCallback();
this.updateValidity();
}
}
/** @hidden @internal */
setActiveDescendant() {
this.activeDescendant = this.dropdown.focusedItem?.id || '';
}
/** @hidden @internal */
toggleCaseSensitive() {
this.filteringOptions = Object.assign({}, this.filteringOptions, { caseSensitive: !this.filteringOptions.caseSensitive });
}
updateValidity() {
if (this.ngControl && this.ngControl.invalid) {
this.valid = IgxInputState.INVALID;
}
else {
this.valid = IgxInputState.INITIAL;
}
}
get isTouchedOrDirty() {
return (this.ngControl.control.touched || this.ngControl.control.dirty);
}
get hasValidators() {
return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
}
/** if there is a valueKey - map the keys to data items, else - just return the keys */
convertKeysToItems(keys) {
if (this.valueKey === null || this.valueKey === undefined) {
return keys;
}
return keys.map(key => {
const item = this.data.find(entry => isEqual(entry[this.valueKey], key));
return item !== undefined ? item : { [this.valueKey]: key };
});
}
checkMatch() {
const itemMatch = this.filteredData.some(this.findMatch);
this.customValueFlag = this.allowCustomValues && !itemMatch;
}
manageRequiredAsterisk() {
if (this.ngControl) {
this.inputGroup.isRequired = this.required;
}
}
/** Contains key-value pairs of the selected valueKeys and their resp. displayKeys */
registerRemoteEntries(ids, add = true) {
if (add) {
const selection = this.getValueDisplayPairs(ids);
for (const entry of selection) {
this._remoteSelection[entry[this.valueKey]] = entry[this.displayKey];