8

In Angular, is it possible to have a linear stepper where the individual steps are separate components? For example:

<mat-horizontal-stepper [linear]="isLinear">
    <mat-step [stepControl]="firstFormGroup" label="Some Form">
        <first-component></first-component>
    </mat-step>
    <mat-step [stepControl]="secondFormGroup" label="Another Form">
        <second-component></second-component>
    </mat-step>
    <mat-step [stepControl]="thirdFormGroup" label="Review">
        <third-component></third-component>
    </mat-step>
</mat-horizontal-stepper>

When I try this, I receive the following error upon hitting the matStepperNext button:

TypeError: Cannot read property 'invalid' of undefined.

Michael
  • 1,036
  • 1
  • 11
  • 22
  • Can we see your component.ts code? My guess is that one or more of your stepcontrol variables is undefined in the component. – Brian Wright Nov 16 '17 at 15:00
  • @BrianWright So I tried to put together a minimal example on plnkr, but I must have screwed something up, because it tells me it can't find one of the components... https://embed.plnkr.co/5Yx4RTIrIHklRtH5rJHO/ (Totally different than the issue I'm talking about above). – Michael Nov 16 '17 at 16:43
  • No worries, see my answer below. Also, check this out: https://stackblitz.com/ – Brian Wright Nov 16 '17 at 17:33
  • @BrianWright Thanks! Stackblitz is way easier than plnkr. So, I've made the following example. I'm actually getting a separate issue - where the `linear` property `mat-horizontal-stepper` isn't being enforced. It's allowing you to skip ahead to a subsequent step even though the form isn't valid. However, I suspect that if that weren't the case, you'd see the same issue (the error when hitting the Next button). Here's the example: https://stackblitz.com/edit/angular-fjhcgm – Michael Nov 16 '17 at 20:08
  • Sure thing! Yeah this is much better. I'll take a look a little later, unless someone beats me to it! – Brian Wright Nov 16 '17 at 20:29
  • I have the same scenario. (y) – Jagruttam Panchal May 29 '18 at 12:44

4 Answers4

8

You can use sub-forms to resolve it. I actually gave a talk a few months ago in Angular-UP conference about it: https://www.youtube.com/watch?v=sb7tgsNF2Jk

The idea, in general, is to create the form in the child component, inject the controlContainer using DI and setting the local form to be the controlContainer form.

Child Component:

 @Component({
  selector: 'app-company-info',
  templateUrl: './company-info.component.html',
  styleUrls: ['./company-info.component.scss'],
  viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }]
})
export class CompanyInfoComponent implements OnInit {

  form: FormGroup;
  constructor(
    private ctrlContainer: FormGroupDirective,
    private fb: FormBuilder) { }

  ngOnInit() {

    this.form = this.ctrlContainer.form;

    this.form.addControl('company',
      this.fb.group({
        'companyName': this.fb.control(null, [Validators.required]),
        'numOfEmployees': this.fb.control(null, [Validators.required])});

  }

}

Parent Component (html):

<mat-horizontal-stepper [linear]="isLinear" #stepper>
  <mat-step [stepControl]="companyInfo">
    <ng-template matStepLabel>Fill out your name</ng-template>

    <form [formGroup]="companyInfo">
      <app-company-info></app-company-info>
    </form>

    <div>
      <button mat-button matStepperNext>Next</button>
    </div>
  </mat-step>
</mat-horizontal-stepper>

Parent Component (ts):

export class WizardComponent implements OnInit {

  isLinear = true;
  companyInfo: FormGroup;

  constructor(private _formBuilder: FormBuilder) {

  }

