Angular组件库的详细指南

209 阅读8分钟

Angular组件库的历史

Angular组件库的前身是Angular Material,这是Angular团队为Angular构建的一套Material Design组件。在Angular 5发布的时候,Angular Material库由大约30个材料组件组成。该团队花了近两年的时间来构建这些组件,在这个过程中,他们一直在寻找常见的模式。这些通用模式被转化为可重用的构建块,一旦足够成熟,就被重构为Angular组件开发工具包(CDK)的第一个版本。

最初版本的CDK包含一些常见的行为,包括可访问性工具、双向文本、覆盖、布局断点和表格。今天,CDK已经在这些原始模式的基础上进行了扩展,并增加了一些新的模式。

那么CDK是一个组件库吗?不是。也就是说,它是一个不断增长的指令、服务和一些组件的集合,通过解决常见问题,将大大加快你自己的Angular组件(或你自己的Angular组件库)的创建速度。在造型方面,CDK没有主见,允许开发者轻松添加自己的风格。

CDK的特点

可访问性

CDK的这一部分也被称为a11y模块,它侧重于在你的Angular组件库中提供无障碍支持,包括菜单中的键盘交互以及针对屏幕阅读器和高对比度用户的造型工具。然而,它最大的特点是聚焦实用程序。

有些时候,有必要将用户的焦点限制在屏幕的某个区域,最明显的是模态。一旦打开,模态就必须让用户受到限制,直到它被关闭。a11y模块提供了cdkTrapFocus 指令来实现这一点:

<div class="my-inner-dialog-content" cdkTrapFocus>
   <!-- Tab and Shift + Tab will not leave this element. -->
 </div>

用户如何与一个给定的元素进行交互,也可以使用焦点监视器进行跟踪,无论是通过鼠标、键盘、触摸还是应用程序本身强制焦点:

// HTML
 <div class="example-focus-monitor">
   <button cdkMonitorSubtreeFocus
           (cdkFocusChange)="elementOrigin = formatOrigin($event); markForCheck()">
     Focus Monitored Element ({{elementOrigin}})
   </button>
 </div>
 <div class="example-focus-monitor">
   <div cdkMonitorSubtreeFocus
        (cdkFocusChange)="subtreeOrigin = formatOrigin($event); markForCheck()">
     <p>Focus Monitored Subtree ({{subtreeOrigin}})</p>
     <button>Child Button 1</button>
     <button>Child Button 2</button>
   </div>
 </div>
// TS
import {FocusOrigin} from '@angular/cdk/a11y';
import {ChangeDetectorRef, Component, NgZone} from '@angular/core';

@Component({
  selector: 'focus-monitor-directives-example',
  templateUrl: 'focus-monitor-directives-example.html',
  styleUrls: ['focus-monitor-directives-example.css']
})
export class FocusMonitorDirectivesExample {
  elementOrigin = this.formatOrigin(null);
  subtreeOrigin = this.formatOrigin(null);

  constructor(private _ngZone: NgZone, private _cdr: ChangeDetectorRef) {}


  formatOrigin(origin: FocusOrigin): string {
    return origin ? origin + ' focused' : 'blurred';
  }

  // Workaround for the fact that (cdkFocusChange) emits outside NgZone.
  markForCheck() {
    this._ngZone.run(() => this._cdr.markForCheck());
  }
}

剪贴板

剪贴板 模块就是把文本复制到系统的剪贴板上,并使其尽可能的容易处理。为了实现这个目标,CDK提供了cdkCopyToClipboard 指令,该指令将一些文本作为输入,并添加一个点击处理程序来复制该文本到系统剪贴板:

<img src="avatar.jpg" alt="Hero avatar" cdkCopyToClipboard="Text to be copied">

拖放

拖放 模块提供了一套易于使用和全面的指令,以便在你的Angular组件中快速实现拖放功能。它通过cdkDrag 指令支持可自由拖动的元素,通过cdkDropList 指令支持在列表中或列表之间拖动元素。

