angular - Angular Material 2 Stepper Controls
Asked Answered
B

2

6

I set up a linear stepper using the Angular Material 2 Stepper.

I have forms in different components (component-a, component-b, component-c). In my main container component (container-component) I want to have linear stepper that 'steps' through each component when their form is valid.

Is there some sort of functionality to talk up to the stepControl in the stepper to make it properly work?

I have attached documentation for the stepper and a StackBlitz version of the application. Also, a link to the repo I have working as well.

Material Stepper Component: https://material.angular.io/components/stepper/overview

StackBlitz: https://stackblitz.com/edit/angular-material2-issue-mjmu9u?file=app/app.component.ts

Github: https://github.com/sam11385

Bastinado answered 25/10, 2017 at 18:5 Comment(0)
V
11

We had the exact same problem, and found a working solution after several days of trial and error. Basically since the steps are divided into several components that each define a form, and that their corresponding stepControl must be in the parent, the FormGroup in the child component must be sent to the parent component that defines the stepper. After creating the FormGroup, emit an event and have the parent listen to that event, and pass the FormGroup through that event. You can apply this for all children, creating a separate event for each and a separate listener in the parent for each emitter.

In the child component:

  1. declare the event

    @Output() notify: EventEmitter<FormGroup> = new EventEmitter<FormGroup>();                                                                                                         
    
  2. emit the formGroup to the parent component

    this.notify.emit(this.myFormGroup);                                                                                                                                                
    

In the parent component containing the mat-stepper's stepControl(s):

  1. In the component(.ts), add a function that will receive and set the formgroup:

    myFormGroup: FormGroup;
    onNotify(formGroup: FormGroup): void {
      this.myFormGroup = formGroup;
    }
    
  2. In the html, add the notification listener:

    <mat-step [completed]="false" [stepControl]="myFormGroup">
      <ng-template matStepLabel>Step 1</ng-template>
      <app-child (notify)='onNotify($event)'></app-child>
    </mat-step>                                                                                                                                            
    

Nested components explanations: https://www.themarketingtechnologist.co/building-nested-components-in-angular-2/

Venal answered 5/12, 2017 at 19:38 Comment(4)
great answer !!Hessian
Brilliant answer! ThanksMotorboat
that is what i did and it does not work for me, could you check this?#57909818Anal
I get this ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: '[object Object]'.Sweptback
S
2

The answer above is spot on, however I encountered a few deeper issues with this implementation in my particular use case, and so I solved it with a shared service:

form-management.service.ts

import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class FormManagementService {

    private formSource: Subject<FormGroup> = new Subject();
    locationForm: Observable<FormGroup> = this.formSource.asObservable();

    formReady(form: FormGroup): void {
        this.formSource.next(form);
    }
}

parent.component.ts

import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormManagementService } from './form-management.service';

@Component({
    ....
    providers: [ FormManagementService ]
})
export class ParentComponent {

    form: FormGroup;

    constructor(private formManagementService: FormManagementService) {
        this.formManagementService.locationForm.subscribe(form => this.form = form);
    }

}

Note, here we are subscribing to changes on the shared service, which will pass a form into the component when we need it. When the parent component receives the form, it populates the FormGroup.

Also note that we are providing the shared service here as part of the component decorator. You can also provide it in your feature module, if that's the way your code is structured.

child.component.ts

import { Component } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { FormManagementService } from './form-management.service';

@Component({ ... })
export class ChildComponent {

    form: FormGroup;

    constructor(private formManagementService: FormManagementService,
                private formBuilder: FormBuilder) {
        this.form = this.formBuilder.group({});
        this.formManagementService.formReady(this.form);
    }

}

Here in the child component we initialise the form and then push it through the shared service, which will in turn populate it in the parent component.

Note, I found that if I did this anywhere other than the constructor I encountered the ExpressionChangedAfterItHasBeenCheckedError, even when I did this in ngAfterContentInit. Perhaps someone else will have a further advancement on my solution to resolve this.

Anyway, hope this is of some help to someone out there, as this issue had been plaguing me for a couple of weeks.

Suggest answered 23/2, 2018 at 13:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.