Javascript is required
Mokkapps Logo

How I Built A Custom Stepper/Wizard Component Using The Angular Material CDK

Michael Hoffmann (Mokkapps) - Senior Frontend Developer (Freelancer)
Dec 25, 2018
5 min read
|
3877 views
Development
Angular
Frontend
How I Built A Custom Stepper/Wizard Component Using The Angular Material CDK Image

Update 12.02.2018: Meanwhile, I have created a PR to the Angular Material repository and added there an official guide

I recently had to refactor a quite complex legacy Angular component and want to share my experiences with you.

The Legacy Component

The component should look this way per design:

Angular Legacy Component Design

You can freely navigate through the content by clicking either the navigation arrows or clicking on a certain step in the navigation area at the bottom.

The HTML template of the legacy component looked similar to this simple example:

<div *ngIf="isContent1 && !isContent2 && !isFinished && !noError">  Content 1</div><div *ngIf="!isContent1 && isContent2 && !isFinished && !noError">  Content 2</div>

So basically, there was a div container for each content page with multiple *ngIf statements. As only one content container could be visible per time, the *ngIf directives controlled their visibility.

Maybe at first glance, this sounds not that bad for you, but this approach had some significant problems:

  • It contained large and confusing ngIf statements.
  • Accessibility was not considered.
  • Keyboard interactions were not supported by default.
  • Managing which state is active had to be implemented manually.
  • It is a custom solution that needs to be tested.
  • It provided a non-scalable component architecture.

Additionally, we got a new requirement: It should be possible to switch between the content pages linearly. That means going from the first content page to the second content page should only be possible if the first page is completed, and going backward should not be allowed.

Refactoring

To fulfill the new requirement, I started research for existing components that provide a similar logic and found, for example, Angular Archwizard.

This excellent component also worked fine with the latest Angular version, but I could not easily modify the styling for our design requirements.

So I continued my research and stumbled upon the Angular Material CDK Stepper, which was exactly what I was looking for.

Angular Material CDK

On the official website, they describe the Component Dev Kit (CDK) as:

The Component Dev Kit (CDK) is a set of tools that implement common interaction patterns whilst being unopinionated about their presentation. It represents an abstraction of the core functionalities found in the Angular Material library, without any styling specific to Material Design. Think of the CDK as a blank state of well-tested functionality upon which you can develop your own bespoke components.

The CDK is divided into two parts: "Common Behaviors" and "Components".

Common Behaviors

Tools for implementing common application features

This is a list of common behaviors provided by the CDK:

Angular Material Common Behaviors

Components

Unstyled components with useful functionality

The following image shows the list of components provided by the CDK:

Angular Material Components

CDK Stepper

The CdkStepper was exactly what I was looking for: A well-tested stepper functionality that I can design however I want to. It consists of a CdkStep used to manage the state of each step in the stepper and the CdkStepper, which contains the steps (CdkStep) and primarily handles which step is active.

Getting Started

It is straightforward to add the CDK to your Angular project:

bash
npm install --save @angular/cdk

Or alternatively for Yarn:

bash
yarn add @angular/cdk

You also need to add the CdkStepperModule to your Angular module:

import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { CdkStepperModule } from '@angular/cdk/stepper'; // this is the relevant importantimport { AppComponent } from './app.component';@NgModule({  imports: [BrowserModule, CdkStepperModule], // add the module to your imports  declarations: [AppComponent],  bootstrap: [AppComponent],})export class AppModule {}

Demo Stepper Project

As the official documentation does not provide any code examples, I created a simple demo project on Stackblitz which I want to describe in the following sections.

Create CustomStepperComponent

The first step was to create a new Angular component for the CdkStepper to be able to modify it. Therefore, the component needs to extend from CdkStepper. The following example is a minimal implementation of a custom CDK stepper component:

import { Directionality } from '@angular/cdk/bidi';import { ChangeDetectorRef, Component } from '@angular/core';import { CdkStepper } from '@angular/cdk/stepper';@Component({  selector: 'app-custom-stepper',  templateUrl: './custom-stepper.component.html',  styleUrls: ['./custom-stepper.component.css'],  providers: [{ provide: CdkStepper, useExisting: CustomStepperComponent }],})export class CustomStepperComponent extends CdkStepper {  constructor(dir: Directionality, changeDetectorRef: ChangeDetectorRef) {    super(dir, changeDetectorRef);  }    onClick(index: number): void {    this.selectedIndex = index;  }}

The HTML template for this basic component:

<ng-container [ngTemplateOutlet]="selected.content"></ng-container><!-- This is the navigation bar of the stepper --><footer>  <button cdkStepperPrevious>Previous</button>  <button *ngFor="let step of _steps; let i = index;" (click)="onClick(i)">Step {{i + 1}}</button>  <button cdkStepperNext>Next</button></footer>

We can now use our new CustomStepperComponent in another component:

<app-custom-stepper>  <cdk-step>    <input type="text" name="a" value="a" />  </cdk-step>  <cdk-step>    <input type="text" name="b" value="b" />  </cdk-step></app-custom-stepper>

You must wrap each step inside a <cdk-step> tag. For multiple steps, you can of course, use *ngFor and use your custom step component inside:

<app-custom-stepper>  <cdk-step *ngFor="let step of mySteps; let stepIndex = index">    <my-step-component [step]="step"></my-step-component>  </cdk-step></app-custom-stepper>

Linear Mode

The above example allowed the user to navigate between all steps freely. The CdkStepper additionally provides the linear mode, which requires the user to complete previous steps before proceeding.

You can either use a single form for the entire stepper or a form for each step to validate if a step is completed. Alternatively, you can pass the completed property to each step and set the property value depending on your logic without using a form.

A simple example without using forms could look this way:

<app-custom-stepper linear>  <cdk-step editable="false" [completed]="completed">    <input type="text" name="a" value="Cannot proceed to next step" />    <button (click)="completeStep()">Complete step</button>  </cdk-step>  <cdk-step editable="false">    <input type="text" name="b" value="b" />  </cdk-step></app-custom-stepper>
export class MyComponent {  completed = false;  completeStep(): void {    this.completed = true;  }}

The steps are marked as editable="false" which means that the user cannot return to this step once it has been marked as completed. It is impossible to navigate to the second step until the first one has been completed by clicking the Complete Step button.

If you are then on step 2 it is impossible to navigate back to step 1.

Conclusion

I am pleased with the CdkStepper, and it provided all the functionality I needed to refactor my legacy component. It was not necessary to write tests for this logic, and it automatically includes keyboard interaction support and cares about accessibility.

My advice is: If you ever need to implement a common behavior or component logic for your Angular application, please first look at the Angular Material CDK or similar libraries. Do not implement them yourself, as you will never get the same level of quality as from a maintained, widely-used open-source project like Angular Material.