Skip to content

Commit 5bed094

Browse files
committed
fix(cdk/stepper): reset submitted state when resetting stepper
`CdkStepper` has a `reset` method that reset all the controls to their initial values, but that won't necessarily put the form into its initial state, because form controls also show errors on submit by default and `AbstractControl.reset` won't reset the submitted state. These changes add a call to reset all child forms to their unsubmitted state. Fixes #29781. (cherry picked from commit 02823c0)
1 parent fa43a24 commit 5bed094

File tree

2 files changed

+28
-3
lines changed

2 files changed

+28
-3
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ import {
3535
booleanAttribute,
3636
numberAttribute,
3737
} from '@angular/core';
38-
import {type AbstractControl} from '@angular/forms';
38+
import {
39+
ControlContainer,
40+
type AbstractControl,
41+
type NgForm,
42+
type FormGroupDirective,
43+
} from '@angular/forms';
3944
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
4045
import {Observable, of as observableOf, Subject} from 'rxjs';
4146
import {startWith, takeUntil} from 'rxjs/operators';
@@ -103,7 +108,7 @@ export interface StepperOptions {
103108
@Component({
104109
selector: 'cdk-step',
105110
exportAs: 'cdkStep',
106-
template: '<ng-template><ng-content></ng-content></ng-template>',
111+
template: '<ng-template><ng-content/></ng-template>',
107112
encapsulation: ViewEncapsulation.None,
108113
changeDetection: ChangeDetectionStrategy.OnPush,
109114
standalone: true,
@@ -115,6 +120,19 @@ export class CdkStep implements OnChanges {
115120
/** Template for step label if it exists. */
116121
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
117122

123+
/** Forms that have been projected into the step. */
124+
@ContentChildren(
125+
// Note: we look for `ControlContainer` here, because both `NgForm` and `FormGroupDirective`
126+
// provides themselves as such, but we don't want to have a concrete reference to both of
127+
// the directives. The type is marked as `Partial` in case we run into a class that provides
128+
// itself as `ControlContainer` but doesn't have the same interface as the directives.
129+
ControlContainer,
130+
{
131+
descendants: true,
132+
},
133+
)
134+
protected _childForms: QueryList<Partial<NgForm | FormGroupDirective>> | undefined;
135+
118136
/** Template for step content. */
119137
@ViewChild(TemplateRef, {static: true}) content: TemplateRef<any>;
120138

@@ -206,6 +224,10 @@ export class CdkStep implements OnChanges {
206224
}
207225

208226
if (this.stepControl) {
227+
// Reset the forms since the default error state matchers will show errors on submit and we
228+
// want the form to be back to its initial state (see #29781). Submitted state is on the
229+
// individual directives, rather than the control, so we need to reset them ourselves.
230+
this._childForms?.forEach(form => form.resetForm?.());
209231
this.stepControl.reset();
210232
}
211233
}

tools/public_api_guard/cdk/stepper.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import { Directionality } from '@angular/cdk/bidi';
1212
import { ElementRef } from '@angular/core';
1313
import { EventEmitter } from '@angular/core';
1414
import { FocusableOption } from '@angular/cdk/a11y';
15+
import { FormGroupDirective } from '@angular/forms';
1516
import * as i0 from '@angular/core';
1617
import * as i1 from '@angular/cdk/bidi';
1718
import { InjectionToken } from '@angular/core';
19+
import { NgForm } from '@angular/forms';
1820
import { OnChanges } from '@angular/core';
1921
import { OnDestroy } from '@angular/core';
2022
import { QueryList } from '@angular/core';
@@ -26,6 +28,7 @@ export class CdkStep implements OnChanges {
2628
constructor(_stepper: CdkStepper, stepperOptions?: StepperOptions);
2729
ariaLabel: string;
2830
ariaLabelledby: string;
31+
protected _childForms: QueryList<Partial<NgForm | FormGroupDirective>> | undefined;
2932
get completed(): boolean;
3033
set completed(value: boolean);
3134
// (undocumented)
@@ -62,7 +65,7 @@ export class CdkStep implements OnChanges {
6265
// (undocumented)
6366
_stepper: CdkStepper;
6467
// (undocumented)
65-
static ɵcmp: i0.ɵɵComponentDeclaration<CdkStep, "cdk-step", ["cdkStep"], { "stepControl": { "alias": "stepControl"; "required": false; }; "label": { "alias": "label"; "required": false; }; "errorMessage": { "alias": "errorMessage"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "state": { "alias": "state"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "optional": { "alias": "optional"; "required": false; }; "completed": { "alias": "completed"; "required": false; }; "hasError": { "alias": "hasError"; "required": false; }; }, { "interactedStream": "interacted"; }, ["stepLabel"], ["*"], true, never>;
68+
static ɵcmp: i0.ɵɵComponentDeclaration<CdkStep, "cdk-step", ["cdkStep"], { "stepControl": { "alias": "stepControl"; "required": false; }; "label": { "alias": "label"; "required": false; }; "errorMessage": { "alias": "errorMessage"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "state": { "alias": "state"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "optional": { "alias": "optional"; "required": false; }; "completed": { "alias": "completed"; "required": false; }; "hasError": { "alias": "hasError"; "required": false; }; }, { "interactedStream": "interacted"; }, ["stepLabel", "_childForms"], ["*"], true, never>;
6669
// (undocumented)
6770
static ɵfac: i0.ɵɵFactoryDeclaration<CdkStep, [null, { optional: true; }]>;
6871
}

0 commit comments

Comments
 (0)