Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion goldens/cdk/stepper/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AfterContentInit } from '@angular/core';
import { AfterViewInit } from '@angular/core';
import { ElementRef } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { Field } from '@angular/forms/signals';
import { FormGroupDirective } from '@angular/forms';
import * as i0 from '@angular/core';
import { InjectionToken } from '@angular/core';
Expand Down Expand Up @@ -63,7 +64,7 @@ export class CdkStep implements OnChanges {
_showError(): boolean;
get state(): StepState;
set state(value: StepState);
stepControl: AbstractControl;
stepControl: StepControl;
stepLabel: CdkStepLabel;
// (undocumented)
_stepper: CdkStepper;
Expand Down Expand Up @@ -181,6 +182,9 @@ export const STEP_STATE: {
// @public
export type StepContentPositionState = 'previous' | 'current' | 'next';

// @public
export type StepControl = AbstractControl | Field<unknown>;

// @public
export const STEPPER_GLOBAL_OPTIONS: InjectionToken<StepperOptions>;

Expand Down
40 changes: 35 additions & 5 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
type NgForm,
type FormGroupDirective,
} from '@angular/forms';
import type {Field} from '@angular/forms/signals';
import {_getFocusedElementPierceShadowDom} from '../platform';
import {Observable, of as observableOf, Subject} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
Expand All @@ -56,6 +57,9 @@ export type StepContentPositionState = 'previous' | 'current' | 'next';
/** Possible orientation of a stepper. */
export type StepperOrientation = 'horizontal' | 'vertical';

/** Possible controls that can be assigned to a step. */
export type StepControl = AbstractControl | Field<unknown>;

/** Change event emitted on selection changes. */
export class StepperSelectionEvent {
/** Index of the step now selected. */
Expand Down Expand Up @@ -132,7 +136,7 @@ export class CdkStep implements OnChanges {
@ViewChild(TemplateRef, {static: true}) content!: TemplateRef<any>;

/** The top level abstract control of the step. */
@Input() stepControl!: AbstractControl;
@Input() stepControl!: StepControl;

/** Whether user has attempted to move away from the step. */
get interacted(): boolean {
Expand Down Expand Up @@ -195,7 +199,7 @@ export class CdkStep implements OnChanges {
return override;
}

return interacted && (!this.stepControl || this.stepControl.valid);
return interacted && (!this.stepControl || isValid(this.stepControl));
}
set completed(value: boolean) {
this._completedOverride.set(value);
Expand Down Expand Up @@ -253,7 +257,7 @@ export class CdkStep implements OnChanges {
private _customError = signal<boolean | null>(null);

private _getDefaultError() {
return this.interacted && !!this.stepControl?.invalid;
return this.interacted && !!this.stepControl && isInvalid(this.stepControl);
}

constructor() {
Expand Down Expand Up @@ -284,7 +288,7 @@ export class CdkStep implements OnChanges {
// 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();
reset(this.stepControl);
}
}

Expand Down Expand Up @@ -593,7 +597,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
.some(step => {
const control = step.stepControl;
const isIncomplete = control
? control.invalid || control.pending || !step.interacted
? isInvalid(control) || isPending(control) || !step.interacted
: !step.completed;
return isIncomplete && !step.optional && !step._completedOverride();
});
Expand All @@ -618,3 +622,29 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
return index > -1 && (!this.steps || index < this.steps.length);
}
}

function isField(value: StepControl): value is Field<unknown> {
return typeof value === 'function';
}

function isValid(control: StepControl): boolean {
return isField(control) ? control().valid() : control.valid;
}

function isInvalid(control: StepControl): boolean {
// Note: it's a bit redundant to have both `isValid` and `isInvalid`. We need both, because
// some internal apps mock out `invalid` specifically so `!valid` won't hit the mock.
return isField(control) ? control().invalid() : control.invalid;
}

function isPending(control: StepControl): boolean {
return isField(control) ? control().pending() : control.pending;
}

function reset(control: StepControl): void {
if (isField(control)) {
control().reset();
} else {
control.reset();
}
}
1 change: 1 addition & 0 deletions src/components-examples/material/stepper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export {StepperLazyContentExample} from './stepper-lazy-content/stepper-lazy-con
export {StepperResponsiveExample} from './stepper-responsive/stepper-responsive-example';
export {StepperHeaderPositionExample} from './stepper-header-position/stepper-header-position-example';
export {StepperAnimationsExample} from './stepper-animations/stepper-animations-example';
export {StepperSignalFormsExample} from './stepper-signal-forms/stepper-signal-forms-example';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.mat-stepper-horizontal {
margin-top: 8px;
}

.mat-mdc-form-field {
margin-top: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<button matButton="elevated" (click)="isLinear.set(!isLinear())">
{{!isLinear ? 'Enable linear mode' : 'Disable linear mode'}}
</button>

<mat-stepper [linear]="isLinear()" #stepper>
<mat-step [stepControl]="nameFormGroup">
<form (submit)="$event.preventDefault()">
<ng-template matStepLabel>Fill out your name</ng-template>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput placeholder="Last name, First name" [formField]="nameFormGroup.name">
</mat-form-field>
<div>
<button matButton matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="adddressFormGroup" label="Fill out your address">
<form (submit)="$event.preventDefault()">
<mat-form-field>
<mat-label>Address</mat-label>
<input matInput [formField]="adddressFormGroup.address" placeholder="Ex. 1 Main St, New York, NY">
</mat-form-field>
<div>
<button matButton matStepperPrevious>Back</button>
<button matButton matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel>Done</ng-template>
<p>You are now done.</p>
<div>
<button matButton matStepperPrevious>Back</button>
<button matButton (click)="stepper.reset()">Reset</button>
</div>
</mat-step>
</mat-stepper>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Component, signal} from '@angular/core';
import {form, required, FormField} from '@angular/forms/signals';
import {MatButtonModule} from '@angular/material/button';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatStepperModule} from '@angular/material/stepper';

/**
* @title Stepper using signal forms
*/
@Component({
selector: 'stepper-signal-forms-example',
templateUrl: 'stepper-signal-forms-example.html',
styleUrl: 'stepper-signal-forms-example.css',
imports: [FormField, MatButtonModule, MatFormFieldModule, MatInputModule, MatStepperModule],
})
export class StepperSignalFormsExample {
readonly nameFormGroup = form(signal({name: ''}), tree => {
required(tree.name);
});

readonly adddressFormGroup = form(signal({address: ''}), tree => {
required(tree.address);
});

readonly isLinear = signal(false);
}
6 changes: 6 additions & 0 deletions src/material/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ are completed.
</mat-step>
</mat-stepper>
```

#### Using Signal Forms
The stepper also supports passing in a Signal Forms field as the `stepControl`:

<!-- example(stepper-signal-forms) -->

### Types of steps

#### Optional step
Expand Down
Loading