The Long Way To Angular - Tabs 组件设计与开发

The Long Way To Angular(下文以 TLWTA代指)系列停更很久,现在考虑继续。

在我个人的工作中,还是非常愿意使用 Angular 去开发 Web 应用的,只不过主观因素和客观因素之下,实际 Angular 在我的工作中很难起到主导地位。原因我也在之前的文章中提过了,这样一个复杂而笨重的框架不管是对于我的工作还是我的同事来说,都很难成为一个可靠的选择方案。

比如我个人而言,如果是短平快的应用,我会选择 React 去开发;如果是各种管理平台,我会选择 Vue 去开发;而各种插件式的开发需求,我更愿意选择 lit-element 等 Web Components 框架去开发。以至于到现在,我还没有找到一个打定主意把 Angular 作为唯一选择的场景。

这种情况下,TLWTA 就以我平时开发过程中的各种经历和问题来展开会更好,比如 Tabs 组件。

Tabs 组件设计

Tabs(标签页)组件也算是常用网页组件之一了,它可以很方便的展示同一类内容的不同区块。 一般来说,Tabs 组件包含两个部分 Tab 标签和 Tab 内容区。以 Material Design 中的 Tabs 组件为例,大概就是下图这个样子: Material Design 中的 Tabs 组件 Tab 标签和内容是一一对应的,那么在组件设计的时候,就需要解决它们的对应关系,那么最基本的使用方式就是:

<!-- app.component.html -->
<tabs>
    <tab title="">
        <p>...</p>
    </tab>
    <tab title="">
        <p>...</p>
    </tab>
</tabs>

这样在使用组件的地方,就明确了 Tab 的结构,那么这个 Tabs 的逻辑就是:

  1. 获取所有 <tab> 的title组成标签元素;
  2. 获取所有 <tab> 的内容作为标签内容,展示当前激活的那一个; Angular 已经提供了很多方法来获取组件内部的其他组件,比如 slot 或者 <ng-content select="">,但是这两种方法的问题在于不方便处理多个重复元素,而 Tabs 组件的场景,重复的元素是要分开处理的。 因此我们选择另一种方式
// tabs.component.ts
class TabsComponent {
    @ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
}

@ContentChildren 就是做组件内部元素查询的,它需要的最基本参数就是 selector(选择器),所以直接用组件的类作为参数也是可以的,QueryList 则是它查询结果的类型。 ngAfterContentInit 的回调中,tabs 属性就已经可用了。

// tabs.component.ts
selectTab(tab) {
    this.tabs.toArray().forEach((t) => t.active = false);
    tab.active = true;
}

ngAfterContentInit() {
    const activeTabs = this.tabs.filter((tab) => tab.active);

    if (activeTabs.length === 0) {
      this.selectTab(this.tabs.first);
    }
}

通过上面的逻辑,我们就已经实现了选择某个tab的逻辑,并且默认展示了第一个tab的内容。注意 @ContentChildren 查询的结果不是直接的数组,记得使用 .toArray() 方法得到对应数组。 然后我们编写对应的 tab 标签的模版:

<!-- tabs.component.html -->
<div class="tabs">
  <div
    class="tab"
    *ngFor="let tab of tabs"
    (click)="selectTab(tab)"
    [class.active]="tab.active"
  >
    <span class="tab-text">{{tab.title}}</span>
  </div>
</div>
<ng-content></ng-content>

这里 <ng-content> 就是所有的 <tab>,所以我们只需要在 tab 组件中,根据它的 active 状态展示他就是可以了

<!-- tab.component.html -->
<div [hidden]="!active" class="pane">
  <ng-content></ng-content>
</div>

在这篇文章中,我们学到了 <ng-content>, @ContentChildren 等特性的使用。

demo: