diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts
index 70483c54efe5..12e21b813350 100644
--- a/src/cdk/stepper/stepper.ts
+++ b/src/cdk/stepper/stepper.ts
@@ -33,6 +33,12 @@ import {
numberAttribute,
inject,
} from '@angular/core';
+import {
+ ControlContainer,
+ type AbstractControl,
+ type NgForm,
+ type FormGroupDirective,
+} from '@angular/forms';
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
import {Observable, of as observableOf, Subject} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
@@ -100,7 +106,7 @@ export interface StepperOptions {
@Component({
selector: 'cdk-step',
exportAs: 'cdkStep',
- template: '',
+ template: '',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
@@ -113,11 +119,24 @@ export class CdkStep implements OnChanges {
/** Template for step label if it exists. */
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
+ /** Forms that have been projected into the step. */
+ @ContentChildren(
+ // Note: we look for `ControlContainer` here, because both `NgForm` and `FormGroupDirective`
+ // provides themselves as such, but we don't want to have a concrete reference to both of
+ // the directives. The type is marked as `Partial` in case we run into a class that provides
+ // itself as `ControlContainer` but doesn't have the same interface as the directives.
+ ControlContainer,
+ {
+ descendants: true,
+ },
+ )
+ protected _childForms: QueryList> | undefined;
+
/** Template for step content. */
@ViewChild(TemplateRef, {static: true}) content: TemplateRef;
/** The top level abstract control of the step. */
- @Input() stepControl: AbstractControlLike;
+ @Input() stepControl: AbstractControl;
/** Whether user has attempted to move away from the step. */
interacted = false;
@@ -204,6 +223,10 @@ export class CdkStep implements OnChanges {
}
if (this.stepControl) {
+ // Reset the forms since the default error state matchers will show errors on submit and we
+ // want the form to be back to its initial state (see #29781). Submitted state is on the
+ // individual directives, rather than the control, so we need to reset them ourselves.
+ this._childForms?.forEach(form => form.resetForm?.());
this.stepControl.reset();
}
}
@@ -556,54 +579,3 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
return index > -1 && (!this.steps || index < this.steps.length);
}
}
-
-/**
- * Simplified representation of an "AbstractControl" from @angular/forms.
- * Used to avoid having to bring in @angular/forms for a single optional interface.
- * @docs-private
- */
-interface AbstractControlLike {
- asyncValidator: ((control: any) => any) | null;
- dirty: boolean;
- disabled: boolean;
- enabled: boolean;
- errors: {[key: string]: any} | null;
- invalid: boolean;
- parent: any;
- pending: boolean;
- pristine: boolean;
- root: AbstractControlLike;
- status: string;
- readonly statusChanges: Observable;
- touched: boolean;
- untouched: boolean;
- updateOn: any;
- valid: boolean;
- validator: ((control: any) => any) | null;
- value: any;
- readonly valueChanges: Observable;
- clearAsyncValidators(): void;
- clearValidators(): void;
- disable(opts?: any): void;
- enable(opts?: any): void;
- get(path: (string | number)[] | string): AbstractControlLike | null;
- getError(errorCode: string, path?: (string | number)[] | string): any;
- hasError(errorCode: string, path?: (string | number)[] | string): boolean;
- markAllAsTouched(): void;
- markAsDirty(opts?: any): void;
- markAsPending(opts?: any): void;
- markAsPristine(opts?: any): void;
- markAsTouched(opts?: any): void;
- markAsUntouched(opts?: any): void;
- patchValue(value: any, options?: Object): void;
- reset(value?: any, options?: Object): void;
- setAsyncValidators(newValidator: (control: any) => any | ((control: any) => any)[] | null): void;
- setErrors(errors: {[key: string]: any} | null, opts?: any): void;
- setParent(parent: any): void;
- setValidators(newValidator: (control: any) => any | ((control: any) => any)[] | null): void;
- setValue(value: any, options?: Object): void;
- updateValueAndValidity(opts?: any): void;
- patchValue(value: any, options?: any): void;
- reset(formState?: any, options?: any): void;
- setValue(value: any, options?: any): void;
-}
diff --git a/tools/public_api_guard/cdk/stepper.md b/tools/public_api_guard/cdk/stepper.md
index de11cf1d9db7..6c82b9b106b9 100644
--- a/tools/public_api_guard/cdk/stepper.md
+++ b/tools/public_api_guard/cdk/stepper.md
@@ -4,15 +4,17 @@
```ts
+import { AbstractControl } from '@angular/forms';
import { AfterContentInit } from '@angular/core';
import { AfterViewInit } from '@angular/core';
import { ElementRef } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { FocusableOption } from '@angular/cdk/a11y';
+import { FormGroupDirective } from '@angular/forms';
import * as i0 from '@angular/core';
import * as i1 from '@angular/cdk/bidi';
import { InjectionToken } from '@angular/core';
-import { Observable } from 'rxjs';
+import { NgForm } from '@angular/forms';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { QueryList } from '@angular/core';
@@ -24,6 +26,7 @@ export class CdkStep implements OnChanges {
constructor(...args: unknown[]);
ariaLabel: string;
ariaLabelledby: string;
+ protected _childForms: QueryList> | undefined;
get completed(): boolean;
set completed(value: boolean);
// (undocumented)
@@ -55,12 +58,12 @@ export class CdkStep implements OnChanges {
select(): void;
_showError(): boolean;
state: StepState;
- stepControl: AbstractControlLike;
+ stepControl: AbstractControl;
stepLabel: CdkStepLabel;
// (undocumented)
_stepper: CdkStepper;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}