有了它,使用cdkDropList 指令就可以快速简单地创建一个具有拖放功能的待办和已办任务列表:

// HTML
 <div cdkDropListGroup>
   <div class="example-container">
     <h2>To do</h2>
     <div cdkDropList
       [cdkDropListData]="todo" class="example-list" (cdkDropListDropped)="drop($event)">
       <div class="example-box" *ngFor="let item of todo" cdkDrag>{{item}}</div>
     </div>
   </div>
   <div class="example-container">
     <h2>Done</h2>
     <div cdkDropList
       [cdkDropListData]="done" class="example-list" (cdkDropListDropped)="drop($event)">
       <div class="example-box" *ngFor="let item of done" cdkDrag>{{item}}</div>
     </div>
   </div>
 </div>
// TS
import {Component} from '@angular/core';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';

@Component({
  selector: 'cdk-drag-drop-connected-sorting-group-example',
  templateUrl: 'cdk-drag-drop-connected-sorting-group-example.html',
  styleUrls: ['cdk-drag-drop-connected-sorting-group-example.css'],
})
export class CdkDragDropConnectedSortingGroupExample {
  todo = [
    'Get to work',
    'Pick up groceries',
    'Go home',
    'Fall asleep'
  ];

  done = [
    'Get up',
    'Brush teeth',
    'Take a shower',
    'Check e-mail',
    'Walk dog'
  ];

  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      transferArrayItem(event.previousContainer.data,
                        event.container.data,
                        event.previousIndex,
                        event.currentIndex);
    }
  }
}

版面设计

布局模块包含两个用于创建响应式布局的强大工具:BreakpointObserverMediaMatcher

BreakpointObserver允许一个组件对方向或视口大小的变化做出反应。在材料设计规范中已经定义了几个默认的断点,但你可以通过一个简单的媒体查询轻松地定义你自己的断点:

const isSmallScreen = breakpointObserver.isMatched('(max-width: 599px)');
const layoutChanges = breakpointObserver.observe([
  '(orientation: portrait)',
  '(orientation: landscape)',
]);

layoutChanges.subscribe(result => {
  updateMyLayoutForOrientationChange();
});

为了提供更好的跨浏览器支持,MediaMatcher将媒体查询中的浏览器差异标准化。BreakpointObserver在后台使用这个功能,但你也可以直接使用它。

this.mobileQuery = media.matchMedia('(max-width: 600px)');
this._mobileQueryListener = () => changeDetectorRef.detectChanges();
this.mobileQuery.addListener(this._mobileQueryListener);

叠加

覆盖模块提供了一种在屏幕上轻松创建浮动面板的方法。常见的用途是菜单、模版、日期选择器和屏幕上的通知。这个模块包括一些必要的结构性CSS,但除此之外,当涉及到风格设计时,就像CDK的其他部分一样,没有任何意见。

当使用Overlay服务或cdkConnectedOverlay 指令时,创建一个浮动面板是很容易和快速的。叠加的位置可以被设置为与原点元素相连,或相对于视口的全局定位。该模块还提供了滚动策略,以帮助处理覆盖层打开时的滚动事件,包括:关闭覆盖层、阻止页面滚动或在滚动时重新定位覆盖层:

<!-- This button triggers the overlay and is its origin -->
 <button (click)="isOpen = !isOpen" type="button" cdkOverlayOrigin #trigger="cdkOverlayOrigin">
   {{isOpen ? "Close" : "Open"}}
 </button>
 <!-- This template displays the overlay content and is connected to the button -->
 <ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOpen]="isOpen">
   <ul class="example-list">
     <li>Item 1</li>
     <li>Item 2</li>
     <li>Item 3</li>
   </ul>
 </ng-template>

平台

平台 模块允许你的Angular组件库快速确定用户当前所处的平台以及该平台支持的功能,包括支持的输入类型:

