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>