angular - 保留覆盖状态

angular - Preserve state of overlay

我一直在为 angular 构建一个组件库,其中包含手风琴和 offcanvas 组件。我已经 created a minimal StackBlitz 演示了。

现在我希望能够在侧边栏中保留手风琴的展开状态。目前,当显示侧边栏并展开多个手风琴时,当您关闭并重新打开侧边栏时,所有手风琴都会再次折叠。

offcanvas 显示 following code snippet:

const injector = Injector.create({
  providers: [{ provide: 'OFFCANVAS_CONTENT', useValue: template }],
  parent: this.parentInjector
});

// Portal which will hold the offcanvas component
const portal = new ComponentPortal(BsOffcanvasComponent, null, injector);

// Create overlay to the left side of the screen
const overlay = this.overlayService.create({
  scrollStrategy: this.overlayService.scrollStrategies.reposition(),
  positionStrategy: this.overlayService.position().global()
    .centerVertically().left('0'),
  height: '100%',
  hasBackdrop: true
});

// Instantiate the offcanvas. This will resolve the provider,
// and render the template in the bootstrap 5 offcanvas
const componentInstance = overlay.attach<BsOffcanvasComponent>(portal);

代码呈现通过 OffcanvasComponent with the following template 中的 OFFCANVAS_CONTENT 提供程序传递的模板:

<div class="offcanvas overflow-hidden" [class]="offcanvasClass$ | async"
     [class.show]="show$ | async"
     [class.h-100]="offcanvasHeight100$ | async"
     [style.height.px]="size"
     [class.oc-max-width]="['start', 'end'].includes(position)"
     [class.oc-max-height]="['top', 'bottom'].includes(position)">
    <ng-container *ngTemplateOutlet="content; context: { $implicit: this }" ></ng-container>
</div>

在示例中,我在叠加层的模板中渲染 AccordionComponent

<body>
  <button (click)="toggleSidebar(sidebarTemplate)">Toggle sidebar</button>
  <ng-template #sidebarTemplate let-offcanvas>
    <app-sidebar></app-sidebar>
  </ng-template>
</body>

但是由于每次显示 offcanvas 时我都基于 TemplateRef 创建一个新的组件实例,侧边栏内组件的状态再次丢失。

我该如何避免这种情况?在 CDK overlay 中渲染组件实例似乎是不可能的,你只能在 CDK overlay 中渲染模板。

通过创建 2 个指令解决此问题,这些指令将活动选项卡保留在 AppComponent 上的一个字段中。

from-overlay

@Directive({
  selector: 'bs-accordion[bsFromOverlay]'
})
export class BsFromOverlayDirective implements AfterContentInit, OnDestroy {

  constructor(private accordion: BsAccordionComponent) {
    this.accordion.disableAnimations = true;
    combineLatest([this.inited$, this.activeOverlayIdentifier$])
      .pipe(filter(([inited, activeOverlayIdentifier]) => inited))
      .pipe(takeUntil(this.destroyed$))
      .subscribe(([inited, activeOverlayIdentifier]) => {
        this.bsFromOverlayChange.emit(activeOverlayIdentifier);
        this.accordion.tabPages.forEach((tab) => {
          tab.isActive = (tab["tabOverlayIdentifier"] == activeOverlayIdentifier);
        });
        setTimeout(() => this.accordion.disableAnimations = false, 30);
      });
  }

  private readonly inited$ = new BehaviorSubject<boolean>(false);
  private readonly destroyed$ = new Subject();
  private readonly activeOverlayIdentifier$ = new BehaviorSubject<string | null>(null);

  @Output() public bsFromOverlayChange = new EventEmitter<string | null>();
  private _bsFromOverlay: string | null = null;

  /** Binds the active tab of an accordion to a field, in case the accordion is rendered in an overlay. */
  public get bsFromOverlay() {
    return this._bsFromOverlay;
  }
  @Input() public set bsFromOverlay(value: string | null) {
    if (this._bsFromOverlay != value) {
      this._bsFromOverlay = value;
      this.activeOverlayIdentifier$.next(value);
    }
  }

  @ContentChildren(BsFromOverlayIdDirective, { read: ElementRef }) tabPages!: QueryList<ElementRef<BsAccordionTabComponent>>;
  
  ngAfterContentInit() {
    this.inited$.next(true);
  }
  ngOnDestroy() {
    this.destroyed$.next(true);
  }
}

from-overlay-id

@Directive({
  selector: 'bs-accordion-tab[bsFromOverlayId]'
})
export class BsFromOverlayIdDirective implements OnDestroy {
  constructor(private accordionTab: BsAccordionTabComponent, private bsFromOverlay: BsFromOverlayDirective) {
    this.accordionTab.isActiveChange
      .pipe(takeUntil(this.destroyed$))
      .subscribe((isActive) => {
        if (isActive) {
          bsFromOverlay.bsFromOverlay = this.bsFromOverlayId;
        } else {
          bsFromOverlay.bsFromOverlay = null;
        }
      });
  }

  private destroyed$ = new Subject();
  ngOnDestroy() {
    this.destroyed$.next(true);
  }

  //#region bsFromOverlayId
  private _bsFromOverlayId!: string;
  /** String value containing the accordion tab identifier. */
  public get bsFromOverlayId() {
    return this._bsFromOverlayId;
  }
  @Input() public set bsFromOverlayId(value: string) {
    this._bsFromOverlayId = value;
    this.accordionTab['tabOverlayIdentifier'] = value;
  }
  //#endregion
}