Skip to content

Commit

Permalink
fix(cdk/stepper): reset submitted state when resetting stepper
Browse files Browse the repository at this point in the history
`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.
  • Loading branch information
crisbeto committed Oct 4, 2024
1 parent 9eb1f86 commit 02823c0
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 3 deletions.
26 changes: 24 additions & 2 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -101,7 +106,7 @@ export interface StepperOptions {
@Component({
selector: 'cdk-step',
exportAs: 'cdkStep',
template: '<ng-template><ng-content></ng-content></ng-template>',
template: '<ng-template><ng-content/></ng-template>',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
Expand All @@ -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<Partial<NgForm | FormGroupDirective>> | undefined;

/** Template for step content. */
@ViewChild(TemplateRef, {static: true}) content: TemplateRef<any>;

Expand Down Expand Up @@ -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();
}
}
Expand Down
5 changes: 4 additions & 1 deletion tools/public_api_guard/cdk/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,6 +26,7 @@ export class CdkStep implements OnChanges {
constructor(...args: unknown[]);
ariaLabel: string;
ariaLabelledby: string;
protected _childForms: QueryList<Partial<NgForm | FormGroupDirective>> | undefined;
get completed(): boolean;
set completed(value: boolean);
// (undocumented)
Expand Down Expand Up @@ -60,7 +63,7 @@ export class CdkStep implements OnChanges {
// (undocumented)
_stepper: CdkStepper;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CdkStep, never>;
}
Expand Down

0 comments on commit 02823c0

Please sign in to comment.