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; }