import {
ComponentFactoryResolver,
ComponentRef,
Directive,
EmbeddedViewRef,
Host,
Inject,
Input,
OnChanges,
OnDestroy,
OnInit,
Optional,
Self,
SimpleChanges,
TemplateRef,
ViewContainerRef
} from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { AclTagHintComponent } from '../acl-hint/public-api';
import { AclInspectorService } from '../inspector/inspector.service';
import { createAsyncAclTagProviders } from '../providers/public-api';
import { AclService } from '../services/acl.service';
import { ACL_TAG_NAME_ASYNC_TOKEN, ACL_TAG_TOKEN, ACL_TAG_TRANSIENT } from '../tokens/public-api';
import { AclTag } from '@allianz/taly-acl';
/**
* Overloaded directive which acts as
* A) a tagging mechanism to create an acl hierarchy (aclTag="my-tag")
* B) people can also use it as a structural directive (*aclTag="'my-tag'")
* to hide resources depending on their granted acl view action
*/
@Directive({
selector: '[aclTag]',
providers: [createAsyncAclTagProviders()],
standalone: false
})
export class AclTagDirective implements OnInit, OnDestroy, OnChanges {
private show = false;
private hintIsVisible = false;
private componentIsVisible = false;
destroyed$ = new Subject<void>();
@Input('aclTagHint') hint = false;
@Input('aclTagTransient') transient = false;
@Input('aclTag') tag!: string;
inspectorServiceOverrideShowHint = false;
aclTagHintComponentRef!: ComponentRef<AclTagHintComponent>;
currentViewRef!: EmbeddedViewRef<unknown>;
constructor(
@Optional()
@Self()
@Inject(ACL_TAG_NAME_ASYNC_TOKEN)
private aclTagUpdate$: BehaviorSubject<string>,
@Optional()
@Host()
@Inject(ACL_TAG_TRANSIENT)
private aclTagTransient$: BehaviorSubject<boolean>,
@Optional()
@Self()
@Inject(ACL_TAG_TOKEN)
private aclTag: AclTag,
@Optional() private templateRef: TemplateRef<unknown>,
@Optional() private viewContainer: ViewContainerRef,
@Inject(AclService) private aclService: AclService,
@Inject(AclInspectorService) private aclInspectorService: AclInspectorService,
private componentFactoryResolver: ComponentFactoryResolver
) {
if (!aclTag) {
throw new Error('Could not find any Acl Tag');
}
this.aclInspectorService.showAclHints$
.pipe(
takeUntil(this.destroyed$),
tap((value) => {
this.inspectorServiceOverrideShowHint = value;
this.render();
})
)
.subscribe();
}
get tagName() {
return this.tag;
}
get showHint() {
return this.hint || this.inspectorServiceOverrideShowHint;
}
ngOnInit(): void {
this.updateTagName(this.tagName);
/**
* Only in case this directive is used as a structural directive:
* Subscribe for any acl key change as the key is async by design
* to prevent any circular dependencies. See ACL_TAG_NAME_ASYNC_TOKEN & aclTagProvider()
*/
if (this.isStructuralDirective()) {
this.renderWhenAclViewGranted();
}
}
renderWhenAclViewGranted() {
this.aclTag.aclKey$
.pipe(
map((item) => {
return item.aclKey;
}),
switchMap((resourceName) => this.aclService.isHidden$(resourceName)),
tap((isHidden) => {
this.show = !isHidden;
this.render();
}),
takeUntil(this.destroyed$)
)
.subscribe();
this.render();
}
ngOnChanges(changes: SimpleChanges): void {
let renderRequired = false;
if (false === changes['tag']?.firstChange) {
this.updateTagName(changes['tag']?.currentValue);
renderRequired = true;
}
if (false === changes['transient']?.firstChange) {
this.updateTransient(changes['transient']?.currentValue);
renderRequired = true;
}
if (renderRequired) {
this.render();
}
}
renderComponent() {
if (!this.show && this.componentIsVisible) {
const index = this.viewContainer.indexOf(this.currentViewRef);
if (index !== -1) {
this.viewContainer.remove(index);
this.componentIsVisible = false;
}
} else if (this.show && !this.componentIsVisible) {
this.currentViewRef = this.viewContainer.createEmbeddedView(this.templateRef, 1);
this.componentIsVisible = true;
}
}
renderHint() {
if (!this.showHint && this.hintIsVisible) {
const index = this.viewContainer.indexOf(this.aclTagHintComponentRef.hostView);
if (index !== -1) {
this.viewContainer.remove(index);
this.hintIsVisible = false;
}
} else if (this.showHint) {
if (!this.hintIsVisible) {
this.attachAclTagHint();
this.hintIsVisible = true;
}
this.updateAclHintContentShownInput();
}
}
render() {
this.renderComponent();
this.renderHint();
}
ngOnDestroy(): void {
// complete the tag stream to notify anyone subscribed (like the AclTag instances)
this.aclTagUpdate$.complete();
this.aclTagTransient$.complete();
this.destroyed$.next();
}
private attachAclTagHint() {
this.aclTagHintComponentRef = this.viewContainer.createComponent(AclTagHintComponent, {
index: 0
});
const hintComponentInstance = this.aclTagHintComponentRef.instance;
hintComponentInstance.givenAclTag = this.aclTag;
}
private updateAclHintContentShownInput() {
const hintComponentInstance = this.aclTagHintComponentRef.instance;
/**
* the contentShown input tells the hint if some content is shown
* which we answer as truthy if the content is indeed shown
* or if the directive is not used as a structural directive
*/
hintComponentInstance.contentShown = this.show || !this.isStructuralDirective();
}
private updateTagName(value: string) {
this.aclTagUpdate$.next(value);
}
private updateTransient(transient: boolean) {
this.aclTagTransient$.next(transient);
}
private isStructuralDirective() {
return null !== this.templateRef;
}
}