[angular] CDK之accordion 手风琴

163 阅读2分钟

手风琴是具有一个或多个可展开分区的组件。 CDK accordion提供了一个基础,你可以在此基础上构建自己的自定义手风琴组件。 CDK 手风琴为手风琴交互模式提供了逻辑,没有任何样式。你可以根据其能力一次封闭成自己想要的手风琴的外观

CdkAccordion

import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { Directive, InjectionToken, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { Subject } from 'rxjs';

/** 用于为每个手风琴生成唯一的ID */
let nextId = 0;

/**
 * 用来引用' CdkAccordion '的实例
 */
export const CDK_ACCORDION = new InjectionToken<CdkAccordion>('CdkAccordion');

/**
 * 手风琴指令 父级对象,用来管理CdkAccordionItem子对象的状态.
 */
@Directive({
  selector: 'cdk-accordion, [cdkAccordion]',
  exportAs: 'cdkAccordion',
  providers: [{ provide: CDK_ACCORDION, useExisting: CdkAccordion }],
})
export class CdkAccordion implements OnDestroy, OnChanges {
  /** 当 CdkAccordion 状态更改是发出*/
  readonly _stateChanges = new Subject<SimpleChanges>();

  /** 当 openAll/closeAll 被触发时 发送其状态. */
  readonly _openCloseAllActions: Subject<boolean> = new Subject<boolean>();

  /** 唯一的id. 用于区分父级*/
  readonly id: string = `cdk-accordion-${nextId++}`;

  /** 是否允许多个展开的手风琴项. */
  @Input()
  get multi(): boolean {
    return this._multi;
  }
  set multi(multi: BooleanInput) {
    this._multi = coerceBooleanProperty(multi);
  }
  private _multi: boolean = false;

  /** 当multi为true时accordion中展开所有已启用的CdkAccordionItem */
  openAll(): void {
    if (this._multi) {
      this._openCloseAllActions.next(true);
    }
  }

  /** 在accordion中关闭所有CdkAccordionItem */
  closeAll(): void {
    this._openCloseAllActions.next(false);
  }

  ngOnChanges(changes: SimpleChanges) {
    this._stateChanges.next(changes);
  }

  ngOnDestroy() {
    this._stateChanges.complete();
    this._openCloseAllActions.complete();
  }
}

CDKAccordion是一个父容器,提供一个统一管理CDKAccordionItem指令中控中心

CdkAccordionItem

import {
  Output,
  Directive,
  EventEmitter,
  Input,
  OnDestroy,
  Optional,
  ChangeDetectorRef,
  SkipSelf,
  Inject,
} from '@angular/core';
import { UniqueSelectionDispatcher } from '@angular/cdk/collections';
import { CDK_ACCORDION, CdkAccordion } from './accordion';
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subscription } from 'rxjs';

/** 为每一个accordion item 生成唯一id. */
let nextId = 0;

/**
 * 事件和属性需要由CdkAccordion父节点管理
 */
@Directive({
  selector: 'cdk-accordion-item, [cdkAccordionItem]',
  exportAs: 'cdkAccordionItem',
  providers: [
    // 提供' CDK_ACCORDION '作为未定义的,以防止嵌套的cdkAccordionItem
    // 让其注册到相同的accordion
    { provide: CDK_ACCORDION, useValue: undefined },
  ],
})
export class CdkAccordionItem implements OnDestroy {
  /** 订阅父级的_openCloseAllActions事件. */
  private _openCloseAllSubscription = Subscription.EMPTY;
  /** 每次AccordionItem关闭时触发的事件. */
  @Output() readonly closed: EventEmitter<void> = new EventEmitter<void>();
  /** 每次AccordionItem打开时触发的事件. */
  @Output() readonly opened: EventEmitter<void> = new EventEmitter<void>();
  /** 当AccordionItem被销毁时触发的事件. */
  @Output() readonly destroyed: EventEmitter<void> = new EventEmitter<void>();

  /**
   * 当 属性 expanded 状态更改时触发的事件
   */
  @Output() readonly expandedChange: EventEmitter<boolean> = new EventEmitter<boolean>();

  /** 指令的唯一id. */
  readonly id: string = `cdk-accordion-child-${nextId++}`;

  /** AccordionItem 展开状态 true 为展开. */
  @Input()
  get expanded(): boolean {
    return this._expanded;
  }
  set expanded(expanded: BooleanInput) {
    expanded = coerceBooleanProperty(expanded);

    // 当expanded状态更改才更新状态,触发状态更改事件.
    if (this._expanded !== expanded) {
      this._expanded = expanded;
      this.expandedChange.emit(expanded);

      if (expanded) {
        this.opened.emit();
        // 派发 当前和父级id,通知所有监听订阅的事件,用于在父级只允许一个展开一个cdkAccordionItem时,打开当前 关闭其它cdkAccordionItem
        const accordionId = this.accordion ? this.accordion.id : this.id;
        this._expansionDispatcher.notify(this.id, accordionId);
      } else {
        this.closed.emit();
      }
      this._changeDetectorRef.markForCheck();
    }
  }
  private _expanded = false;

  /** AccordionItem是否被禁用. */
  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(disabled: BooleanInput) {
    this._disabled = coerceBooleanProperty(disabled);
  }
  private _disabled = false;

  /** 用于取消_expansionDispatcher的注册的函数. */
  private _removeUniqueSelectionListener: () => void = () => {};

  constructor(
    // 获取相同的父级引用
    @Optional() @Inject(CDK_ACCORDION) @SkipSelf() public accordion: CdkAccordion,
    private _changeDetectorRef: ChangeDetectorRef,
    protected _expansionDispatcher: UniqueSelectionDispatcher,
  ) {
    this._removeUniqueSelectionListener = _expansionDispatcher.listen((id: string, accordionId: string) => {
      // 关闭父级其它cdkAccordionItem
      if (this.accordion && !this.accordion.multi && this.accordion.id === accordionId && this.id !== id) {
        this.expanded = false;
      }
    });

    // 当一个AccordionItem托管在一个accordion中时,订阅打开/关闭事件.
    if (this.accordion) {
      this._openCloseAllSubscription = this._subscribeToOpenCloseAllActions();
    }
  }

  /** 销毁accordionItemt每个事件. */
  ngOnDestroy() {
    this.opened.complete();
    this.closed.complete();
    this.destroyed.emit();
    this.destroyed.complete();
    this._removeUniqueSelectionListener();
    this._openCloseAllSubscription.unsubscribe();
  }

  /** 切换展开状态. */
  toggle(): void {
    if (!this.disabled) {
      this.expanded = !this.expanded;
    }
  }

  /** 设置展开状态为false. */
  close(): void {
    if (!this.disabled) {
      this.expanded = false;
    }
  }

  /** 设置展开状态为true. */
  open(): void {
    if (!this.disabled) {
      this.expanded = true;
    }
  }

  private _subscribeToOpenCloseAllActions(): Subscription {
    return this.accordion._openCloseAllActions.subscribe(expanded => {
      // 用于子级的全部展开或关闭
      if (!this.disabled) {
        this.expanded = expanded;
      }
    });
  }
}

accordionItem单独的状态管理,每个accordionItem之间的联动通过accordion托管状态更改