igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
625 lines (545 loc) • 19.9 kB
text/typescript
import {
AfterContentChecked,
AfterContentInit,
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChild,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
forwardRef,
HostBinding,
Inject,
Injector,
Input,
OnDestroy,
OnInit,
Optional,
Output,
QueryList,
TemplateRef,
ViewChild
} from '@angular/core';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { AbstractControl, ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { noop } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
import { EditorProvider } from '../core/edit-provider';
import { IgxSelectionAPIService } from '../core/selection';
import { IBaseCancelableBrowserEventArgs, IBaseEventArgs } from '../core/utils';
import { IgxLabelDirective } from '../directives/label/label.directive';
import { IgxDropDownItemBaseDirective } from '../drop-down/drop-down-item.base';
import { IGX_DROPDOWN_BASE, ISelectionEventArgs, Navigate } from '../drop-down/drop-down.common';
import { IgxInputGroupComponent } from '../input-group/input-group.component';
import { AbsoluteScrollStrategy } from '../services/overlay/scroll/absolute-scroll-strategy';
import { OverlaySettings } from '../services/overlay/utilities';
import { IgxDropDownComponent } from './../drop-down/drop-down.component';
import { IgxSelectItemComponent } from './select-item.component';
import { SelectPositioningStrategy } from './select-positioning-strategy';
import { IgxSelectBase } from './select.common';
import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from '../directives/toggle/toggle.directive';
import { IgxOverlayService } from '../services/overlay/overlay';
import { IgxIconComponent } from '../icon/icon.component';
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
import { IgxSelectItemNavigationDirective } from './select-navigation.directive';
import { IgxInputDirective, IgxInputState } from '../directives/input/input.directive';
/** @hidden @internal */
export class IgxSelectToggleIconDirective {
}
/** @hidden @internal */
export class IgxSelectHeaderDirective {
}
/** @hidden @internal */
export class IgxSelectFooterDirective {
}
/**
* **Ignite UI for Angular Select** -
* [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)
*
* The `igxSelect` provides an input with dropdown list allowing selection of a single item.
*
* Example:
* ```html
* <igx-select #select1 [placeholder]="'Pick One'">
* <label igxLabel>Select Label</label>
* <igx-select-item *ngFor="let item of items" [value]="item.field">
* {{ item.field }}
* </igx-select-item>
* </igx-select>
* ```
*/
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
/** @hidden @internal */
public inputGroup: IgxInputGroupComponent;
/** @hidden @internal */
public input: IgxInputDirective;
/** @hidden @internal */
public override children: QueryList<IgxSelectItemComponent>;
protected prefixes: QueryList<IgxPrefixDirective>;
protected suffixes: QueryList<IgxSuffixDirective>;
/** @hidden @internal */
public label: IgxLabelDirective;
/**
* An @Input property that sets input placeholder.
*
*/
public placeholder;
/**
* An @Input property that disables the `IgxSelectComponent`.
* ```html
* <igx-select [disabled]="'true'"></igx-select>
* ```
*/
public disabled = false;
/**
* An @Input property that sets custom OverlaySettings `IgxSelectComponent`.
* ```html
* <igx-select [overlaySettings] = "customOverlaySettings"></igx-select>
* ```
*/
public overlaySettings: OverlaySettings;
/** @hidden @internal */
public override maxHeight = '256px';
/**
* Emitted before the dropdown is opened
*
* ```html
* <igx-select opening='handleOpening($event)'></igx-select>
* ```
*/
public override opening = new EventEmitter<IBaseCancelableBrowserEventArgs>();
/**
* Emitted after the dropdown is opened
*
* ```html
* <igx-select (opened)='handleOpened($event)'></igx-select>
* ```
*/
public override opened = new EventEmitter<IBaseEventArgs>();
/**
* Emitted before the dropdown is closed
*
* ```html
* <igx-select (closing)='handleClosing($event)'></igx-select>
* ```
*/
public override closing = new EventEmitter<IBaseCancelableBrowserEventArgs>();
/**
* Emitted after the dropdown is closed
*
* ```html
* <igx-select (closed)='handleClosed($event)'></igx-select>
* ```
*/
public override closed = new EventEmitter<IBaseEventArgs>();
/**
* The custom template, if any, that should be used when rendering the select TOGGLE(open/close) button
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.select.toggleIconTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-select #select>
* ...
* <ng-template igxSelectToggleIcon let-collapsed>
* <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
* </ng-template>
* </igx-select>
* ```
*/
public toggleIconTemplate: TemplateRef<any> = null;
/**
* The custom template, if any, that should be used when rendering the HEADER for the select items list
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.select.headerTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-select #select>
* ...
* <ng-template igxSelectHeader>
* <div class="select__header">
* This is a custom header
* </div>
* </ng-template>
* </igx-select>
* ```
*/
public headerTemplate: TemplateRef<any> = null;
/**
* The custom template, if any, that should be used when rendering the FOOTER for the select items list
*
* ```typescript
* // Set in typescript
* const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
* myComponent.select.footerTemplate = myCustomTemplate;
* ```
* ```html
* <!-- Set in markup -->
* <igx-select #select>
* ...
* <ng-template igxSelectFooter>
* <div class="select__footer">
* This is a custom footer
* </div>
* </ng-template>
* </igx-select>
* ```
*/
public footerTemplate: TemplateRef<any> = null;
private hintElement: ElementRef;
/** @hidden @internal */
public override width: string;
/** @hidden @internal do not use the drop-down container class */
public override cssClass = false;
/** @hidden @internal */
public override allowItemsFocus = false;
/** @hidden @internal */
public override height: string;
private ngControl: NgControl = null;
private _overlayDefaults: OverlaySettings;
private _value: any;
private _type = null;
/**
* An @Input property that gets/sets the component value.
*
* ```typescript
* // get
* let selectValue = this.select.value;
* ```
*
* ```typescript
* // set
* this.select.value = 'London';
* ```
* ```html
* <igx-select [value]="value"></igx-select>
* ```
*/
public get value(): any {
return this._value;
}
public set value(v: any) {
if (this._value === v) {
return;
}
this._value = v;
this.setSelection(this.items.find(x => x.value === this.value));
}
/**
* An @Input property that sets how the select will be styled.
* The allowed values are `line`, `box` and `border`. The input-group default is `line`.
* ```html
* <igx-select [type]="'box'"></igx-select>
* ```
*/
public get type(): IgxInputGroupType {
return this._type || this._inputGroupType || 'line';
}
public set type(val: IgxInputGroupType) {
this._type = val;
}
/** @hidden @internal */
public get selectionValue() {
const selectedItem = this.selectedItem;
return selectedItem ? selectedItem.itemText : '';
}
/** @hidden @internal */
public override get selectedItem(): IgxSelectItemComponent {
return this.selection.first_item(this.id);
}
private _onChangeCallback: (_: any) => void = noop;
private _onTouchedCallback: () => void = noop;
constructor(
elementRef: ElementRef,
cdr: ChangeDetectorRef,
selection: IgxSelectionAPIService,
protected overlayService: IgxOverlayService,
_displayDensityOptions: IDisplayDensityOptions,
private _inputGroupType: IgxInputGroupType,
private _injector: Injector) {
super(elementRef, cdr, selection, _displayDensityOptions);
}
//#region ControlValueAccessor
/** @hidden @internal */
public writeValue = (value: any) => {
this.value = value;
};
/** @hidden @internal */
public registerOnChange(fn: any): void {
this._onChangeCallback = fn;
}
/** @hidden @internal */
public registerOnTouched(fn: any): void {
this._onTouchedCallback = fn;
}
/** @hidden @internal */
public setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
//#endregion
/** @hidden @internal */
public getEditElement(): HTMLInputElement {
return this.input.nativeElement;
}
/** @hidden @internal */
public override selectItem(newSelection: IgxDropDownItemBaseDirective, event?) {
const oldSelection = this.selectedItem ?? <IgxDropDownItemBaseDirective>{};
if (newSelection === null || newSelection.disabled || newSelection.isHeader) {
return;
}
if (newSelection === oldSelection) {
this.toggleDirective.close();
return;
}
const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false, owner:this };
this.selectionChanging.emit(args);
if (args.cancel) {
return;
}
this.setSelection(newSelection);
this._value = newSelection.value;
if (event) {
this.toggleDirective.close();
}
this.cdr.detectChanges();
this._onChangeCallback(this.value);
}
/** @hidden @internal */
public getFirstItemElement(): HTMLElement {
return this.children.first.element.nativeElement;
}
/**
* Opens the select
*
* ```typescript
* this.select.open();
* ```
*/
public override open(overlaySettings?: OverlaySettings) {
if (this.disabled || this.items.length === 0) {
return;
}
if (!this.selectedItem) {
this.navigateFirst();
}
super.open(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
}
public inputGroupClick(event: MouseEvent, overlaySettings?: OverlaySettings) {
const targetElement = event.target as HTMLElement;
if (this.hintElement && targetElement.contains(this.hintElement.nativeElement)) {
return;
}
this.toggle(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
}
/** @hidden @internal */
public ngAfterContentInit() {
this._overlayDefaults = {
target: this.getEditElement(),
modal: false,
positionStrategy: new SelectPositioningStrategy(this),
scrollStrategy: new AbsoluteScrollStrategy(),
excludeFromOutsideClick: [this.inputGroup.element.nativeElement as HTMLElement]
};
const changes$ = this.children.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.setSelection(this.items.find(x => x.value === this.value));
this.cdr.detectChanges();
});
Promise.resolve().then(() => {
if (!changes$.closed) {
this.children.notifyOnChanges();
}
});
}
/**
* Event handlers
*
* @hidden @internal
*/
public handleOpening(e: ToggleViewCancelableEventArgs) {
const args: IBaseCancelableBrowserEventArgs = { owner:this, event:e.event, cancel: e.cancel };
this.opening.emit(args);
e.cancel = args.cancel;
if (args.cancel) {
return;
}
}
/** @hidden @internal */
public override onToggleContentAppended(event: ToggleViewEventArgs) {
const info = this.overlayService.getOverlayById(event.id);
if (info?.settings?.positionStrategy instanceof SelectPositioningStrategy) {
return;
}
super.onToggleContentAppended(event);
}
/** @hidden @internal */
public handleOpened() {
this.updateItemFocus();
this.opened.emit({ owner: this });
}
/** @hidden @internal */
public handleClosing(e: ToggleViewCancelableEventArgs) {
const args: IBaseCancelableBrowserEventArgs = { owner:this, event:e.event, cancel: e.cancel };
this.closing.emit(args);
e.cancel = args.cancel;
}
/** @hidden @internal */
public handleClosed() {
this.focusItem(false);
this.closed.emit({ owner: this });
}
/** @hidden @internal */
public onBlur(): void {
this._onTouchedCallback();
if (this.ngControl && this.ngControl.invalid) {
this.input.valid = IgxInputState.INVALID;
} else {
this.input.valid = IgxInputState.INITIAL;
}
}
/** @hidden @internal */
public onFocus(): void {
this._onTouchedCallback();
}
/**
* @hidden @internal
*/
public override ngOnInit() {
this.ngControl = this._injector.get<NgControl>(NgControl, null);
super.ngOnInit();
}
/**
* @hidden @internal
*/
public override ngAfterViewInit() {
super.ngAfterViewInit();
if (this.ngControl) {
this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this));
this.manageRequiredAsterisk();
}
this.cdr.detectChanges();
}
/** @hidden @internal */
public ngAfterContentChecked() {
if (this.inputGroup && this.prefixes?.length > 0) {
this.inputGroup.prefixes = this.prefixes;
}
if (this.inputGroup && this.suffixes?.length > 0) {
this.inputGroup.suffixes = this.suffixes;
}
}
/**
* @hidden @internal
*/
public override ngOnDestroy() {
this.destroy$.next(true);
this.destroy$.complete();
this.selection.clear(this.id);
}
/**
* @hidden @internal
* Prevent input blur - closing the items container on Header/Footer Template click.
*/
public mousedownHandler(event) {
event.preventDefault();
}
protected onStatusChanged() {
this.manageRequiredAsterisk();
if (this.ngControl && !this.disabled && this.isTouchedOrDirty) {
if (this.hasValidators && this.inputGroup.isFocused) {
this.input.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
} else {
// B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
this.input.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
} else {
this.input.valid = IgxInputState.INITIAL;
}
}
private get isTouchedOrDirty(): boolean {
return (this.ngControl.control.touched || this.ngControl.control.dirty);
}
private get hasValidators(): boolean {
return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
}
protected override navigate(direction: Navigate, currentIndex?: number) {
if (this.collapsed && this.selectedItem) {
this.navigateItem(this.selectedItem.itemIndex);
}
super.navigate(direction, currentIndex);
}
protected manageRequiredAsterisk(): void {
const hasRequiredHTMLAttribute = this.elementRef.nativeElement.hasAttribute('required');
if (this.ngControl && this.ngControl.control.validator) {
// Run the validation with empty object to check if required is enabled.
const error = this.ngControl.control.validator({} as AbstractControl);
this.inputGroup.isRequired = error && error.required;
this.cdr.markForCheck();
// If validator is dynamically cleared and no required HTML attribute is set,
// reset label's required class(asterisk) and IgxInputState #6896
} else if (this.inputGroup.isRequired && this.ngControl && !this.ngControl.control.validator && !hasRequiredHTMLAttribute) {
this.input.valid = IgxInputState.INITIAL;
this.inputGroup.isRequired = false;
this.cdr.markForCheck();
}
}
private setSelection(item: IgxDropDownItemBaseDirective) {
if (item && item.value !== undefined && item.value !== null) {
this.selection.set(this.id, new Set([item]));
} else {
this.selection.clear(this.id);
}
}
}