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.

May 6, 2023 angularangular-16

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.

1
<app-stepper>
2
<ng-template appStepLabel="user"> User form </ng-template>
3
<ng-template appStepLabel="address"> Address form </ng-template>
4
<ng-template appStepLabel="account"> Account form </ng-template>
5
<ng-template appStepLabel="summary"> Summary </ng-template>
6
7
<router-outlet></router-outlet>
8
</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.

1
const routes: Routes = [
2
{
3
path: "user",
4
loadChildren: () => import("./modules/process/user"),
5
},
6
{
7
path: "address",
8
loadChildren: () => import("./modules/process/address"),
9
},
10
// more steps
11
];

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

1
{
2
path: 'user',
3
loadChildren: () => import('./modules/process/user'),
4
outlet: 'stepper',
5
canActivate: [/* */],
6
canDeactivate: [/* */],
7
// more configuration
8
}

Creating StepLabelDirective

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

1
@Directive({
2
selector: "[appStepLabel]",
3
})
4
export class StepLabelDirective {
5
@Input({ required: true, alias: "appStepLabel" }) routerPath!: string;
6
7
constructor(public templateRef: TemplateRef<void>) {}
8
}

Building StepperComponent

Template

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

1
<ol>
2
<li *ngFor="let label of labels$ | async">
3
<ng-template [ngTemplateOutlet]="label"></ng-template>
4
</li>
5
</ol>
6
7
<ng-content></ng-content>

Logic

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

1
@Component({
2
/* */
3
})
4
export class StepperComponent implements AfterContentInit {
5
@ContentChildren(StepLabelDirective, { descendants: false })
6
labelsQueryList!: QueryList<StepLabelDirective>;
7
8
private contentInit$ = new ReplaySubject<void>(1);
9
10
labels$ = this.contentInit$.pipe(
11
switchMap(() => this.labelsQueryList.changes),
12
startWith(void 0),
13
map(() => this.labelsQueryList.toArray().map((el) => el.templateRef)),
14
);
15
16
ngAfterContentInit(): void {
17
this.contentInit$.next();
18
this.contentInit$.complete();
19
}
20
}

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

1
<!-- this label will be first -->
2
<ng-template appStepLabel="user"> User form </ng-template>
3
4
<!-- this label will be second -->
5
<ng-template appStepLabel="address"> Address form </ng-template>
6
7
<!-- etc -->
8
<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.

1
@Component({
2
/* */
3
})
4
export class StepperComponent implements AfterContentInit {
5
/* ... */
6
7
private activationStart$ = this.router.events.pipe(
8
filter((event): event is ActivationEnd => event instanceof ActivationEnd),
9
);
10
11
activeStepIndex$ = this.contentInit$.pipe(
12
switchMap(() => this.activationStart$),
13
map((event) => {
14
const path = event.snapshot.routeConfig?.path;
15
const labels = this.labelsQueryList.toArray();
16
17
return labels.findIndex((label) => label.routerPath === path);
18
}),
19
filter((index) => index !== -1),
20
map((index) => ({ index })),
21
);
22
23
/* ... */
24
}

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.

1
<ng-container *ngIf="activeStepIndex$ | async as activeStepIndex">
2
<li
3
*ngFor="let label of labels$ | async; let i = index"
4
[class.done]="i < activeStepIndex.index"
5
[class.active]="i === activeStepIndex.index"
6
>
7
<ng-template [ngTemplateOutlet]="label"></ng-template>
8
</li>
9
</ng-container>

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

1
<form id="addressForm">
2
<!-- ... -->
3
</form>
4
5
<div class="navigation">
6
<button type="button" routerLink="/user">Previous step</button>
7
<button form="addressForm" type="submit" routerLink="/account">Next step</button>
8
</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