Wizards
When you have a large or complex form, the best user experience is to turn it into a multi step wizard so they can fill out bits of information at a time. Follow these best practices to streamline wizards.
Component structure
There should be one parent component, and a child component for each step/nested view. The job of the parent is to handle the routing between steps, and the children can focus on their individual step's form logic.
In the above file structure, step 1 has nested views. These views will still be managed by the parent however, so we keep them at the same level as the main steps even though they will appear nested in step 1's breadcrumbs.
One Form per Step
Rather than trying to connect all the steps together under 1 BwForm, give each step it's own form and emit the final object on form submission. This avoids complications with trying to validate steps when they are not visible in the dom.
State Management
Utilize BwSession's for keeping track of the user's progress through the wizard. The parent component will open a session, and will wait to close it until the user completes the final step. Some benefits to this approach is:
- Avoids spaghetti code from trying to keep all the values in memory.
- Users progress will not be lost if they refresh the page or leave and come back later. You have to explicitly close the session for the progress to be lost.
- BwSession's are not permanent, they are tied to sessionStorage. You get a little bit of persistence without needing to mantain storage.
Full Example
This is a rough example showcasing wizards and sessions
Note: For brevity, each step is directly inside the ng-template, but in production you should put them in their own components to keep the parent clean.
- Typescript
- HTML
import { CurrencyPipe, NgTemplateOutlet } from '@angular/common';
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, signal, viewChild, ViewEncapsulation } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { closeSession, createDialog, openSession } from 'bluewater'
import { OverlayDirective } from "bluewater-angular";
import { combineLatest } from 'rxjs';
@Component({
selector: 'app-root',
imports: [NgTemplateOutlet, OverlayDirective, CurrencyPipe],
templateUrl: './app.html',
styleUrl: './app.css',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
encapsulation: ViewEncapsulation.None
})
export class App {
step = signal(0)
session = openSession('WizardExample')
wizardComp = viewChild<ElementRef<HTMLBwWizardElement>>('wiz')
products = toSignal(this.session.get<{name: string,price:number}[]>('products'))
addProduct(data: any) {
const products = this.products() ?? []
this.session.set('products', [...products, data]).subscribe(() => {
this.step.set(1)
})
}
async done() {
const res = await firstValueFrom(combineLatest({
step1: this.session.get('step1'),
step2: this.session.get('step2'),
step3: this.session.get('step3')
}))
createDialog({
header: 'Wizard complete!',
message: JSON.stringify(res)
}).present()
closeSession(this.session).subscribe()
}
}
@let steps = [{label: 'Step 1'},{label:'Step 2'},{label:'Step 3'}];
<bw-modal size="lg" #modal backdrop-dismiss="false">
<bw-button slot="trigger">Open Wizard</bw-button>
@switch (step()) {
@case (0) {
<ng-template [ngTemplateOutlet]="step1" />
} @case (1) {
<ng-template [ngTemplateOutlet]="step2" />
} @case (1.5) {
<ng-template [ngTemplateOutlet]="step2Nested" />
} @case (2) {
<ng-template [ngTemplateOutlet]="step3" />
}
}
</bw-modal>
<ng-template #step1>
<div class="bw-overlay-container">
<bw-toolbar class="bw-overlay-header">
<bw-breadcrumbs>
<bw-breadcrumb>Step 1</bw-breadcrumb>
</bw-breadcrumbs>
<bw-discard-form form="step1form" slot="end" (discard)="modal.dismiss()">
<bw-icon-button size="lg" icon="close" />
</bw-discard-form>
</bw-toolbar>
<div class="bw-overlay-content">
<div class="flex-row align-center justify-center">
<bw-wizard #wiz [initialStep]="0" [steps]="steps"/>
</div>
<bw-divider spacing="lg"/>
<bw-form id="step1form" (dataSubmit)="session.set('step1', $event.detail).subscribe(); step.set(1)">
<bw-input label="Full Name" required name="fullName" />
</bw-form>
</div>
<bw-toolbar class="bw-overlay-footer">
<bw-button slot="end" form="step1form" type="submit">Next</bw-button>
</bw-toolbar>
</div>
</ng-template>
<ng-template #step2>
<div class="bw-overlay-container">
<bw-toolbar class="bw-overlay-header">
<bw-breadcrumbs>
<bw-breadcrumb>Step 2</bw-breadcrumb>
</bw-breadcrumbs>
<bw-discard-form form="step2form" slot="end" (discard)="modal.dismiss()">
<bw-icon-button size="lg" icon="close" />
</bw-discard-form>
</bw-toolbar>
<div class="bw-overlay-content">
<div class="flex-row align-center justify-center">
<bw-wizard #wiz [initialStep]="1" [steps]="steps"/>
</div>
<bw-divider spacing="lg"/>
<bw-form id="step2form" (dataSubmit)="session.set('step2', $event.detail).subscribe(); step.set(2)">
<div class="flex-row gap align-center">
<bw-input label="Product" style="flex: 1;" required name="product">
<bw-menu slot="dropdown">
@for (item of products(); track item.name) {
<bw-option [value]="item.name">{{item.name}} - {{item.price | currency}}</bw-option>
}
</bw-menu>
</bw-input>
<bw-icon-button variant="secondary" size="xs" icon="add" (click)="step.set(1.5)"/>
</div>
</bw-form>
</div>
<bw-toolbar class="bw-overlay-footer">
<bw-button slot="end" form="step2form" type="submit">Next</bw-button>
</bw-toolbar>
</div>
</ng-template>
<ng-template #step2Nested>
<div class="bw-overlay-container">
<bw-toolbar class="bw-overlay-header">
<bw-breadcrumbs>
<bw-breadcrumb (click)="step.set(1)">Step 2</bw-breadcrumb>
<bw-breadcrumb>Nested</bw-breadcrumb>
</bw-breadcrumbs>
<bw-discard-form form="step2Nestedform" slot="end" (discard)="modal.dismiss()">
<bw-icon-button size="lg" icon="close" />
</bw-discard-form>
</bw-toolbar>
<div class="bw-overlay-content">
<div class="flex-row align-center justify-center">
<bw-wizard #wiz [initialStep]="1" [steps]="steps"/>
</div>
<bw-divider spacing="lg"/>
<bw-form id="step2Nestedform" (dataSubmit)="addProduct($event.detail)">
<bw-input label="Name" required name="name" />
<bw-input label="Price" type="number" name="price"/>
</bw-form>
</div>
<bw-toolbar class="bw-overlay-footer">
<bw-button slot="end" form="step2Nestedform" type="submit">Next</bw-button>
</bw-toolbar>
</div>
</ng-template>
<ng-template #step3>
<div class="bw-overlay-container">
<bw-toolbar class="bw-overlay-header">
<bw-breadcrumbs>
<bw-breadcrumb>Step 3</bw-breadcrumb>
</bw-breadcrumbs>
<bw-discard-form form="step3form" slot="end" (discard)="modal.dismiss()">
<bw-icon-button size="lg" icon="close" />
</bw-discard-form>
</bw-toolbar>
<div class="bw-overlay-content">
<div class="flex-row align-center justify-center">
<bw-wizard #wiz [initialStep]="1" [steps]="steps"/>
</div>
<bw-divider spacing="lg"/>
<bw-form id="step3form" (dataSubmit)="session.set('step3', $event.detail).subscribe(); modal.dismiss(); done()">
<bw-input label="Shipping Address" required name="address"/>
</bw-form>
</div>
<bw-toolbar class="bw-overlay-footer">
<bw-button slot="end" form="step3form" type="submit">Next</bw-button>
</bw-toolbar>
</div>
</ng-template>