In this article, we will learn how to build a stepper component using Angular CDK, just like the one in Angular Material. While it will function just like the Material Stepper Component, we will be using our theme so we can customize how it looks and feels.

Angular's Component Development Kit (CDK) is a set of tools that implements common interaction patterns while being unopinionated about the UI. It is used as the building block for most angular UI frameworks like Angular Material.

For this article, we will be using Bulma CSS Framework for styling, feel free to replace this with your custom CSS styles or any CSS framework.

Installing & Setting up Angular CDK#

First, we will use ng add to install angular CDK using the default package manager.

ng add @angular/cdk
You can use your favorite package manager to install @angular/cdk package – ng add in this case does not do anything.

Then, we will import CdkStepperModule from @angular/cdk/stepper in our app module.

import { CdkStepperModule } from '@angular/cdk/stepper';
// other imports here
…
@NgModule({
 declarations: [AppComponent],
 imports: [
  …
  CdkStepperModule,
  …
 ],
 providers: [],
 bootstrap: [AppComponent],
})
…

Building the Stepper Component#

We will start by generating a new component, which we will creatively name stepper-component.

ng g c stepper/stepper-component

Component Class#

Next, we will extend the CdkStepper class with our new Stepper Component. This allows our custom stepper component to inherit all the properties and methods from the CdkStepper class. These methods and properties are necessary to make our stepper component work.

Please note, we will only inherit properties and methods from CdkStepper and not the template:

@Component({
 selector: 'app-my-stepper',
 templateUrl: './my-stepper.component.html',
 styleUrls: ['./my-stepper.component.scss'],
})

export class MyStepperComponent extends CdkStepper {
  ...
}

You can also add custom props to your custom stepper component, just like you would in any other angular component. This gives you the ability to tinker with the stepper component appearance if it is used in multiple places within your app. For instance, if you wanted to override the default current step tab CSS class, you can add an activeClass props for that, as shown below:

export class MyStepperComponent extends CdkStepper {
 @Input()
  activeClass = 'active';
}

Providing the Component#

We will make our custom stepper component provide itself as a CdkStepper. This allows other components in our angular app to recognize our custom stepper component as a CdkStepper:

@Component({
 selector: 'app-my-stepper',
 templateUrl: './my-stepper.component.html',
 styleUrls: ['./my-stepper.component.scss'],
 providers: [{ provide: CdkStepper, useExisting: MyStepperComponent }],
})
export class MyStepperComponent extends CdkStepper {
 // rest of code here
}

Template#

We will have two sections for Our stepper component – a header and a body.

Stepper Header#

The header section will be for navigation purposes – showing all the steps we currently have and highlighting the current step. To achieve this, we are going to loop over the steps and use the step labels for the header label.

The steps property and any other properties that are not defined in our component are inherited from the CdkStepper class:

<header class="header">
  <ol>
   <ng-container *ngFor="let step of steps; let i = index;">
    <li>
      <a >
      <!-- label here -->
     </a>
    </li>
  </ng-container>
 </ol>
</header>

For our header’s label, CDK Stepper supports two ways to provide it:

  • a label props which is plain text, and
  • a cdkStepLabel directive, a template you can use to add richer labels such as icons and styling. To use the cdkStepLabel directive, you just need to add the directive to a template, inside a step, as shown below:
<cdk-step>
  <ng-template cdkStepLabel>
    <!-- Label Content Here -->
  </ng-template>
  <!-- Step content here -->
</cdk-step>
Using cdkStepLabel directive

For this article, we will support both, giving our stepper versatility. Feel free to support just one, nothing is wrong with that. We will give the stepLabel directive priority though, so if the user provides both, we will display the template version, because it is richer. To support both, we will first check if steps’ stepLabel property is defined, then display its content using ngTemplateOutlet and default to steps’ label property if it is not defined:

<ng-container *ngIf="step.stepLabel; else showLabelText" [ngTemplateOutlet]="step.stepLabel.template">
</ng-container>
<ng-template #showLabelText>
 {{ step.label }}
