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>
Navigating between steps
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>