Newer
Older
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 {
isEmptyString,
isNonEmptyString,
isNullOrUndefined,

Florent Poittevin
committed
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;

Florent Poittevin
committed
alreadyInit: boolean = false;
noFormControl: boolean;

Florent Poittevin
committed
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);

Florent Poittevin
committed
constructor(private _store: Store,
protected _fb: FormBuilder,

Florent Poittevin
committed
private _elementRef: ElementRef,
private _viewContainerRef: ViewContainerRef,

Florent Poittevin
committed
private _overlay: Overlay,
private _changeDetector: ChangeDetectorRef) {
super();
}
ngOnInit(): void {
if (isNullOrUndefined(this.formControl)) {
this.noFormControl = true;
this.formControl = this._fb.control(undefined);
}

Florent Poittevin
committed
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();

Florent Poittevin
committed
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)) {

Florent Poittevin
committed
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 {

Florent Poittevin
committed
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;

Florent Poittevin
committed
const overlayRef = this._overlay.create({
positionStrategy: this._getPosition(this._elementRef),
scrollStrategy: this._overlay.scrollStrategies.block(),
hasBackdrop: true,
backdropClass: "cdk-overlay-transparent-backdrop",
});
const overlayContentComponent = new ComponentPortal(SharedSearchableSingleSelectContentPresentational);
const componentRef = overlayRef.attach(overlayContentComponent);
componentRef.instance.host = this;
this._observeValueSelectedInOverlay(componentRef, overlayRef);
this._observeCloseEventInOverlay(componentRef, overlayRef);

Florent Poittevin
committed
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);
}),
));
}

Florent Poittevin
committed
private _observeClickBackdrop(componentRef: ComponentRef<SharedSearchableSingleSelectContentPresentational>, overlayRef: OverlayRef): void {
this.subscribe(overlayRef.backdropClick().pipe(
tap(() => {
this._closeOverlay(overlayRef);
}),
));
}
private _closeOverlay(overlayRef: OverlayRef): void {
overlayRef.detach();

Florent Poittevin
committed
this.formControl.markAsTouched();
this._changeDetector.detectChanges();
}
private _getPosition(elementToConnectTo: ElementRef): FlexibleConnectedPositionStrategy {

Florent Poittevin
committed
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 {

Florent Poittevin
committed
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();

Florent Poittevin
committed
focusInputIfNotClickTarget($event: MouseEvent, input: HTMLInputElement): void {
if ($event.target !== input) {
input.focus();
}
}
}
class FormComponentFormDefinition extends BaseFormDefinition {
@PropertyName() search: string;
@PropertyName() value: string;
}