UNPKG

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
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];