libs/acl/input-element-injector-directive/src/lib/input-element-injector-directive.directive.ts
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.
getNativeElement$(control).subscribe((nativeElement) => {});
getNxAbstractControl$(control).subscribe((nxAbstractControl) => {});
Selector | [formControlName], [formControl] |
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$!;
}