DEV Community

Preston Lamb
Preston Lamb

Posted on • Originally published at prestonlamb.com on

Multi Step Forms in Angular

tldr;

Forms with multiple steps are common in web applications, but they can be difficult to organize and manage the data. Using reactive forms in Angular, however, it can be less painful. In this article we'll take a look at one way to manage multi step forms in your Angular app.

For this article, we'll use an example form with multiple steps. This sample will have just two steps, but the principles here can be applied to forms with more steps as well. The first of our two steps gathers the name, address, and phone number of a user. The second step gets their email, username, and password. We'll talk about how to split the form up into two parts and how to submit the form when finished.

Multi Step Form Container

Before we make the component to manage the first step of the form, the step that gathers the personal information for the user, we need to create a container component which will hold the two form steps. This is where we will manage the data from the two subforms, as well as manage which step the user is on or wants to be on. Finally, we'll also submit the completed form from this component as well. So, for the first step, create a new component:

$ ng g c form-container
Enter fullscreen mode Exit fullscreen mode

Inside the component, add the following content:

type Step = 'personalInfo' | 'loginInfo';

export class FormContainerComponent implements OnInit {
  private currentStepBs: BehaviorSubject<Step> = new BehaviorSubject<Step>('personalInfo');
  public currentStep$: Observable<Step> = this.currentStepBs.asObservable();

  public userForm: FormGroup;

  constructor(private _fb: FormBuilder) {}

  ngOnInit() {
    this.userForm = this._fb.group({
      personalInfo: null,
      loginInfo: null
    });
  }

  subformInitialized(name: string, group: FormGroup) {
    this.userForm.setControl(name, group);
  }

  changeStep(currentStep: string, direction: 'forward' | 'back') {
    switch(currentStep) {
      case 'personalInfoStep':
        if (direction === 'forward') {
          this.currentStepBs.next('loginInfo');
        }
        break;
      case 'loginInfoStep':
        if (direction === 'back') {
          this.currentStepBs.next('personalInfo');
        }
        break;
    }
  }

  submitForm() {
      const formValues = this.userForm.value;
      // submit the form with a service
  }
}
Enter fullscreen mode Exit fullscreen mode

This is the majority of the component's class file. It's all pretty straightforward, but let's break it down real quick. First, a BehaviorSubject is created, currentStepBs to keep track of which form step to show. The type is declared right before the component class, and is called Step. The possible values are strings representing the two steps. If you have more than two steps, you can provide one string for each step. Next, the userForm variable is created, but not initialized. That is done in the ngOnInit method. In the method, the form is intialized as a FormGroup with two attributes, one for each step of the form. Their value is initially null, but that will be updated when the step components appear on the screen. Next up is a method, subformInitialized which will be called when a form step is created and appears on the page. The method sets the control on the main userForm. Next is a method that changes the current step. It is pretty simple; it calls the next method on the BehaviorSubject and passes in the correct step. The last method is where the actual form submission will be done. This is where the form value will be used and passed into a service method to submit the value of the form.

Before moving on to the next section, let's look at the component's HTML file briefly. There won't be a lot here, but it will be good to look at.

<ng-container [ngSwitch]="currentStep$ | async">
  <app-personal-info 
    *ngSwitchCase="'personalInfo'" 
    [startingForm]="userForm.get('personalInfo').value" 
    (subformInitialized)="subformInitialized('personalInfo', $event)" 
    (changeStep)="changeStep('personalInfoStep', $event)"
  ></app-personal-info>
  <app-login-info 
    *ngSwitchCase="'loginInfo'" 
    [startingForm]="userForm.get('loginInfo').value" 
    (subformInitialized)="subformInitialized('loginInfo', $event)" 
    (changeStep)="changeStep('loginInfoStep', 'back')"
    (submitForm)="submitForm()"
  ></app-login-info>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

The two components that manage each step of the form are on the page. Only one is visible at a time, and they each get a starting value (to show previously entered information if they go back a step), as well as have Outputs for the subform being initialized and changing steps. The second step also has a submitForm output.

Now that our container component is created and everything is set up, we can move on to creating the PersonalInfo component, which will contain the form to gather the personal info.

Individual Steps

Let's begin by creating the component for the personal info step:

$ ng g c personal-info
Enter fullscreen mode Exit fullscreen mode

Next, let's look at the class file for the component.

export class PersonalInfoComponent implements OnInit {
  @Input() startingForm: FormGroup;
  @Output() subformInitialized: EventEmitter<FormGroup> = new EventEmitter<FormGroup>();
  @Output() changeStep: EventEmitter<boolean> = new EventEmitter<boolean>();
  public personalInfoForm: FormGroup;

  constructor(private _fb: FormBuilder) {}

  ngOnInit() {
    if (this.startingForm) {
      this.personalInfoForm = this.startingForm;
    } else {
      this.personalInfoForm = this._fb.group({
        firstName: '',
        lastName: '',
        // ... continue with the other fields
      })
    }

    this.subformInitialized.emit(this.personalInfoForm);
  }

  doChangeStep(direction: 'forward') {
    this.changeStep.emit(direction);
  }
}
Enter fullscreen mode Exit fullscreen mode

This component is a lot more simple than the container component. A form is created in the ngOnInit method, either from the data that's passed in to the component or with the FormBuilder. After the form is created, its value is emitted to the parent so the form values can be tracked in the parent component. There's a doChangeStep method that will emit forward and move the user to the next step. The template of the component will just be the form inputs as well as a "Next" button.

We won't cover creating the login info component, but it will be the same as this component, but will only allow the user to go back, and will allow the user to click a submit button to take them on to the next portion of the app after they've filled out the forms.

Conclusion

This method of managing complex forms has worked out really well for us. By organizing the forms like this each step can use its own reactive form with form validation and all of the other benefits of reactive forms. The parent container controls all the data from each step automatically. When it's time to submit the form, each step's data is contained in a separate attribute of the form.value object on the parent component. We've been able to easily add custom validators to the forms (one of my favorite features of reactive forms) which would have been more difficult with template driven forms.

This is also just one way to manage multi step forms in Angular. I'm sure you can achieve the same result with template driven forms, or in other manners. This way has been pretty straightforward and not overly complex. Most importantly, it worked for our situation. Hopefully it helps you as well

Top comments (0)