Skip to content

Commit 02823c0

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 angular#29781.
1 parent 9eb1f86 commit 02823c0

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
@@ -33,7 +33,12 @@ import {
3333
numberAttribute,
3434
inject,
3535
} from '@angular/core';
36-
import {type AbstractControl} from '@angular/forms';
36+
import {
37+
ControlContainer,
38+
type AbstractControl,
39+
type NgForm,
40+
type FormGroupDirective,
41+
} from '@angular/forms';
3742
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
3843
import {Observable, of as observableOf, Subject} from 'rxjs';
3944
import {startWith, takeUntil} from 'rxjs/operators';
@@ -101,7 +106,7 @@ export interface StepperOptions {
101106
@Component({
102107
selector: 'cdk-step',
103108
exportAs: 'cdkStep',
104-
template: '<ng-template><ng-content></ng-content></ng-template>',
109+
template: '<ng-template><ng-content/></ng-template>',
105110
encapsulation: ViewEncapsulation.None,
106111
changeDetection: ChangeDetectionStrategy.OnPush,
107112
standalone: true,
@@ -114,6 +119,19 @@ export class CdkStep implements OnChanges {
114119
/** Template for step label if it exists. */
115120
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
116121

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

@@ -205,6 +223,10 @@ export class CdkStep implements OnChanges {
205223
}
206224

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

tools/public_api_guard/cdk/stepper.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { AfterViewInit } from '@angular/core';
1010
import { ElementRef } from '@angular/core';
1111
import { EventEmitter } from '@angular/core';
1212
import { FocusableOption } from '@angular/cdk/a11y';
13+
import { FormGroupDirective } from '@angular/forms';
1314
import * as i0 from '@angular/core';
1415
import * as i1 from '@angular/cdk/bidi';
1516
import { InjectionToken } from '@angular/core';
17+
import { NgForm } from '@angular/forms';
1618
import { OnChanges } from '@angular/core';
1719
import { OnDestroy } from '@angular/core';
1820
import { QueryList } from '@angular/core';
@@ -24,6 +26,7 @@ export class CdkStep implements OnChanges {
2426
constructor(...args: unknown[]);
2527
ariaLabel: string;
2628
ariaLabelledby: string;
29+
protected _childForms: QueryList<Partial<NgForm | FormGroupDirective>> | undefined;
2730
get completed(): boolean;
2831
set completed(value: boolean);
2932
// (undocumented)
@@ -60,7 +63,7 @@ export class CdkStep implements OnChanges {
6063
// (undocumented)
6164
_stepper: CdkStepper;
6265
// (undocumented)
63-
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>;
66+
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>;
6467
// (undocumented)
6568
static ɵfac: i0.ɵɵFactoryDeclaration<CdkStep, never>;
6669
}

0 commit comments

Comments
 (0)