// HTML
 <h2>Platform information:</h2>
 <p>Is Android: {{platform.ANDROID}}</p>
 <p>Is iOS: {{platform.IOS}}</p>
 <p>Is Firefox: {{platform.FIREFOX}}</p>
 <p>Is Blink: {{platform.BLINK}}</p>
 <p>Is Webkit: {{platform.WEBKIT}}</p>
 <p>Is Trident: {{platform.TRIDENT}}</p>
 <p>Is Edge: {{platform.EDGE}}</p>
 <p>Is Safari: {{platform.SAFARI}}</p>
 <p>Supported input types: {{supportedInputTypes}}</p>
 <p>Supports passive event listeners: {{supportsPassiveEventListeners}}</p>
 <p>Supports scroll behavior: {{supportsScrollBehavior}}</p>
// TS
import {Component} from '@angular/core';
import {
  getSupportedInputTypes,
  Platform,
  supportsPassiveEventListeners,
  supportsScrollBehavior,
} from '@angular/cdk/platform';

@Component({
  selector: 'cdk-platform-overview-example',
  templateUrl: 'cdk-platform-overview-example.html',
})
export class CdkPlatformOverviewExample {
  supportedInputTypes = Array.from(getSupportedInputTypes()).join(', ');
  supportsPassiveEventListeners = supportsPassiveEventListeners();
  supportsScrollBehavior = supportsScrollBehavior();

  constructor(public platform: Platform) {}
}

步进器

Stepper模块提供的CdkStepper组件为创建基于步骤的工作流组件(如向导)提供了必要的构建块。它包括在多个步骤中使用一个表单或为每个步骤设置自己的表单的选项。每个步骤都可以是一个必要的步骤或可选的步骤。步进模块还提供了允许或阻止返回过去的步骤并对其进行编辑的选项:

// HTML
 <example-custom-stepper>
   <cdk-step> <p>This is any content of "Step 1"</p> </cdk-step>
   <cdk-step> <p>This is any content of "Step 2"</p> </cdk-step>
 </example-custom-stepper>
 // Component HTML
 <section class="example-container">
   <header>
     <h2>Step {{ selectedIndex + 1 }}/{{ steps.length }}</h2>
   </header>
 <div [ngTemplateOutlet]="selected ? selected.content : null"></div>
 <footer class="example-step-navigation-bar">
     <button class="example-nav-button" cdkStepperPrevious></button>
     <button class="example-step" [class.example-active]="selectedIndex === i" *ngFor="let step of steps; let i = index" (click)="selectStepByIndex(i)">
       Step {{ i + 1 }}
     </button>
     <button class="example-nav-button" cdkStepperNext></button>
   </footer>
 </section>
// TS
import {Component} from '@angular/core';
import {CdkStepper} from '@angular/cdk/stepper';

/** Custom CDK stepper component */
@Component({
  selector: 'example-custom-stepper',
  templateUrl: './example-custom-stepper.html',
  styleUrls: ['./example-custom-stepper.css'],
  providers: [{provide: CdkStepper, useExisting: CustomStepper}]
})
export class CustomStepper extends CdkStepper {
  selectStepByIndex(index: number): void {
    this.selectedIndex = index;
  }
}

表格

表格模块为应用程序面临的最常见的问题之一提供解决方案:在表格或网格中显示数据。无论你只是需要一个带有一些标题和行的简单表格,还是一个带有排序、分页或动态列的复杂表格,模块都提供了必要的基础知识来快速设置。

你可以用cdk-table 指令来设置传统的表格,或者完全抛弃table 标签,直接使用cdk-table 组件,它将使用display flex。然后你只需为每一列设置一个模板,指出哪些列应该被显示,并提供dataSource

dataSource 是一个简单的类,它提供了一个连接函数,返回数据的可观察性。然后你可以扩展该类,以包括排序、分页、过滤以及你的表格所需的任何其他功能。Angular Material库提供了一个非常彻底的数据源,它与自己的组件绑定在一起,但可以提供关于如何为自己的组件建立排序、分页和过滤的见解:

