From 26afcf88cedd391205a8bd7d339bb474f28aa042 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 4 Oct 2024 09:55:44 +0200 Subject: [PATCH 1/2] fix(cdk/stepper): remove mock of forms type Previously we were mocking out the `AbstractControl` type, because we didn't want to bring in additional code for the optional forms integration. That's no longer necessary, because we can use type-only imports. --- src/cdk/stepper/stepper.ts | 54 +-------------------------- tools/public_api_guard/cdk/stepper.md | 4 +- 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 70483c54efe5..ad0e2e10f89b 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -33,6 +33,7 @@ import { numberAttribute, inject, } from '@angular/core'; +import {type AbstractControl} from '@angular/forms'; import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {Observable, of as observableOf, Subject} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; @@ -117,7 +118,7 @@ export class CdkStep implements OnChanges { @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; @@ -556,54 +557,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..74de9c630274 100644 --- a/tools/public_api_guard/cdk/stepper.md +++ b/tools/public_api_guard/cdk/stepper.md @@ -4,6 +4,7 @@ ```ts +import { AbstractControl } from '@angular/forms'; import { AfterContentInit } from '@angular/core'; import { AfterViewInit } from '@angular/core'; import { ElementRef } from '@angular/core'; @@ -12,7 +13,6 @@ import { FocusableOption } from '@angular/cdk/a11y'; import * as i0 from '@angular/core'; import * as i1 from '@angular/cdk/bidi'; import { InjectionToken } from '@angular/core'; -import { Observable } from 'rxjs'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { QueryList } from '@angular/core'; @@ -55,7 +55,7 @@ export class CdkStep implements OnChanges { select(): void; _showError(): boolean; state: StepState; - stepControl: AbstractControlLike; + stepControl: AbstractControl; stepLabel: CdkStepLabel; // (undocumented) _stepper: CdkStepper; From 11b7feae4036bfc6c3eba1d936738a25a748bc9e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 4 Oct 2024 10:08:27 +0200 Subject: [PATCH 2/2] 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. --- src/cdk/stepper/stepper.ts | 26 ++++++++++++++++++++++++-- tools/public_api_guard/cdk/stepper.md | 5 ++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index ad0e2e10f89b..12e21b813350 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -33,7 +33,12 @@ import { numberAttribute, inject, } from '@angular/core'; -import {type AbstractControl} from '@angular/forms'; +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'; @@ -101,7 +106,7 @@ export interface StepperOptions { @Component({ selector: 'cdk-step', exportAs: 'cdkStep', - template: '', + template: '', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, @@ -114,6 +119,19 @@ 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; @@ -205,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(); } } diff --git a/tools/public_api_guard/cdk/stepper.md b/tools/public_api_guard/cdk/stepper.md index 74de9c630274..6c82b9b106b9 100644 --- a/tools/public_api_guard/cdk/stepper.md +++ b/tools/public_api_guard/cdk/stepper.md @@ -10,9 +10,11 @@ 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 { 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) @@ -60,7 +63,7 @@ export class CdkStep implements OnChanges { // (undocumented) _stepper: CdkStepper; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }