History of the Angular Components Library

The Angular Components library started its life as Angular Material, a set of Material Design components built for Angular by the Angular team. Released around the time of Angular 5, the Angular Material library consisted of some 30 material components. The team had spent nearly two years building the components and during that process had been on the lookout for common patterns. These common patterns were turned into reusable building blocks and, once mature enough, were refactored into the first version of the Angular Component Development Kit (CDK).

The initial version of the CDK contained some common behaviors including accessibility utilities, bidirectional text, overlays, layout breakpoints, and tables. Today the CDK has expanded on those original patterns and added several more.

So is the CDK a component library? No. That said, it is an ever-growing collection of directives, services, and some components that will greatly speed up the creation of your own Angular components (or your own Angular component library) by solving common problems. The CDK is not opinionated when it comes to styling, allowing developers to easily add their own styles.

CDK Features

Accessibility

Also known as the a11y module, this section of the CDK focuses on accessibility support within your Angular component library, including keyboard interactions in menus and styling utilities for screen readers and high contrast users. However, its biggest feature is the focus utilities.

There are times when it is necessary to restrict a user’s focus to a certain region of the screen, most notably with modals. Once open the modal must keep the user restrained with it until it is closed. The a11y module provides the cdkTrapFocus directive for doing just that.

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

How a user interacts with a given element can also be tracked using the focus monitor, whether it is via mouse, keyboard, touch or if the application itself forces the focus.

// 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());
  }
}

Clipboard

The clipboard module is all about copying text to the system clipboard and making it as easy to handle as possible. To achieve this the CDK provides the cdkCopyToClipboard directive that takes some text as input and adds a click handler to copy that text to the system clipboard.

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

Drag and Drop

The drag and drop module provides an easy-to-use and comprehensive set of directives to quickly enable drag and drop functionality in your Angular components. It supports freely draggable elements via the cdkDrag directive and dragging elements in or between lists via cdkDropList directive.

With this, it is quick and simple to create a To Do and Done task list with drag and drop functionality using the cdkDropList directive.

// 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);
    }
  }
}

Layout

The layout module contains two powerful utilities for creating responsive layouts, the BreakpointObserver and the MediaMatcher.

The BreakpointObserver allows a component to react to changes in orientation or viewport size. There are several default breakpoints already defined per the material design specifications but you can easily define your own with a simple media query.

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

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

To provide better support across browsers, the MediaMatcher normalizes the browser differences in media queries. The BreakpointObserver uses this under the hood, but you can also use it directly.

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

Overlay

The overlay module provides a method for easily creating floating panels on the screen. Common uses would be for menus, modals, date pickers, and on-screen notifications. This module includes some necessary structural CSS, but beyond that remains unopinionated when it comes to styling just like the rest of the CDK.

When using either the Overlay service or the cdkConnectedOverlay directive, it is easy and quick to create a floating panel. The position of the overlay can be set so it is connected to an origin element or globally positioned relative to the viewport. The module also provides scrolling strategies to help handle scrolling events while the overlay is open, including: closing the overlay, blocking page scrolling or repositioning the overlay on scroll.

<!-- 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>

Platform

The platform module allows your Angular component library to quickly determine what platform a user is currently on and what features the platform supports, including the input types supported.

// 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

The CdkStepper component, provided by the Stepper module, provides the necessary building blocks to create step-based workflow components like a wizard. It includes options to use one form across multiple steps or set each step up with its own form. Each step can either be a required step or optional steps. The Stepper module also provides the option to either allow or block going back past steps and editing them.

// 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;
  }
}

Table

The Table module provides solutions for one of the most common problems applications face: displaying data in tables or grids. Whether you just need a simple table with some headers and rows or a complex one with sorting, paging, or dynamic columns, the Table module provides the basics necessary to get that set up quickly.

You can set up traditional tables with the cdk-table directive or ditch the table tag entirely and directly use the cdk-table component, which will use display flex. Then you simply set up a template for each column, indicate which columns should be displayed, and provide the dataSource

The dataSource is a simple class that provides a connect function that returns an observable of the data. You can then extend that class to include sorting, pagination, filtering, and any other features needed by your table. The Angular Material library provides a very thorough data source, which is tied to its own components but can provide insight into how to build sorting, pagination, and filtering for your own components.

// 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() {}
}

Component Harnesses

Relatively new to the CDK, the Testing module provides a template for creating component harnesses to aid in testing and prevent breaking tests when selectors change. This topic is too large to cover in this article. We will cover this in a future blog post, Simplify Angular Unit Test Authoring.

Text Fields

The Text Field module provides several utilities for dealing with text input fields.

There is the cdkTextareaAutosize directive that will automatically change the number of rows in a textarea based on the content. The resize can also be manually triggered. This is useful, for example, when styling affects the size of the textarea contents.

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

The Text Field module also provides the cdkAutofill directive to monitor and react to the autofill state of an input. It also includes some CSS mixins to create animation hooks when the autofill state changes and to style the autofill color.

Conclusion

Whether you are creating a single Angular component or an entire Angular component library for your application, you should always start by checking the Angular CDK to see whether they already have a directive, service, or component that solves your problem. Doing so will greatly speed up your development process. The documentation for the CDK is thorough and there are even more examples of the CDK in action in the other half of the Angular Components library, Angular Material. Read more about Angular development to see if this framework is a good fit for your next project.