import { FlexibleConnectedPositionStrategy, Overlay, OverlayRef, } from "@angular/cdk/overlay"; import {ComponentPortal} from "@angular/cdk/portal"; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, ElementRef, Input, OnInit, Output, ViewContainerRef, } from "@angular/core"; import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALUE_ACCESSOR, } from "@angular/forms"; import {FloatLabelType} from "@angular/material/core"; import {SharedAbstractPresentational} from "@app/shared/components/presentationals/shared-abstract/shared-abstract.presentational"; import {Store} from "@ngxs/store"; import {SharedSearchableSingleSelectContentPresentational} from "@shared/components/presentationals/shared-searchable-single-select-content/shared-searchable-single-select-content.presentational"; import {BaseFormDefinition} from "@shared/models/base-form-definition.model"; import { BehaviorSubject, Observable, } from "rxjs"; import { distinctUntilChanged, filter, take, tap, } from "rxjs/operators"; import { FormValidationHelper, isEmptyString, isNonEmptyString, isNullOrUndefined, isTrue, ObservableUtil, PropertyName, ResourceNameSpace, ResourceState, } from "solidify-frontend"; @Component({ selector: "dlcm-shared-searchable-single-select", templateUrl: "./shared-searchable-single-select.presentational.html", styleUrls: ["./shared-searchable-single-select.presentational.scss"], changeDetection: ChangeDetectionStrategy.OnPush, providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: SharedSearchableSingleSelectPresentational, }, ], }) export class SharedSearchableSingleSelectPresentational extends SharedAbstractPresentational implements ControlValueAccessor, OnInit, AfterViewInit { formDefinition: FormComponentFormDefinition = new FormComponentFormDefinition(); formLabel: FormGroup; @Input() placeholder: string; @Input() valueKey: string; @Input() labelKey: string; @Input() labelCallback: (value: any) => string = (value => value[this.labelKey]); @Input() formControl: FormControl; @Input() required: boolean; @Input() resourceNameSpace: ResourceNameSpace; @Input() state: typeof ResourceState; private _readonly: boolean; @Input() set readonly(value: boolean) { this._readonly = value; this.updateFormReadonlyState(); } get readonly(): boolean { return this._readonly; } currentObs: Observable<any>; isLoadingObs: Observable<boolean>; floatLabel: FloatLabelType; alreadyInit: boolean = false; noFormControl: boolean; labelInitialized: boolean = false; get formValidationHelper(): typeof FormValidationHelper { return FormValidationHelper; } private readonly _valueBS: BehaviorSubject<any | undefined> = new BehaviorSubject<any | undefined>(undefined); @Output("valueChange") readonly valueObs: Observable<any | undefined> = ObservableUtil.asObservable(this._valueBS); constructor(private _store: Store, protected _fb: FormBuilder, private _elementRef: ElementRef, private _viewContainerRef: ViewContainerRef, private _overlay: Overlay, private _changeDetector: ChangeDetectorRef) { super(); } ngOnInit(): void { if (isNullOrUndefined(this.formControl)) { this.noFormControl = true; this.formControl = this._fb.control(undefined); } if (isTrue(this.alreadyInit)) { // When validator is updated, the ngOnInit is call a new time // This hack prevent this to happen (case with deposit license validator impacted by access level return; } this._initFormLabel(); this._dispatchGetActionToRetrieveLabel(); this._observeLabelUpdate(); this.alreadyInit = true; } private _initFormLabel(): void { this.formLabel = this._fb.group({ [this.formDefinition.value]: [undefined], }); this.updateFormReadonlyState(); } private updateFormReadonlyState(): void { if (isNullOrUndefined(this.formLabel)) { return; } if (this.readonly) { this.formLabel.disable(); } else { this.formLabel.enable(); } } private _dispatchGetActionToRetrieveLabel(): void { const key = this.formControl.value; this.currentObs = this.getSelectorWithMemoizedSelectorName("current"); this.isLoadingObs = this.getSelectorWithMemoizedSelectorName("isLoading"); if (!isNullOrUndefined(key) && isNonEmptyString(key)) { this._store.dispatch(new this.resourceNameSpace.GetById(key)); } else { this.labelInitialized = true; } } private _observeLabelUpdate(): void { this.subscribe(this.currentObs.pipe( distinctUntilChanged(), filter((value) => !isNullOrUndefined(value)), take(1), tap((value) => { if (isNullOrUndefined(this.formControl.value) || isEmptyString(this.formControl.value)) { return; } if (value[this.valueKey] === this.formControl.value) { this.formLabel.get(this.formDefinition.value).setValue(this.labelCallback(value)); } else { this.formLabel.get(this.formDefinition.value).setValue("Label not found for " + this.formControl.value); } this.labelInitialized = true; this.computeFloatLabel(); }), )); } getSelectorWithMemoizedSelectorName(key: string): Observable<any> { try { return this._store.select(this.state[key]); } catch (e) { throw new Error(`The state '${this.state.name}' should have the memoized selector with name '${key}'`); } } propagateChange = (__: any) => {}; registerOnChange(fn: any): void { this.propagateChange = fn; } registerOnTouched(fn: any): void { } setDisabledState(isDisabled: boolean): void { } writeValue(value: string[]): void { if (!isNullOrUndefined(value) && this.noFormControl) { this.formControl.setValue(value); this._dispatchGetActionToRetrieveLabel(); } } openOverlay(): void { const widthField = this._elementRef.nativeElement.getBoundingClientRect().width; const overlayRef = this._overlay.create({ positionStrategy: this._getPosition(this._elementRef), scrollStrategy: this._overlay.scrollStrategies.block(), hasBackdrop: true, backdropClass: "cdk-overlay-transparent-backdrop", width: widthField, }); const overlayContentComponent = new ComponentPortal(SharedSearchableSingleSelectContentPresentational); const componentRef = overlayRef.attach(overlayContentComponent); componentRef.instance.host = this; this._observeValueSelectedInOverlay(componentRef, overlayRef); this._observeCloseEventInOverlay(componentRef, overlayRef); this._observeClickBackdrop(componentRef, overlayRef); } private _observeValueSelectedInOverlay(componentRef: ComponentRef<SharedSearchableSingleSelectContentPresentational>, overlayRef: OverlayRef): void { this.subscribe(componentRef.instance.valueObs.pipe( distinctUntilChanged(), tap(value => { const valueKey = value[this.valueKey]; this.propagateChange(valueKey); this.formControl.setValue(valueKey); this._valueBS.next(valueKey); this.formLabel.get(this.formDefinition.value).setValue(this.labelCallback(value)); this.computeFloatLabel(); this._closeOverlay(overlayRef); }), )); } private _observeCloseEventInOverlay(componentRef: ComponentRef<SharedSearchableSingleSelectContentPresentational>, overlayRef: OverlayRef): void { this.subscribe(componentRef.instance.closeObs.pipe( tap(() => { this._closeOverlay(overlayRef); }), )); } private _observeClickBackdrop(componentRef: ComponentRef<SharedSearchableSingleSelectContentPresentational>, overlayRef: OverlayRef): void { this.subscribe(overlayRef.backdropClick().pipe( tap(() => { this._closeOverlay(overlayRef); }), )); } private _closeOverlay(overlayRef: OverlayRef): void { overlayRef.detach(); this.formControl.markAsTouched(); this._changeDetector.detectChanges(); } private _getPosition(elementToConnectTo: ElementRef): FlexibleConnectedPositionStrategy { return this._overlay.position() .flexibleConnectedTo(elementToConnectTo) .withFlexibleDimensions(false) .withPositions([ { originX: "start", originY: "top", overlayX: "start", overlayY: "top", }, ]); } isValuePresent(): boolean { return isNonEmptyString(this.formControl.value); } clearValue($event: MouseEvent): void { this.formControl.setValue(""); this.formLabel.get(this.formDefinition.value).setValue(null); this._valueBS.next(undefined); $event.stopPropagation(); this.computeFloatLabel(); } computeFloatLabel(): void { const value = this.formLabel.get(this.formDefinition.value).value; if (isNullOrUndefined(value) || isEmptyString(value)) { this.floatLabel = "never"; } else { this.floatLabel = "always"; } this._changeDetector.detectChanges(); } focusInputIfNotClickTarget($event: MouseEvent, input: HTMLInputElement): void { if ($event.target !== input) { input.focus(); } } } class FormComponentFormDefinition extends BaseFormDefinition { @PropertyName() search: string; @PropertyName() value: string; }