// HTML
 <cdk-table [dataSource]="dataSource">
   <!-- Position Column -->
   <ng-container cdkColumnDef="position">
     <cdk-header-cell *cdkHeaderCellDef> No. </cdk-header-cell>
     <cdk-cell *cdkCellDef="let element"> {{element.position}} </cdk-cell>
   </ng-container>
 <!-- Name Column -->
   <ng-container cdkColumnDef="name">
     <cdk-header-cell *cdkHeaderCellDef> Name </cdk-header-cell>
     <cdk-cell *cdkCellDef="let element"> {{element.name}} </cdk-cell>
   </ng-container>
 <!-- Weight Column -->
   <ng-container cdkColumnDef="weight">
     <cdk-header-cell *cdkHeaderCellDef> Weight </cdk-header-cell>
     <cdk-cell *cdkCellDef="let element"> {{element.weight}} </cdk-cell>
   </ng-container>
 <!-- Symbol Column -->
   <ng-container cdkColumnDef="symbol">
     <cdk-header-cell *cdkHeaderCellDef> Symbol </cdk-header-cell>
     <cdk-cell *cdkCellDef="let element"> {{element.symbol}} </cdk-cell>
   </ng-container>
 <cdk-header-row *cdkHeaderRowDef="displayedColumns"></cdk-header-row>
   <cdk-row *cdkRowDef="let row; columns: displayedColumns;"></cdk-row>
 </cdk-table>
// TS
import {DataSource} from '@angular/cdk/collections';
import {Component} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';

export interface PeriodicElement {
  name: string;
  position: number;
  symbol: string;
  weight: number;
}

const ELEMENT_DATA: PeriodicElement[] = [
  {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
  {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
  {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
  {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
  {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
  {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
  {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];

@Component({
  selector: 'cdk-table-flex-basic-example',
  styleUrls: ['cdk-table-flex-basic-example.css'],
  templateUrl: 'cdk-table-flex-basic-example.html',
})
export class CdkTableFlexBasicExample {
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = new ExampleDataSource();
}

/**
 * Data source to provide what data should be rendered in the table. Note that the data source
 * can retrieve its data in any way. In this case, the data source is provided a reference
 * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
 * the underlying data. Instead, it only needs to take the data and send the table exactly what
 * should be rendered.
 */
export class ExampleDataSource extends DataSource<PeriodicElement> {
  /** Stream of data that is provided to the table. */
  data = new BehaviorSubject<PeriodicElement[]>(ELEMENT_DATA);

  /** Connect function called by the table to retrieve one stream containing the data to render. */
  connect(): Observable<PeriodicElement[]> {
    return this.data;
  }

  disconnect() {}
}

组件线束

相对于CDK来说,测试模块提供了一个创建组件线束的模板,以帮助测试,防止选择器改变时破坏测试。这个话题太大,无法在本文中涵盖。我们将在未来的博文《简化Angular单元测试编写》中介绍。

文本字段

文本字段 模块提供了几个处理文本输入字段的实用工具。

有一个cdkTextareaAutosize 指令,它将根据内容自动改变textarea 中的行数。调整大小也可以手动触发。这很有用,例如,当造型影响到textarea 内容的大小时。

<textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="1" cdkAutosizeMaxRows="5"></textarea>

文本字段模块还提供了cdkAutofill 指令,以监测和反应一个输入的自动填充状态。它还包括一些CSS混合器,用于在自动填充状态改变时创建动画钩子,并对自动填充的颜色进行设计。

总结

无论你是为你的应用程序创建一个单一的Angular组件还是整个Angular组件库,你都应该从检查Angular CDK开始,看看他们是否已经有一个指令、服务或组件来解决你的问题。这样做会大大加快你的开发过程。CDK的文档很全面,而且在Angular组件库的另一半Angular Material中,有更多CDK的实际应用案例。阅读更多关于Angular开发的信息,看看这个框架是否适合你的下一个项目。