Skip to content
Snippets Groups Projects
shared-searchable-single-select.presentational.ts 9.33 KiB
Newer Older
import {
  FlexibleConnectedPositionStrategy,
  Overlay,
  OverlayRef,
} from "@angular/cdk/overlay";
import {ComponentPortal} from "@angular/cdk/portal";
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  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,
  FormValidationHelper,
  isNonEmptyString,
  isNullOrUndefined,
  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;
  }

  isLoadingObs: Observable<boolean>;
  noFormControl: boolean;
  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);

              private _viewContainerRef: ViewContainerRef,
              private _changeDetector: ChangeDetectorRef) {
    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();
  }

  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) {
    } 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)),
        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);
        }
      }),
    ));
  }

  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();
    }
    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.formLabel.get(this.formDefinition.value).setValue(this.labelCallback(value));
        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 {
      .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);
    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;
}