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()

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 { AclTag } from '@allianz/taly-acl';
import {
  ComponentRef,
  Directive,
  EmbeddedViewRef,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  SimpleChanges,
  TemplateRef,
  ViewContainerRef,
  inject
} 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';

/**
 * 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 aclTagUpdate$ = inject<BehaviorSubject<string>>(ACL_TAG_NAME_ASYNC_TOKEN, {
    optional: true,
    self: true
  });
  private aclTagTransient$ = inject<BehaviorSubject<boolean>>(ACL_TAG_TRANSIENT, {
    optional: true,
    host: true
  });
  private aclTag = inject<AclTag>(ACL_TAG_TOKEN, { optional: true, self: true });
  private templateRef = inject<TemplateRef<unknown>>(TemplateRef, { optional: true });
  private viewContainer = inject(ViewContainerRef, { optional: true });
  private aclService = inject<AclService>(AclService);
  private aclInspectorService = inject<AclInspectorService>(AclInspectorService);

  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() {
    if (!this.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.viewContainer) {
      throw new Error('Could not find any ViewContainerRef');
    }
    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) {
      if (!this.templateRef) {
        throw new Error('Could not find any TemplateRef');
      }
      this.currentViewRef = this.viewContainer.createEmbeddedView(this.templateRef, 1);
      this.componentIsVisible = true;
    }
  }

  renderHint() {
    if (!this.viewContainer) {
      throw new Error('Could not find any ViewContainerRef');
    }
    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() {
    if (!this.viewContainer) {
      throw new Error('Could not find any ViewContainerRef');
    }
    if (!this.aclTag) {
      throw new Error('Could not find any Acl Tag');
    }
    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 ""