typescript xml 182 lines · 4 tabs

Multi-Step Signup Wizard in Angular with a Shared FormGroup and Stepper Service

Shared by codesnips Jul 2026
4 tabs
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Injectable()
export class SignupFormService {
  readonly form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      account: this.fb.group({
        email: ['', [Validators.required, Validators.email]],
        password: ['', [Validators.required, Validators.minLength(8)]],
      }),
      profile: this.fb.group({
        firstName: ['', Validators.required],
        lastName: ['', Validators.required],
        company: [''],
      }),
      confirm: this.fb.group({
        acceptedTerms: [false, Validators.requiredTrue],
      }),
    });
  }

  groupFor(key: string): FormGroup {
    return this.form.get(key) as FormGroup;
  }

  payload(): unknown {
    return this.form.getRawValue();
  }

  reset(): void {
    this.form.reset();
  }
}
4 files · typescript, xml Explain with highlit

This snippet builds a multi-step signup wizard where every step edits slices of one shared FormGroup, and a small service owns the navigation state. The key idea is that a wizard is fundamentally a single form split into views: rather than giving each step its own form and stitching values together at the end, one root FormGroup holds nested groups (account, profile, confirm) so the final payload is always the complete, validated form value.

In signup-form.service.ts, the SignupFormService constructs that root form once with FormBuilder. Because the form lives in a shared service rather than a component, step components can come and go without losing state, and the service can expose helpers like groupFor to hand each step its own nested group. This inversion — form in the service, views in the components — is what makes deep linking and back-navigation safe.

The SignupStepperService in signup-stepper.service.ts tracks the current index with a BehaviorSubject and derives currentStep$, canGoBack$, and isLastStep$ as observables. Navigation is guarded: next refuses to advance while the active step's FormGroup is invalid, calling markAllAsTouched() so validation errors surface immediately instead of silently blocking the button. This keeps the "you can't proceed with bad data" rule in one place instead of scattered across templates.

The steps themselves are declared as data in STEPS, each mapping a label to the nested control key it validates. That makes the wizard configurable — reordering or inserting a step is a data change, not a control-flow rewrite.

In signup-wizard.component.ts, the container wires the two services together. It renders the active step via *ngComponentOutlet or a switch, shows a progress indicator from currentStep$, and only enables submit when the whole root form is valid. onSubmit reads form.getRawValue() for the complete payload.

The trade-off is a little indirection: state lives outside components, so the services must be provided at the right level (here, scoped to the wizard route) to avoid leaking one signup's state into another. In exchange the pattern scales cleanly to conditional steps, resumable drafts, and per-step validation without turning the container into a giant stateful component.


Related snips

Share this code

Here's the card — post it anywhere.

Multi-Step Signup Wizard in Angular with a Shared FormGroup and Stepper Service — share card
Link copied