</ng-template>

To highlight the current step label, we will check if the current index equals to the current position index of loop of steps.

<li [ngClass]="{'active': selectedIndex === i}">
</li>

We will also make it possible for users to navigate via our header by clicking on the label of the step. We can achieve this by setting the selected index as the index of our current step.

<a (click)="selectedIndex = i">
 <!-- label here -->
</a>

Stepper Body#

The body will hold the content of the current/selected step. First, we will start by setting a container for our body, for styling purposes.

<div > <!-- Add your styling here -->
</div>

Then, in our body container, we will project the content of the current step. We will use ngTemplateOutlet to embed the content from the currently selected step.

<ng-container [ngTemplateOutlet]="selected.content">
</ng-container>

I have stripped out the class names and icons, to make it easier on the eye.

Using the Stepper Component#

Our stepper component is now ready for use inside our angular app. First, inside another component, just add the tags of our stepper component.

<app-my-stepper #cdkStepper>
 <!-- steps in here -->
</app-my-stepper>

For us to be able to refer to the stepper we are creating, we have given it a template reference variable #cdkStepper. This makes it easy to refer to the stepper anywhere within the component. For instance, we can control the stepper from the components’ class. To achieve this, we will use the ViewChild decorator to query the view for the stepper, and assign it to a property of the component:

@ViewChild('cdkStepper')
cdkStepper: CdkStepper;

Then, in one of your methods, you can move to the next step, like this:

this.cdkStepper.next()

The CDKStepper class comes with several optional props that you can pass to the custom stepper component. Here are some of the important ones:

  • linear (boolean) – requires the last step to be complete before proceeding to the next, i.e. a form has valid inputs.
  • selected (cdkStep) – the step that is selected.
  • selectedIndex (number) – the index of the step that is selected, an alternative to the selected input.
  • selectionChange (method) – an event emitted whenever the selected steps change.

You can find all the props here. Remember to add any custom props you created.

Then, we can add the steps for our stepper component as shown below:

<app-my-stepper #cdkStepper>
 <cdk-step >
  <!-- content here -->
 </cdk-step>
</app-my-stepper>

For the cdk-step props to use, please refer to the API Reference here, but here are a few notable ones:

stepControl – provide a form control that can be validated before proceeding to the next step. The linear mode must be enabled on the stepper for this to work.

editable – when set to false, it prevents the user from navigating back to a step once they have moved to the next step.

optional – whether the completion of a step is required. It works in tandem with linear mode, so you can have some option steps.

Next and Previous Buttons#

CDK Stepper provides two directives – cdkStepperNext and cdkStepperPrevious – which you can add to your next and previous buttons to add the ability to navigate forward and backward:

<!-- Previous Button -->
<button cdkStepperPrevious>
 Back
</button>

<!-- Next Button -->
<button cdkStepperNext>
 Next
</button>

You can also control the stepper programmatically instead of using the directives. This is great as it gives you the freedom to do something else like saving a form before moving to the next step. This can be achieved by using the template variable cdkStepper, which we added earlier and refers to our stepper.

<button (click)="cdkStepper.next()">
 Next
</button>

<button (click)="cdkStepper.previous()">
 Previous
</button>

What about Labels?#

There are two ways of adding labels, the first one is using the label component props. This accepts plain text only labels.

<cdk-step label="Personal Details" [stepControl]="frmDetails" [optional]="false">
 <!-- content here -->
</cdk-step>

While the second one involves using a template with the cdkStepLabel directive. This approach is more flexible as it allows you to add icons, styling, etc. to your label:

<ng-template cdkStepLabel>
 <span class="icon is-medium">
  <fa-icon [icon]="faPerson" size="fa-lg"></fa-icon>
 </span>
 <span>Personal Details</span>
</ng-template>

Working with Forms#

Okay, so far, we have learned how we can build a custom stepper using Angular CDK and how to use it. Next, we will learn how we can use our custom stepper with Forms.

Remember to import ReactiveFormsModule to your module:

// other imports
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [
   // ...
  ],
  imports: [
    // ...
    ReactiveFormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Single Form#

In this scenario, we will have a single form across the whole stepper. To achieve this, we will use FormArrays to create a form for each step. For instance, if we have 3 steps, we will need a form group for each step, each as part of the form array. The form array will then be a field in a larger form for the whole stepper. This will allow us to also have validator running, ensuring that we can validate different form fields if we want to. We can then submit the form at the last step of the stepper.

Component Class#

We will start by defining our form – frmStepper – as a component prop:

frmStepper: FormGroup;

Then, inside our form group, we are going to define a field called steps, which will be a FormArray. And then, inside the FormArray, we can create the form groups for each of our steps:

this.frmStepper = this.fb.group({
 steps: this.fb.array([
  this.fb.group({
   // ... form controls for our step
  }),
  // ... more form groups for each step we have
 ]),
});

Template#

Inside our component template, we will have a form wrap around our stepper component. Then, we will add the formArrayName directive, with the form field name steps as its name:

<form [formGroup]="frmStepper">
 <app-my-stepper formArrayName="steps">
  <!-- cdk steps here -->
 </app-my-stepper>
</form>

Then, for each step, we will use the formGroupName directive which allows us to treat each of our form groups in the array as independent form groups. We will use their indexes as the value of our formGroupName directive. We will also add stepControl props to our step so that we can have validation before moving to the next step:

<cdk-step formGroupName="0" [stepControl]="formArray.get([0])">
  <!-- content here -->
</cdk-step>

To get the form group to use as the stepControl props value for our step, I have created a getter for FormArray, which returns the form group of the passed index.

// formArray getter
get formArray(): AbstractControl {
  return this.frmStepper.get('steps');
}

You can also enable and disable the next button, by checking if the form group for the step is valid.:

<button [disabled]="formArray.get([1]).invalid" type="button" cdkStepperNext>
 Next
</button>

With Multiple Forms Per Step#

The second option is to have multiple forms, instead of a single form. In this case, you will have a form group for each step.

Component Class#

We will start by defining different forms groups for each of the steps. For instance, our demo has 3 steps – personal details, address, and payment steps. In this case, we will need three different forms.

frmDetails = this.fb.group({
 // ... form fields here
});

frmAddress = this.fb.group({
 // ... form fields here
});

frmPayment = this.fb.group({
 // ... form fields here
});

Template#

The form shall wrap around each steps’ content, with three different forms instead of one big one.

<app-my-stepper >

 <cdk-step [stepControl]="frmDetails">
  <form (ngSubmit)="frmSubmit(frmDetails)" [formGroup]="frmDetails">
   <!-- form content here -->
  </form>
 </cdk-step>

 <cdk-step [stepControl]="frmAddress">
  <form (ngSubmit)="frmSubmit(frmDetails)" [formGroup]="frmAddress">
   <!-- form content here -->
  </form>
 </cdk-step>

 <cdk-step [stepControl]="frmPayment">
  <form (ngSubmit)="frmSubmit(frmDetails)" [formGroup]="frmPayment">
   <!-- form content here -->
  </form>
 </cdk-step>

</app-my-stepper>

For the stepControl prop, we shall use the appropriate form group as the value. For instance, for payment form, the stepControl value will be frmPayment as defined in the component class:

<cdk-step [stepControl]="frmPayment">
   <!-- form here -->
</cdk-step>

Conclusion#

In this article, we have learnt how to install and setup Angular CDK inside an Angular project. Then, we also learnt to build a stepper component that has the same look and feel as the rest of our application. We have also learnt how to use the stepper component in two different common scenarios – with a single form and multiple forms.

From here, we can extract the stepper component into a feature module. This is particularly helpful on large projects where Lazy Loaded implemented, as it allows the stepper feature to be available across your application while also being lazily loaded. You can take it a step further and share the stepper component across multiple projects within a workspace by using a tool like NX workspaces.

Source Code and Demo#

You can find the source code for this article here and the demo on Stack Blitz.

Extra Resources#