File

libs/acl/angular/src/lib/acl-tag-directive/acl-tag.directive.ts

Description

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

Implements

OnInit OnDestroy OnChanges

Metadata

Index

Properties
Methods
Inputs
Accessors

Constructor

constructor(aclTagUpdate$: BehaviorSubject, aclTagTransient$: BehaviorSubject, aclTag: AclTag, templateRef: TemplateRef<>, viewContainer: ViewContainerRef, aclService: AclService, aclInspectorService: AclInspectorService, componentFactoryResolver: ComponentFactoryResolver)
Parameters :
Name Type Optional
aclTagUpdate$ BehaviorSubject<string> No
aclTagTransient$ BehaviorSubject<boolean> No
aclTag AclTag No
templateRef TemplateRef<> No
viewContainer ViewContainerRef No
aclService AclService No
aclInspectorService AclInspectorService No
componentFactoryResolver ComponentFactoryResolver No

Inputs

aclTag
Type : string
aclTagHint
Type : boolean
Default value : false
aclTagTransient
Type : boolean
Default value : false

Methods

render
render()
Returns : void
renderComponent
renderComponent()
Returns : void
renderHint
renderHint()
Returns : void
renderWhenAclViewGranted
renderWhenAclViewGranted()
Returns : void

Properties

aclTagHintComponentRef
Type : ComponentRef<AclTagHintComponent>
currentViewRef
Type : EmbeddedViewRef<>
destroyed$
Default value : new Subject<void>()
inspectorServiceOverrideShowHint
Default value : false

Accessors

tagName
gettagName()
showHint
getshowHint()
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;
  }
}

results matching ""

    No results matching ""