Lazy loaded stepper base on a route in Angular

A stepper is a type of input control that allows the user to increment or decrement a numerical value using predefined steps. See how to create a stepper with lazy loaded steps in Angular.

You can view the source code for this project by following this link: GitHub

Demo

Defining base schema

First of all, we need to define how our component should look. Because we want to lazy load steps individuals, we have to define router-outlet inside the stepper’s template. To define which header is connected to which path, we have to also provide a path name to the appStepLabel directive.

<app-stepper>
<ng-template appStepLabel="user"> User form </ng-template>
<ng-template appStepLabel="address"> Address form </ng-template>
<ng-template appStepLabel="account"> Account form </ng-template>
<ng-template appStepLabel="summary"> Summary </ng-template>
<router-outlet></router-outlet>
</app-stepper>

Routes configuration

After adding router-outlet it is time to define routes. We do it like any other route. There is nothing special.

const routes: Routes = [
{
path: "user",
loadChildren: () => import("./modules/process/user"),
},
{
path: "address",
loadChildren: () => import("./modules/process/address"),
},
// more steps
];

Do not forget that you can also implement canActivate, canDeactivate or even outlet.

{
path: 'user',
loadChildren: () => import('./modules/process/user'),
outlet: 'stepper',
canActivate: [/* */],
canDeactivate: [/* */],
// more configuration
}

Creating StepLabelDirective

The directive has just to carry a template and information for which path it is. Whole logic will be placed in StepperComponent.

@Directive({
selector: "[appStepLabel]",
})
export class StepLabelDirective {
@Input({ required: true, alias: "appStepLabel" }) routerPath!: string;
constructor(public templateRef: TemplateRef<void>) {}
}

Building StepperComponent

Template

The template StepperComponent is very easy. It consists of two parts – displaying a header and content.

<ol>
<li *ngFor="let label of labels$ | async">
<ng-template [ngTemplateOutlet]="label"></ng-template>
</li>
</ol>
<ng-content></ng-content>

Logic

The component has to get defined labels and display in the template.

@Component({
/* */
})
export class StepperComponent implements AfterContentInit {
@ContentChildren(StepLabelDirective, { descendants: false })
labelsQueryList!: QueryList<StepLabelDirective>;
private contentInit$ = new ReplaySubject<void>(1);
labels$ = this.contentInit$.pipe(
switchMap(() => this.labelsQueryList.changes),
startWith(void 0),
map(() => this.labelsQueryList.toArray().map((el) => el.templateRef)),
);
ngAfterContentInit(): void {
this.contentInit$.next();
this.contentInit$.complete();
}
}

Label order is defined by the template. To change the order just reorder ng-template.

<!-- this label will be first -->
<ng-template appStepLabel="user"> User form </ng-template>
<!-- this label will be second -->
<ng-template appStepLabel="address"> Address form </ng-template>
<!-- etc -->
<ng-template appStepLabel="account"> Account form </ng-template>

Highlight active step

Because the current step is saved in a router, we have to get the current index based on the path. To do it we should take the path from the route config.

@Component({
/* */
})
export class StepperComponent implements AfterContentInit {
/* ... */
private activationStart$ = this.router.events.pipe(
filter((event): event is ActivationEnd => event instanceof ActivationEnd),
);
activeStepIndex$ = this.contentInit$.pipe(
switchMap(() => this.activationStart$),
map((event) => {
const path = event.snapshot.routeConfig?.path;
const labels = this.labelsQueryList.toArray();
return labels.findIndex((label) => label.routerPath === path);
}),
filter((index) => index !== -1),
map((index) => ({ index })),
);
/* ... */
}

Thanks to defining the variable we can highlight current and fulfilled steps. All that we need now is to add classes to the element based on logic.

<ng-container *ngIf="activeStepIndex$ | async as activeStepIndex">
<li
*ngFor="let label of labels$ | async; let i = index"
[class.done]="i < activeStepIndex.index"
[class.active]="i === activeStepIndex.index"
>
<ng-template [ngTemplateOutlet]="label"></ng-template>
</li>
</ng-container>

Because steps are defined as a route component, we go through stepper just like between normal pages.

<form id="addressForm">
<!-- ... -->
</form>
<div class="navigation">
<button type="button" routerLink="/user">Previous step</button>
<button form="addressForm" type="submit" routerLink="/account">Next step</button>
</div>

Do you like the content?

Your support helps me continue my work. Please consider making a donation.

Donations are accepted through PayPal or Stripe. You do not need a account to donate. All major credit cards are accepted.

Leave a comment