File

libs/acl/input-element-injector-directive/src/lib/input-element-injector-directive.directive.ts

Description

This directive adds the corresponding native input element to a form control Heavily inspired by https://stackoverflow.com/a/49462046

This directive provides access to the nativeElement of a FormControl, as long as the native element is an input, select, textarea, nx-dropdown, nx-radio-group, or nx-selectable-card-group. It also provides access to the NxAbstractControl provided by some NDBX components.

The nativeElement$ BehaviorSubject always fires whenever the nativeElement is discovered or changed. The nxAbstractControl$ BehaviorSubject always fires whenever the nxAbstractControl is discovered or changed.

How to Use

Import the InputElementInjectorModule from @allianz/taly-acl/input-element-injector-directive.

The nativeElement$ and nxAbstractControl$ BehaviorSubject is then added to the FormControls. There are helper function available, getNativeElement$(control) and getNxAbstractControl$(control), which ensures that the nativeElement$ and nxAbstractControl$ can always be accessed, independent of potential timing issues.

getNativeElement$(control).subscribe((nativeElement) => {});
getNxAbstractControl$(control).subscribe((nxAbstractControl) => {});

Implements

AfterViewInit OnDestroy

Metadata

Constructor

constructor()
import {
  AfterViewInit,
  ChangeDetectorRef,
  Directive,
  ElementRef,
  NgZone,
  OnDestroy,
  inject
} from '@angular/core';
import { AbstractControl, NgControl } from '@angular/forms';
import { NxAbstractControl } from '@aposin/ng-aquila/shared';
import { BehaviorSubject } from 'rxjs';
import { TalyAbstractControl } from './taly-control.model';

/**
 * This directive adds the corresponding native input element to a form control
 * Heavily inspired by https://stackoverflow.com/a/49462046
 *
 * This directive provides access to the `nativeElement` of a `FormControl`, as long as the native element is an `input`, `select`, `textarea`,
 * `nx-dropdown`, `nx-radio-group`, or `nx-selectable-card-group`.
 * It also provides access to the `NxAbstractControl` provided by some NDBX components.
 *
 * The `nativeElement$ BehaviorSubject` always fires whenever the `nativeElement` is discovered or changed.
 * The `nxAbstractControl$ BehaviorSubject` always fires whenever the `nxAbstractControl` is discovered or changed.
 *
 * How to Use
 *
 * Import the `InputElementInjectorModule` from `@allianz/taly-acl/input-element-injector-directive`.
 *
 * The `nativeElement$` and `nxAbstractControl$` `BehaviorSubject` is then added to the `FormControl`s. There are helper function available, `getNativeElement$(control)` and `getNxAbstractControl$(control)`,
 * which ensures that the `nativeElement$` and `nxAbstractControl$` can always be accessed, independent of potential timing issues.
 *
 * ```typescript
 * getNativeElement$(control).subscribe((nativeElement) => {});
 * ```
 * ```typescript
 * getNxAbstractControl$(control).subscribe((nxAbstractControl) => {});
 * ```
 */
@Directive({
  // eslint-disable-next-line @angular-eslint/directive-selector
  selector: '[formControlName], [formControl]',
  exportAs: 'formControlNativeElement',
  standalone: false
})
export class InputElementInjectorDirective implements AfterViewInit, OnDestroy {
  private ngControl = inject(NgControl, { optional: true });
  private nxAbstractControl = inject(NxAbstractControl, { optional: true });
  private elementRef = inject(ElementRef<HTMLElement>);
  private cd = inject(ChangeDetectorRef);

  private ngZone = inject(NgZone);

  private mutationObserver!: MutationObserver;

  constructor() {
    this.ngZone.runOutsideAngular(() => {
      this.mutationObserver = new MutationObserver((records: MutationRecord[]) => {
        const nothingWasAdded = records.every((record) => record.addedNodes.length === 0);
        const nothingWasRemoved = records.every((record) => record.removedNodes.length === 0);
        if (nothingWasAdded && nothingWasRemoved) {
          return;
        }
        this.findNativeElement();
      });
    });
  }

  ngAfterViewInit() {
    if (!this.ngControl || !this.ngControl.control) {
      return;
    }

    if (this.nxAbstractControl) {
      getNxAbstractControl$(this.ngControl.control).next(this.nxAbstractControl);
      // this additional change detection cycle avoids an "ExpressionChangedAfterItHasBeenCheckedError" when readonly is set via policy.txt
      this.cd.detectChanges();
    }

    this.mutationObserver.observe(this.elementRef.nativeElement, {
      childList: true,
      subtree: true
    });

    this.findNativeElement();
  }

  ngOnDestroy(): void {
    if (!this.ngControl || !this.ngControl.control) {
      return;
    }

    getNativeElement$(this.ngControl.control).next(undefined);

    this.mutationObserver.disconnect();
  }

  private findNativeElement() {
    if (!this.ngControl || !this.ngControl.control) {
      return;
    }

    const nativeElement = this.elementRef.nativeElement;
    let formInputElement: HTMLElement | null = null;

    if (
      [
        'input',
        'select',
        'textarea',
        'nx-dropdown',
        'nx-radio-group',
        'nx-selectable-card-group'
      ].includes(nativeElement.tagName.toLowerCase())
    ) {
      formInputElement = nativeElement;
    } else {
      formInputElement =
        nativeElement.querySelector('input') ??
        nativeElement.querySelector('select') ??
        nativeElement.querySelector('textarea');
    }

    if (formInputElement && getNativeElement$(this.ngControl.control).value !== formInputElement) {
      getNativeElement$(this.ngControl.control).next(formInputElement);
    }
  }
}

/**
 * getNativeElement$() allows it to subscribe to native element changes of a control.
 * Use this function instead of directly accessing control.nativeElement$ to avoid issues regarding timing.
 *
 * For example, this might be the case when the native element of a control
 * is initially hidden and appears at a later time.
 */
export function getNativeElement$(
  control: AbstractControl | TalyAbstractControl
): BehaviorSubject<HTMLElement | undefined> {
  (control as TalyAbstractControl).nativeElement$ ??= new BehaviorSubject<HTMLElement | undefined>(
    undefined
  );
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  return (control as TalyAbstractControl).nativeElement$!;
}

/**
 * getNxAbstractControl$() allows it to subscribe to nxAbstractControl changes of a control.
 * Use this function to access the nxAbstractControl of a component without timing issues.
 * It provides functionality to set the readonly state.
 *
 * For example, this might be the case when the element of a nxAbstractControl
 * is initially hidden and appears at a later time.
 */
export function getNxAbstractControl$(control: AbstractControl | TalyAbstractControl) {
  (control as TalyAbstractControl).nxAbstractControl$ ??= new BehaviorSubject<
    NxAbstractControl | undefined
  >(undefined);
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  return (control as TalyAbstractControl).nxAbstractControl$!;
}

results matching ""

    No results matching ""