  ngOnInit() {

    this.companyInfo = this._formBuilder.group({
    });

  }

}
Eliran Eliassy
  • 1,590
  • 12
  • 25
  • Could you provide a working example? I've prepared this [stackblitz](https://stackblitz.com/edit/angular-ivy-zhvrs5?file=src/app/app.module.ts), so it should be quick :) – Christian Steinmeyer Sep 19 '20 at 06:14
  • Improves on both answers using the subform. This is the example on [stackblitz](https://stackblitz.com/edit/angular-ivy-576er7?file=src/app/company-info/company-info.component.html) – Bembem Nov 03 '20 at 08:22
5

Here is the solution that work for me.

<mat-horizontal-stepper [linear]="true" #stepper>
  <mat-step [stepControl]="selectAdvType">
    <ng-template matStepLabel>
      <div class="text-center">
        <mat-icon>queue_play_next</mat-icon><br /><span>Select Adv Type</span>
      </div>
    </ng-template>
    <app-advertisement-type></app-advertisement-type>
  </mat-step>
  <mat-step [stepControl]="selectAdvType">
    <ng-template matStepLabel>
      <div class="text-center">
        <mat-icon>directions_car</mat-icon><br /><span>Select Car</span>
      </div>
    </ng-template>
    <app-select-car-adv></app-select-car-adv>
  </mat-step>
  <mat-step>
    <ng-template matStepLabel>
      <div class="text-center">
        <mat-icon>description</mat-icon><br /><span>Select Features</span>
      </div>
    </ng-template>
    <div>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button (click)="stepper.reset()">Reset</button>
    </div>
  </mat-step>
</mat-horizontal-stepper>

Parent Ts file

@Component({
  selector: 'app-customer.create.advertisement',
  templateUrl: './customer.create.advertisement.component.html',
  styleUrls: ['./customer.create.advertisement.component.scss']
})
export class CustomerCreateAdvertisementComponent implements OnInit {
  isLinear = false;
  selectAdvType: FormGroup;
  constructor(private _formBuilder: FormBuilder) { }
  ngOnInit() {
    this.selectAdvType = this._formBuilder.group({
      firstCtrl: ['', Validators.required]
    });
  }
}

Child component

<form [formGroup]="firstFormGroup">
    <ng-template matStepLabel>Fill out your name</ng-template>
    <mat-form-field>
        <input matInput placeholder="Last name, First name" formControlName="firstCtrl" required>
    </mat-form-field>
    <div>
        <button mat-button matStepperNext>Next</button>
    </div>
</form>


@Component({
  selector: 'app-advertisement-type',
  templateUrl: './advertisement-type.component.html',
  styleUrls: ['./advertisement-type.component.scss']
})
export class AdvertisementTypeComponent implements OnInit {
  firstFormGroup: FormGroup;
  constructor(private _formBuilder: FormBuilder) { }

  ngOnInit() {
    this.firstFormGroup = this._formBuilder.group({
      firstCtrl: ['', Validators.required]
    });
  }

}
San Jaisy
  • 15,327
  • 34
  • 171
  • 290
  • 2
    Why we need to define two times(parent and child component ts file) the below form builder 'firstCtrl: ['', Validators.required]'. Suppose if i have 10 items in the child element form, Does it configure in the parent component also? – Jeyabalan Thavamani Jan 10 '19 at 06:25
2

Improving on @eliran-eliassy answer and @christian-steinmeyer question.

Parent.Component.ts

  export class ParentComponent implements OnInit {

  isLinear = true;
  companyInfo: FormGroup;

  constructor(private _formBuilder: FormBuilder) {

  }

  ngOnInit() {

    this.companyInfo = this._formBuilder.group({
    });

  }

}

Parent.Component.html

<mat-horizontal-stepper [linear]="isLinear" #stepper>
    <mat-step [stepControl]="companyInfo">
        <form [formGroup]="companyInfo">
            <ng-template matStepLabel>Fill out your name</ng-template>
            <app-company-info></app-company-info>
        </form>
        <div>
            <button mat-button matStepperNext>Next</button>
        </div>
    </mat-step>
</mat-horizontal-stepper>

Child.Component.ts -> this is the sub-form

export class ChildComponent implements OnInit {
  form: FormGroup;
  subForm: FormGroup;
  constructor(
    private ctrlContainer: FormGroupDirective,
    private fb: FormBuilder
  ) {}

  ngOnInit() {
    this.subForm = this.fb.group({
      companyName: [null, [Validators.required]],
      numOfEmployees: [null, [Validators.required]]
    });
    
    this.form = this.ctrlContainer.form;
    this.form.addControl("company", this.subForm);
  }
}

Child.Component.html

<div [formGroup]="subForm">
    <mat-form-field appearance="outline">
        <input matInput placeholder="Your Company Name" formControlName="companyName">
  </mat-form-field>
</div>

See this solution on stackblitz

Bembem
  • 249
  • 4
  • 12
0

Ok, I think I see a few issues:

<mat-horizontal-stepper [linear]="isLinear">
  <mat-step [stepControl]="foodFormGroup">
    <food-selection></food-selection>
  </mat-step>
  <mat-step [stepControl]="pieFormGroup">
    <pie-selection></pie-selection>
  </mat-step>
</mat-horizontal-stepper>

foodFormGroup and pieFormGroup need to be defined in your tough-choice.component.ts file (Which, btw is misspelled in your example code)

Here's an example (from the docs) of how this might look:

import {Component} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';

/**
 * @title Stepper overview
 */
@Component({
  selector: 'stepper-overview-example',
  templateUrl: 'stepper-overview-example.html',
  styleUrls: ['stepper-overview-example.css']
})
export class StepperOverviewExample {
  isLinear = false;
  firstFormGroup: FormGroup;
  secondFormGroup: FormGroup;

  constructor(private _formBuilder: FormBuilder) { }

  ngOnInit() {
    this.firstFormGroup = this._formBuilder.group({
      firstCtrl: ['', Validators.required]
    });
    this.secondFormGroup = this._formBuilder.group({
      secondCtrl: ['', Validators.required]
    });
  }
}

Also, I don't see a module.ts file in your example. That's where you would want to import your @angular/material modules, not in the component files.

I'd suggest just giving the Angular Material documentation a once-over. https://material.angular.io/components/stepper/overview

Brian Wright
  • 827
  • 6
  • 9
  • Thanks. I appreciate the heads-up on the typo, that was driving me up a wall. I responded to your comment on the main post with a stackblitz demo. – Michael Nov 16 '17 at 20:09